From 2a46440b3ea70224adcf6e2300d1acf96008ebbc Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 5 Apr 2026 17:18:06 -0700 Subject: [PATCH 1/5] update: add relaxation with MACE --- ...reate_interface_with_relaxation_mace.ipynb | 654 ++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 other/experiments/create_interface_with_relaxation_mace.ipynb diff --git a/other/experiments/create_interface_with_relaxation_mace.ipynb b/other/experiments/create_interface_with_relaxation_mace.ipynb new file mode 100644 index 00000000..67d71539 --- /dev/null +++ b/other/experiments/create_interface_with_relaxation_mace.ipynb @@ -0,0 +1,654 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create an Interface with ZSL and Relax it with MACE\n", + "\n", + "Use Zur and McGill superlattices matching [algorithm](https://doi.org/10.1063/1.3330840) to create interfaces between two materials using the `mat3ra-made` [implementation](https://github.com/Exabyte-io/made).\n", + "\n", + "

Usage

\n", + "\n", + "1. Drop the materials files into the \"uploads\" folder in the JupyterLab file browser\n", + "1. Set Input Parameters below or use the default values\n", + "1. Click \"Run\" > \"Run All\" to run all cells\n", + "1. Wait for the run to complete. Scroll down to view cell results.\n", + "1. Review the strain plot and modify parameters as needed\n", + "\n", + "## Methodology\n", + "\n", + "1. Load materials from JSON files and create substrate and film slabs via `mat3ra-made`\n", + "2. Run ZSL strain matching and plot strain vs interface area\n", + "3. Create the selected interface and convert to ASE atoms with `to_ase()`\n", + "4. Relax the interface with MACE-MP-0 and visualize convergence\n", + "5. Compute interface binding energy decomposition" + ], + "id": "9515ed910c085db8" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Set Input Parameters\n", + "\n", + "### 1.1. Substrate and Film Materials" + ], + "id": "4737d145950b1cc8" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "\n", + "SUBSTRATE_NAME = \"Nickel\"\n", + "FILM_NAME = \"Graphene\"\n", + "SUBSTRATE_INDEX = 0\n", + "FILM_INDEX = 1" + ], + "id": "ef377fcae54adf52", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.2. Slab Parameters" + ], + "id": "401cb093b302dbb1" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "SUBSTRATE_MILLER_INDICES = (1, 1, 1)\n", + "SUBSTRATE_THICKNESS = 6 # in atomic layers\n", + "SUBSTRATE_TERMINATION_FORMULA = None # if None, the first termination is used\n", + "\n", + "FILM_MILLER_INDICES = (0, 0, 1)\n", + "FILM_THICKNESS = 1 # in atomic layers\n", + "FILM_TERMINATION_FORMULA = None # if None, the first termination is used\n", + "\n", + "USE_CONVENTIONAL_CELL = True" + ], + "id": "edd2f829bc0a72ce", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.3. Interface and Relaxation Parameters" + ], + "id": "8fecb819abaa2ca7" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "INTERFACE_DISTANCE = None # gap between substrate and film, in Angstrom, if None, the distance will be set to the sum of the covalent radii of the two materials\n", + "INTERFACE_VACUUM = 10.0 # vacuum over film, in Angstrom\n", + "REDUCE_RESULT_CELL_TO_PRIMITIVE = True\n", + "\n", + "MAX_AREA = 150 # in Angstrom^2\n", + "MAX_AREA_TOLERANCE = 0.09\n", + "MAX_LENGTH_TOLERANCE = 0.05\n", + "MAX_ANGLE_TOLERANCE = 0.02\n", + "\n", + "RELAXATION_PARAMETERS = {\n", + " \"FMAX\": 0.01,\n", + "}\n", + "MACE_MODEL = \"large\" # \"small\", \"medium\", or \"large\" MACE-MP-0 foundation model" + ], + "id": "57dc565952aa2e4e", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Install Packages" + ], + "id": "4e89f2d820acb1ed" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# !pip install mat3ra-made torch \"mace-torch\" ase \"e3nn==0.4.4\" \"numpy<=1.26.4\" pymatgen" + ], + "id": "810243b01ba923b5", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Load Materials" + ], + "id": "f89d3c98ddce2ab5" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.standata.materials import Materials\n", + "\n", + "substrate = Material.create(Materials.get_by_name_first_match(SUBSTRATE_NAME))\n", + "film = Material.create(Materials.get_by_name_first_match(FILM_NAME))\n", + "\n", + "print(\"Substrate:\", substrate.name)\n", + "print(\"Film: \", film.name)" + ], + "id": "8fd400dace70549e", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1. Visualize Input Materials" + ], + "id": "42f12abf6b65aa2c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from utils.visualize import visualize_materials as visualize\n", + "\n", + "visualize([substrate, film], repetitions=[3, 3, 1], rotation=\"0x\")" + ], + "id": "88b4ce8e27118174", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 3.2. Calculate nearest neighbor distance for each material to inform interface distance choice\n", + "id": "51c20f951b402e48" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from mat3ra.made.tools.build_components.entities.reusable.three_dimensional.supercell.helpers import create_supercell\n", + "from mat3ra.made.tools.analyze.rdf import RadialDistributionFunction\n", + "### 7.3. Plot Radial Distribution Functions\n", + "from utils.plot import plot_rdf\n", + "\n", + "substrate_supercell = create_supercell(substrate, scaling_factor=[3, 3, 3])\n", + "film_supercell = create_supercell(film, scaling_factor=[3, 3, 3])\n", + "\n", + "rdf_substrate = RadialDistributionFunction.from_material(substrate_supercell, cutoff=5.0)\n", + "rdf_film = RadialDistributionFunction.from_material(film_supercell, cutoff=5.0)\n", + "\n", + "first_peak_substrate = rdf_substrate.first_peak_distance\n", + "first_peak_film = rdf_film.first_peak_distance\n", + "\n", + "print(f\"First RDF peak for substrate ({substrate.name}): {first_peak_substrate:.3f} Å\")\n", + "print(f\"First RDF peak for film ({film.name}): {first_peak_film:.3f} Å\")\n", + "\n", + "if INTERFACE_DISTANCE is None:\n", + " INTERFACE_DISTANCE = (first_peak_substrate + first_peak_film) / 2\n", + " print(f\"Setting interface distance to {INTERFACE_DISTANCE:.3f} Å based on RDF peaks\")\n", + "\n", + "plot_rdf(substrate_supercell, cutoff=5.0)\n", + "plot_rdf(film_supercell, cutoff=5.0)" + ], + "id": "ca5955d87b780158", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Configure Slabs\n", + "\n", + "### 4.1. Get Possible Terminations" + ], + "id": "46b23cc60cd0454e" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.helpers import get_slab_terminations\n", + "\n", + "film_slab_terminations = get_slab_terminations(material=film, miller_indices=FILM_MILLER_INDICES)\n", + "substrate_slab_terminations = get_slab_terminations(material=substrate, miller_indices=SUBSTRATE_MILLER_INDICES)\n", + "print(\"Film slab terminations: \", film_slab_terminations)\n", + "print(\"Substrate slab terminations:\", substrate_slab_terminations)" + ], + "id": "be35fa07cf206e76", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2. Visualize Slabs for All Possible Terminations" + ], + "id": "caf19a75fb14f280" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.helpers import create_slab, select_slab_termination\n", + "from mat3ra.made.tools.helpers import create_interface_zsl_between_slabs\n", + "\n", + "film_slabs = [\n", + " create_slab(film, miller_indices=FILM_MILLER_INDICES, termination_top=t, vacuum=0)\n", + " for t in film_slab_terminations\n", + "]\n", + "substrate_slabs = [\n", + " create_slab(substrate, miller_indices=SUBSTRATE_MILLER_INDICES, termination_top=t, vacuum=0, number_of_layers=4)\n", + " for t in substrate_slab_terminations\n", + "]\n", + "\n", + "visualize(\n", + " [{\"material\": s, \"title\": str(t)} for s, t in zip(film_slabs, film_slab_terminations)],\n", + " repetitions=[3, 3, 1], rotation=\"-90x\",\n", + ")\n", + "visualize(\n", + " [{\"material\": s, \"title\": str(t)} for s, t in zip(substrate_slabs, substrate_slab_terminations)],\n", + " repetitions=[3, 3, 1], rotation=\"-90x\",\n", + ")" + ], + "id": "7bae67a0b5821aa7", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.3. Create Substrate and Film Slabs" + ], + "id": "a692280e024d5883" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab import SlabConfiguration, SlabBuilder\n", + "\n", + "substrate_slab_config = SlabConfiguration.from_parameters(\n", + " material_or_dict=substrate,\n", + " miller_indices=SUBSTRATE_MILLER_INDICES,\n", + " number_of_layers=SUBSTRATE_THICKNESS,\n", + " vacuum=0.0,\n", + " termination_top_formula=SUBSTRATE_TERMINATION_FORMULA,\n", + " use_conventional_cell=USE_CONVENTIONAL_CELL,\n", + ")\n", + "film_slab_config = SlabConfiguration.from_parameters(\n", + " material_or_dict=film,\n", + " miller_indices=FILM_MILLER_INDICES,\n", + " number_of_layers=FILM_THICKNESS,\n", + " vacuum=0.0,\n", + " termination_bottom_formula=FILM_TERMINATION_FORMULA,\n", + " use_conventional_cell=USE_CONVENTIONAL_CELL,\n", + ")\n", + "\n", + "substrate_slab = SlabBuilder().get_material(substrate_slab_config)\n", + "film_slab = SlabBuilder().get_material(film_slab_config)" + ], + "id": "89f15267bd2f6f66", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Find Interfaces with ZSL Strain Matching\n", + "\n", + "### 5.1. Initialize ZSL Analyzer" + ], + "id": "4c4b81397d2eb68" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.analyze.interface import ZSLInterfaceAnalyzer\n", + "\n", + "zsl_analyzer = ZSLInterfaceAnalyzer(\n", + " substrate_slab_configuration=substrate_slab_config,\n", + " film_slab_configuration=film_slab_config,\n", + " max_area=MAX_AREA,\n", + " max_area_ratio_tol=MAX_AREA_TOLERANCE,\n", + " max_length_tol=MAX_LENGTH_TOLERANCE,\n", + " max_angle_tol=MAX_ANGLE_TOLERANCE,\n", + " reduce_result_cell=False,\n", + ")" + ], + "id": "b04a8543e8fcf455", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2. Generate and Plot Matches" + ], + "id": "a1956430738a9a9a" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from utils.plot import plot_strain_vs_area\n", + "\n", + "PLOT_SETTINGS = {\n", + " \"HEIGHT\": 600,\n", + " \"X_SCALE\": \"log\",\n", + " \"Y_SCALE\": \"log\",\n", + "}\n", + "\n", + "matches = zsl_analyzer.zsl_match_holders\n", + "print(f\"Found {len(matches)} matches\")\n", + "plot_strain_vs_area(matches, PLOT_SETTINGS)" + ], + "id": "8682bbecf48aa30b", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.3. Select the Interface\n", + "\n", + "Choose the match index from the plot above (index 0 has the lowest strain)." + ], + "id": "245b45e7bb4a7ac3" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "selected_index = 0" + ], + "id": "a83a9d7a43391187", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Create the Interface" + ], + "id": "5360346b47cf21ed" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "interface = create_interface_zsl_between_slabs(\n", + " substrate_slab=substrate_slab,\n", + " film_slab=film_slab,\n", + " gap=INTERFACE_DISTANCE,\n", + " vacuum=INTERFACE_VACUUM,\n", + " match_id=selected_index,\n", + " max_area=MAX_AREA,\n", + " max_area_ratio_tol=MAX_AREA_TOLERANCE,\n", + " max_length_tol=MAX_LENGTH_TOLERANCE,\n", + " max_angle_tol=MAX_ANGLE_TOLERANCE,\n", + " reduce_result_cell_to_primitive=REDUCE_RESULT_CELL_TO_PRIMITIVE,\n", + ")" + ], + "id": "4d413413dfaa3f6d", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6.1. Visualize Interface" + ], + "id": "24cc0a761f161676" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from utils.visualize import ViewersEnum\n", + "\n", + "visualize([{\"material\": interface, \"title\": interface.name}], viewer=ViewersEnum.wave)\n", + "visualize(interface, repetitions=[1, 1, 1], rotation=\"-90x\")" + ], + "id": "fcbb4e6c1de21233", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Apply Relaxation\n", + "### 7.1. Relax with MACE" + ], + "id": "e64688fc18c49bb6" + }, + { + "cell_type": "code", + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "source": [ + "import plotly.graph_objs as go\n", + "from IPython.display import display\n", + "from plotly.subplots import make_subplots\n", + "from src.utils import ase_to_poscar\n", + "from mat3ra.made.tools.convert import to_ase\n", + "from ase.optimize import BFGS\n", + "from mace.calculators import mace_mp\n", + "\n", + "calculator = mace_mp(model=MACE_MODEL, dispersion=True, default_dtype=\"float32\", device=\"cpu\")\n", + "\n", + "ase_interface = to_ase(interface)\n", + "ase_interface.set_calculator(calculator)\n", + "dyn = BFGS(ase_interface)\n", + "\n", + "steps = []\n", + "energies = []\n", + "\n", + "fig = make_subplots(rows=1, cols=1, specs=[[{\"type\": \"scatter\"}]])\n", + "scatter = go.Scatter(x=[], y=[], mode=\"lines+markers\", name=\"Energy\")\n", + "fig.add_trace(scatter)\n", + "fig.update_layout(title_text=\"Real-time Optimization Progress\", xaxis_title=\"Step\", yaxis_title=\"Energy (eV)\")\n", + "\n", + "f = go.FigureWidget(fig)\n", + "display(f)\n", + "\n", + "\n", + "def plotly_callback():\n", + " step = dyn.nsteps\n", + " energy = ase_interface.get_total_energy()\n", + " steps.append(step)\n", + " energies.append(energy)\n", + " print(f\"Step: {step}, Energy: {energy:.4f} eV\")\n", + " with f.batch_update():\n", + " f.data[0].x = steps\n", + " f.data[0].y = energies\n", + "\n", + "\n", + "dyn.attach(plotly_callback, interval=1)\n", + "dyn.run(fmax=RELAXATION_PARAMETERS[\"FMAX\"])\n", + "\n", + "ase_original_interface = to_ase(interface)\n", + "ase_original_interface.set_calculator(calculator)\n", + "ase_final_interface = ase_interface\n", + "\n", + "original_energy = ase_original_interface.get_total_energy()\n", + "relaxed_energy = ase_interface.get_total_energy()\n", + "\n", + "print(\"Original structure:\\n\", ase_to_poscar(ase_original_interface))\n", + "print(\"\\nRelaxed structure:\\n\", ase_to_poscar(ase_final_interface))\n", + "print(f\"The final energy is {float(relaxed_energy):.3f} eV.\")" + ], + "id": "3d8746a77f71bab5", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.2. View Structure Before and After Relaxation" + ], + "id": "abfa372909a96bf8" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.convert import from_ase\n", + "\n", + "\n", + "def atoms_to_material(atoms, title):\n", + " material = Material.create(from_ase(atoms))\n", + " material.name = title\n", + " return material\n", + "\n", + "\n", + "material_original = atoms_to_material(ase_original_interface, f\"Original E={original_energy:.3f} eV\")\n", + "material_relaxed = atoms_to_material(ase_final_interface, f\"Relaxed E={relaxed_energy:.3f} eV\")\n", + "\n", + "visualize(\n", + " [\n", + " {\"material\": material_original, \"title\": material_original.name},\n", + " {\"material\": material_relaxed, \"title\": material_relaxed.name},\n", + " ],\n", + " viewer=ViewersEnum.wave,\n", + ")" + ], + "id": "9565d0931b198f63", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 7.4. Output interlayer distance before and after relaxation", + "id": "e4b49774283e5517" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from mat3ra.made.tools.analyze.other import get_average_interlayer_distance\n", + "\n", + "print(f\"Interlayer distance before relaxation: {get_average_interlayer_distance(material_original, 0, 1):.4f} Å\")\n", + "print(f\"Interlayer distance after relaxation: {get_average_interlayer_distance(material_relaxed, 0, 1):.4f} Å\")" + ], + "id": "6dd00402bc2e9d59", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.4. Calculate Interface Energy" + ], + "id": "7ee3d26311a30687" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "def filter_atoms_by_tag(atoms, material_index):\n", + " return atoms[atoms.get_tags() == material_index]\n", + "\n", + "\n", + "def calculate_energy(atoms, calc):\n", + " atoms.set_calculator(calc)\n", + " return atoms.get_total_energy()\n", + "\n", + "\n", + "def calculate_delta_energy(total_energy, *component_energies):\n", + " return total_energy - sum(component_energies)\n", + "\n", + "\n", + "substrate_original = filter_atoms_by_tag(ase_original_interface, SUBSTRATE_INDEX)\n", + "layer_original = filter_atoms_by_tag(ase_original_interface, FILM_INDEX)\n", + "substrate_relaxed = filter_atoms_by_tag(ase_final_interface, SUBSTRATE_INDEX)\n", + "layer_relaxed = filter_atoms_by_tag(ase_final_interface, FILM_INDEX)\n", + "\n", + "original_substrate_energy = calculate_energy(substrate_original, calculator)\n", + "original_layer_energy = calculate_energy(layer_original, calculator)\n", + "relaxed_substrate_energy = calculate_energy(substrate_relaxed, calculator)\n", + "relaxed_layer_energy = calculate_energy(layer_relaxed, calculator)\n", + "\n", + "delta_original = calculate_delta_energy(original_energy, original_substrate_energy, original_layer_energy)\n", + "delta_relaxed = calculate_delta_energy(relaxed_energy, relaxed_substrate_energy, relaxed_layer_energy)\n", + "\n", + "area = ase_original_interface.get_volume() / ase_original_interface.cell[2, 2]\n", + "n_interface = ase_final_interface.get_global_number_of_atoms()\n", + "n_substrate = substrate_relaxed.get_global_number_of_atoms()\n", + "n_layer = layer_relaxed.get_global_number_of_atoms()\n", + "effective_delta_relaxed = (\n", + " relaxed_energy / n_interface\n", + " - (relaxed_substrate_energy / n_substrate + relaxed_layer_energy / n_layer)\n", + " ) / (2 * area)\n", + "\n", + "print(f\"Original Substrate energy: {original_substrate_energy:.4f} eV\")\n", + "print(f\"Relaxed Substrate energy: {relaxed_substrate_energy:.4f} eV\")\n", + "print(f\"Original Layer energy: {original_layer_energy:.4f} eV\")\n", + "print(f\"Relaxed Layer energy: {relaxed_layer_energy:.4f} eV\")\n", + "print(\"\\nDelta between interface energy and sum of component energies\")\n", + "print(f\"Original Delta: {delta_original:.4f} eV\")\n", + "print(f\"Relaxed Delta: {delta_relaxed:.4f} eV\")\n", + "print(f\"Original Delta per area: {delta_original / area:.4f} eV/Ang^2\")\n", + "print(f\"Relaxed Delta per area: {delta_relaxed / area:.4f} eV/Ang^2\")\n", + "print(f\"Relaxed interface energy: {relaxed_energy:.4f} eV\")\n", + "print(\n", + " f\"Effective relaxed Delta per area: {effective_delta_relaxed:.4f} eV/Ang^2 ({effective_delta_relaxed / 0.16:.4f} J/m^2)\")" + ], + "id": "79ea902feda4a8d3", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] mat3ra-made interface builder: https://github.com/Exabyte-io/made \n", + "[2] MACE-MP-0 foundation model: https://github.com/ACEsuit/mace?tab=readme-ov-file#foundation-models " + ], + "id": "2f60fdb73e44c09c" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From aabb6735bcbf75b94700884728ebf3240cd30138 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 5 Apr 2026 19:49:20 -0700 Subject: [PATCH 2/5] update: nb with mace for pyodide --- ...terface_with_relaxation_mace_pyodide.ipynb | 575 ++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb diff --git a/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb b/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb new file mode 100644 index 00000000..9d271089 --- /dev/null +++ b/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb @@ -0,0 +1,575 @@ +{ + "metadata": { + "kernelspec": { + "name": "python", + "display_name": "Python (Pyodide)", + "language": "python" + }, + "language_info": { + "codemirror_mode": { + "name": "python", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8" + } + }, + "nbformat_minor": 5, + "nbformat": 4, + "cells": [ + { + "cell_type": "markdown", + "source": "# Create an Interface with ZSL and Relax it with MACE\n\nUse Zur and McGill superlattices matching [algorithm](https://doi.org/10.1063/1.3330840) to create interfaces between two materials using the `mat3ra-made` [implementation](https://github.com/Exabyte-io/made).\n\n

Usage

\n\n1. Drop the materials files into the \"uploads\" folder in the JupyterLab file browser\n1. Set Input Parameters below or use the default values\n1. Click \"Run\" > \"Run All\" to run all cells\n1. Wait for the run to complete. Scroll down to view cell results.\n1. Review the strain plot and modify parameters as needed\n\n## Methodology\n\n1. Load materials from JSON files and create substrate and film slabs via `mat3ra-made`\n2. Run ZSL strain matching and plot strain vs interface area\n3. Create the selected interface and convert to ASE atoms with `to_ase()`\n4. Relax the interface with MACE-MP-0 and visualize convergence\n5. Compute interface binding energy decomposition", + "metadata": {}, + "id": "9515ed910c085db8" + }, + { + "cell_type": "markdown", + "source": "## 1. Set Input Parameters\n\n### 1.1. Substrate and Film Materials", + "metadata": {}, + "id": "4737d145950b1cc8" + }, + { + "cell_type": "code", + "source": "\nSUBSTRATE_NAME = \"Nickel\"\nFILM_NAME = \"Graphene\"\nSUBSTRATE_INDEX = 0\nFILM_INDEX = 1", + "metadata": { + "trusted": true + }, + "id": "ef377fcae54adf52", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 1.2. Slab Parameters", + "metadata": {}, + "id": "401cb093b302dbb1" + }, + { + "cell_type": "code", + "source": "SUBSTRATE_MILLER_INDICES = (1, 1, 1)\nSUBSTRATE_THICKNESS = 6 # in atomic layers\nSUBSTRATE_TERMINATION_FORMULA = None # if None, the first termination is used\n\nFILM_MILLER_INDICES = (0, 0, 1)\nFILM_THICKNESS = 1 # in atomic layers\nFILM_TERMINATION_FORMULA = None # if None, the first termination is used\n\nUSE_CONVENTIONAL_CELL = True", + "metadata": { + "trusted": true + }, + "id": "edd2f829bc0a72ce", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 1.3. Interface and Relaxation Parameters", + "metadata": {}, + "id": "8fecb819abaa2ca7" + }, + { + "cell_type": "code", + "source": "INTERFACE_DISTANCE = None # gap between substrate and film, in Angstrom, if None, the distance will be set to the sum of the covalent radii of the two materials\nINTERFACE_VACUUM = 10.0 # vacuum over film, in Angstrom\nREDUCE_RESULT_CELL_TO_PRIMITIVE = True\n\nMAX_AREA = 150 # in Angstrom^2\nMAX_AREA_TOLERANCE = 0.09\nMAX_LENGTH_TOLERANCE = 0.05\nMAX_ANGLE_TOLERANCE = 0.02\n\nRELAXATION_PARAMETERS = {\n \"FMAX\": 0.05,\n}\nMACE_MODEL = \"large\" # \"small\", \"medium\", or \"large\" MACE-MP-0 foundation model", + "metadata": { + "trusted": true + }, + "id": "57dc565952aa2e4e", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 2. Install Packages", + "metadata": {}, + "id": "4e89f2d820acb1ed" + }, + { + "cell_type": "code", + "source": [ + "\n", + "try:\n", + " from pyodide.http import pyfetch as _pyodide_pyfetch\n", + "except ImportError:\n", + " _pyodide_pyfetch = None\n", + "\n", + "# Mirrors mace.calculators.foundations_models.mace_mp_urls (mace-torch 0.3.x).\n", + "_MACE_MP_URLS = {\n", + "# \"small\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-10-mace-128-L0_energy_epoch-249.model\",\n", + " \"small\": \"http://localhost:8800/2023-12-10-mace-128-L0_energy_epoch-249.model\",\n", + " \"medium\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-03-mace-128-L1_epoch-199.model\",\n", + " \"large\": \"http://localhost:8800/MACE_MPtrj_2022.9.model\",\n", + "# \"large\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/MACE_MPtrj_2022.9.model\",\n", + " \"small-0b\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_small.model\",\n", + " \"medium-0b\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_medium.model\",\n", + " \"small-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-small-density-agnesi-stress.model\",\n", + " \"medium-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-medium-density-agnesi-stress.model\",\n", + " \"large-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-large-density-agnesi-stress.model\",\n", + " \"medium-0b3\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b3/mace-mp-0b3-medium.model\",\n", + " \"medium-mpa-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mpa_0/mace-mpa-0-medium.model\",\n", + " \"small-omat-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_omat_0/mace-omat-0-small.model\",\n", + " \"medium-omat-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_omat_0/mace-omat-0-medium.model\",\n", + " \"mace-matpes-pbe-0\": \"https://github.com/ACEsuit/mace-foundations/releases/download/mace_matpes_0/MACE-matpes-pbe-omat-ft.model\",\n", + " \"mace-matpes-r2scan-0\": \"https://github.com/ACEsuit/mace-foundations/releases/download/mace_matpes_0/MACE-matpes-r2scan-omat-ft.model\",\n", + "}\n", + "\n", + "def _matscipy_neighbour_list_compat(quantities, atoms=None, cutoff=None, positions=None, cell=None, pbc=None, **_):\n", + " \"\"\"Translate matscipy-style keyword-arg call into an ASE neighbor_list call.\"\"\"\n", + " from ase import Atoms as _Atoms\n", + " from ase.neighborlist import neighbor_list as _ase_neighbor_list\n", + " if atoms is None:\n", + " atoms = _Atoms(positions=positions, cell=cell, pbc=pbc if pbc is not None else [False, False, False])\n", + " return _ase_neighbor_list(quantities, atoms, cutoff)\n", + "\n", + "def _tensor_array_compat(self, dtype=None):\n", + " \"\"\"Replacement for Tensor.__array__ in Pyodide where tensor.numpy() is unavailable.\"\"\"\n", + " import numpy as _np\n", + " arr = _np.array(self.tolist())\n", + " return arr.astype(dtype) if dtype is not None else arr\n", + "\n", + "\n", + "class _TorchDFTD3CalculatorStub:\n", + " def __init__(self, *args, **kwargs):\n", + " raise RuntimeError(\"torch_dftd is unavailable in Pyodide; use dispersion=False with mace_mp()\")\n", + "\n", + " \n", + "\n", + "class _LoggingTensorModeStub:\n", + " def __enter__(self):\n", + " return self\n", + "\n", + " def __exit__(self, *args):\n", + " return False\n", + "\n", + "\n", + "def _capture_logs_stub(*args, **kwargs):\n", + " return _LoggingTensorModeStub()\n", + "\n", + "\n", + "def patch_mace_for_pyodide():\n", + "\n", + " \"\"\"Stub modules absent in pyodide's stripped torch build and missing C-extension packages.\"\"\"\n", + " _internal = types.ModuleType(\"torch.testing._internal\")\n", + " _internal.__path__ = [] # __path__ marks it as a package so sub-imports resolve\n", + " _internal.__package__ = \"torch.testing._internal\"\n", + "\n", + " _common_utils = types.ModuleType(\"torch.testing._internal.common_utils\")\n", + " _common_utils.dtype_abbrs = {}\n", + "\n", + " _logging_tensor = types.ModuleType(\"torch.testing._internal.logging_tensor\")\n", + " _logging_tensor.LoggingTensorMode = _LoggingTensorModeStub\n", + " _logging_tensor.capture_logs = _capture_logs_stub\n", + "\n", + " _internal.common_utils = _common_utils\n", + " _internal.logging_tensor = _logging_tensor\n", + " sys.modules[\"torch.testing._internal\"] = _internal\n", + " sys.modules[\"torch.testing._internal.common_utils\"] = _common_utils\n", + " sys.modules[\"torch.testing._internal.logging_tensor\"] = _logging_tensor\n", + "\n", + " _matscipy = types.ModuleType(\"matscipy\")\n", + " _matscipy.__path__ = []\n", + " _matscipy.__package__ = \"matscipy\"\n", + " _matscipy_neighbours = types.ModuleType(\"matscipy.neighbours\")\n", + " _matscipy_neighbours.neighbour_list = _matscipy_neighbour_list_compat\n", + " _matscipy.neighbours = _matscipy_neighbours\n", + " sys.modules[\"matscipy\"] = _matscipy\n", + " sys.modules[\"matscipy.neighbours\"] = _matscipy_neighbours\n", + " \n", + " \n", + " # lmdb, orjson, h5py are C-extension packages that cannot be compiled in\n", + " # Emscripten (no subprocess / cc support). They are only used by the\n", + " # training/dataset-loading paths (mace.data.lmdb_dataset,\n", + " # mace.data.hdf5_dataset) which are pulled in by mace.data.__init__ but\n", + " # are never exercised during Pyodide inference. Stubs let the import\n", + " # succeed; any actual call to these APIs would raise at runtime, which is\n", + " # the correct behaviour for an unsupported operation.\n", + " for _pkg in (\"lmdb\", \"h5py\"):\n", + " if _pkg not in sys.modules:\n", + " sys.modules[_pkg] = types.ModuleType(_pkg)\n", + "\n", + " # mace.data.atomic_data accesses torch_geometric.data.Data at class-definition\n", + " # time. In Pyodide the submodule attribute can be unset when mace.tools.__init__\n", + " # is still mid-import (circular via mace.cli.visualise_train) at the point where\n", + " # train.py runs `from . import torch_geometric` – so the subpackage exists in\n", + " # sys.modules but .data has not yet been stamped onto it. Pre-importing here,\n", + " # with the matscipy stub already in place, forces the full init to complete and\n", + " # guarantees the attribute is set before any mace.data import runs.\n", + " try:\n", + " import importlib as _importlib\n", + " _tg = _importlib.import_module(\"mace.tools.torch_geometric\")\n", + " _tg_data = _importlib.import_module(\"mace.tools.torch_geometric.data\")\n", + " _tg.data = _tg_data\n", + " except Exception:\n", + " pass\n", + " \n", + " \n", + " # Pyodide's WASM torch build disables Tensor.numpy() and __array__.\n", + " # MACECalculator.__init__ calls np.array([model.r_max.cpu(), ...]) which\n", + " # triggers __array__ on each tensor element. Route both through tolist().\n", + " try:\n", + " import torch as _torch\n", + " except ImportError:\n", + " _torch = None\n", + " # Pyodide's WASM torch build disables Tensor.numpy() and __array__.\n", + " # MACECalculator.__init__ calls np.array([model.r_max.cpu(), ...]) which\n", + " # triggers __array__ on each tensor element. Route both through tolist().\n", + " if _torch is not None:\n", + " import numpy as _np\n", + " _torch.Tensor.__array__ = _tensor_array_compat\n", + " _torch.Tensor.numpy = lambda self: _np.array(self.detach().tolist())\n", + "\n", + " # torch.compiler exists in the WASM build but is_compiling is absent;\n", + " # mace.modules.utils.prepare_graph calls it unconditionally.\n", + " if not hasattr(_torch, \"compiler\"):\n", + " _torch.compiler = types.ModuleType(\"torch.compiler\")\n", + " if not hasattr(_torch.compiler, \"is_compiling\"):\n", + " _torch.compiler.is_compiling = lambda: False\n", + " \n", + " # torch.linalg.det calls LAPACK, which is absent in the WASM build.\n", + " # mace.modules.utils.compute_forces_virials uses it to compute the\n", + " # unit-cell volume (a 3×3 determinant) when compute_stress=True.\n", + " # Redirect to NumPy for the fallback case.\n", + " _orig_linalg_det = _torch.linalg.det\n", + "\n", + " def _safe_linalg_det(A):\n", + " try:\n", + " return _orig_linalg_det(A)\n", + " except RuntimeError:\n", + " import numpy as _np\n", + " result = _np.linalg.det(_np.array(A.detach().tolist()))\n", + " return _torch.tensor(result, dtype=A.dtype, device=A.device)\n", + "\n", + " _torch.linalg.det = _safe_linalg_det\n", + "\n", + "\n", + "\n", + " # # torch_dftd (dispersion correction) is a C-extension unavailable in Pyodide.\n", + " # # Stub it so mace.calculators imports succeed; instantiating TorchDFTD3Calculator\n", + " # # raises a clear RuntimeError directing the user to set dispersion=False.\n", + " # if \"torch_dftd\" not in sys.modules:\n", + " # _torch_dftd = types.ModuleType(\"torch_dftd\")\n", + " # _torch_dftd.__path__ = []\n", + " # _torch_dftd3_mod = types.ModuleType(\"torch_dftd.torch_dftd3_calculator\")\n", + " # _torch_dftd3_mod.TorchDFTD3Calculator = _TorchDFTD3CalculatorStub\n", + " # _torch_dftd.torch_dftd3_calculator = _torch_dftd3_mod\n", + " # sys.modules[\"torch_dftd\"] = _torch_dftd\n", + " # sys.modules[\"torch_dftd.torch_dftd3_calculator\"] = _torch_dftd3_mod\n", + "\n", + "\n", + "\n", + "\n", + "async def download_mace_model_pyodide(model = None) -> str:\n", + " \"\"\"Download a MACE foundation model in Pyodide using the browser's fetch API.\n", + "\n", + " urllib.request does not support HTTPS in Pyodide WASM; this function uses\n", + " pyodide.http.pyfetch instead, which delegates to the browser's native fetch.\n", + "\n", + " Args:\n", + " model: MACE model name (e.g. \"medium\", \"medium-mpa-0\") or a direct HTTPS URL.\n", + " None defaults to \"medium-mpa-0\".\n", + " Returns:\n", + " Local filesystem path to the downloaded model file.\n", + " \"\"\"\n", + " if _pyodide_pyfetch is None:\n", + " raise RuntimeError(\"pyodide.http.pyfetch unavailable; call mace_mp() directly in standard Python.\")\n", + " url = _MACE_MP_URLS.get(model or \"medium-mpa-0\", model)\n", + " if url is None or not url.startswith(\"http\"):\n", + " return model # already a local path\n", + " cache_dir = os.path.join(os.path.expanduser(\"~\"), \".cache\", \"mace\")\n", + " os.makedirs(cache_dir, exist_ok=True)\n", + " filename = \"\".join(c for c in os.path.basename(url) if c.isalnum() or c in \"_\")\n", + " local_path = os.path.join(cache_dir, filename)\n", + " if not os.path.isfile(local_path):\n", + " print(f\"Downloading MACE model from {url!r}\")\n", + " response = await _pyodide_pyfetch(url)\n", + " with open(local_path, \"wb\") as f:\n", + " f.write(await response.bytes())\n", + " print(f\"Cached MACE model to {local_path}\")\n", + " return local_path\n", + "\n" + ], + "metadata": { + "trusted": true + }, + "id": "e58d503d-7686-4320-b182-1a9be23a3f56", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": "import sys\n\nif sys.platform == \"emscripten\":\n import micropip\n\n\n await micropip.install(\"mat3ra-api-examples\", deps=False)\n await micropip.install(\"mat3ra-utils\")\n from mat3ra.utils.jupyterlite.packages import install_packages\n \n import types\n\n await install_packages(\"api_examples|torch\")\n await micropip.install(\"torch-dftd\")\n\n await micropip.install(\"opt_einsum\")\n await micropip.install(\"opt_einsum_fx\", deps=False)\n await micropip.install(\"e3nn==0.4.4\", deps=False)\n\n # MACE training deps\n await micropip.install(\"prettytable\")\n await micropip.install(\"torch_ema\", deps=False)\n await micropip.install(\"lightning-utilities\", deps=False)\n await micropip.install(\"torchmetrics\", deps=False)\n \n #\n await micropip.install(\"ssl\")\n await micropip.install(\"h5py\")\n await micropip.install(\"lmdb\")\n \n # MACE\n await micropip.install(\"mace-torch\", deps=False)\n patch_mace_for_pyodide()\n", + "metadata": { + "trusted": true + }, + "id": "3cdce8f3-7508-49bd-9ee1-e9dc99bf1a2c", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": "", + "metadata": { + "trusted": true + }, + "id": "a660f174-a0d3-4beb-bc84-8cfc36471281", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": "# micropip.list()", + "metadata": { + "trusted": true + }, + "id": "810243b01ba923b5", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 3. Load Materials", + "metadata": {}, + "id": "f89d3c98ddce2ab5" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.material import Material\nfrom mat3ra.standata.materials import Materials\n\nsubstrate = Material.create(Materials.get_by_name_first_match(SUBSTRATE_NAME))\nfilm = Material.create(Materials.get_by_name_first_match(FILM_NAME))\n\nprint(\"Substrate:\", substrate.name)\nprint(\"Film: \", film.name)", + "metadata": { + "trusted": true + }, + "id": "8fd400dace70549e", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 3.1. Visualize Input Materials", + "metadata": {}, + "id": "42f12abf6b65aa2c" + }, + { + "cell_type": "code", + "source": "from utils.visualize import visualize_materials as visualize\n\nvisualize([substrate, film], repetitions=[3, 3, 1], rotation=\"0x\")", + "metadata": { + "trusted": true + }, + "id": "88b4ce8e27118174", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 3.2. Calculate nearest neighbor distance for each material to inform interface distance choice\n", + "metadata": {}, + "id": "51c20f951b402e48" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.build_components.entities.reusable.three_dimensional.supercell.helpers import create_supercell\nfrom mat3ra.made.tools.analyze.rdf import RadialDistributionFunction\n### 7.3. Plot Radial Distribution Functions\nfrom utils.plot import plot_rdf\n\nsubstrate_supercell = create_supercell(substrate, scaling_factor=[3, 3, 3])\nfilm_supercell = create_supercell(film, scaling_factor=[3, 3, 3])\n\nrdf_substrate = RadialDistributionFunction.from_material(substrate_supercell, cutoff=5.0)\nrdf_film = RadialDistributionFunction.from_material(film_supercell, cutoff=5.0)\n\nfirst_peak_substrate = rdf_substrate.first_peak_distance\nfirst_peak_film = rdf_film.first_peak_distance\n\nprint(f\"First RDF peak for substrate ({substrate.name}): {first_peak_substrate:.3f} Å\")\nprint(f\"First RDF peak for film ({film.name}): {first_peak_film:.3f} Å\")\n\nif INTERFACE_DISTANCE is None:\n INTERFACE_DISTANCE = (first_peak_substrate + first_peak_film) / 2\n print(f\"Setting interface distance to {INTERFACE_DISTANCE:.3f} Å based on RDF peaks\")\n\n# plot_rdf(substrate_supercell, cutoff=5.0)\n# plot_rdf(film_supercell, cutoff=5.0)", + "metadata": { + "trusted": true + }, + "id": "ca5955d87b780158", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 4. Configure Slabs\n\n### 4.1. Get Possible Terminations", + "metadata": {}, + "id": "46b23cc60cd0454e" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.helpers import get_slab_terminations\n\nfilm_slab_terminations = get_slab_terminations(material=film, miller_indices=FILM_MILLER_INDICES)\nsubstrate_slab_terminations = get_slab_terminations(material=substrate, miller_indices=SUBSTRATE_MILLER_INDICES)\nprint(\"Film slab terminations: \", film_slab_terminations)\nprint(\"Substrate slab terminations:\", substrate_slab_terminations)", + "metadata": { + "trusted": true + }, + "id": "be35fa07cf206e76", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 4.2. Visualize Slabs for All Possible Terminations", + "metadata": {}, + "id": "caf19a75fb14f280" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.helpers import create_slab, select_slab_termination\nfrom mat3ra.made.tools.helpers import create_interface_zsl_between_slabs\n\nfilm_slabs = [\n create_slab(film, miller_indices=FILM_MILLER_INDICES, termination_top=t, vacuum=0)\n for t in film_slab_terminations\n]\nsubstrate_slabs = [\n create_slab(substrate, miller_indices=SUBSTRATE_MILLER_INDICES, termination_top=t, vacuum=0, number_of_layers=4)\n for t in substrate_slab_terminations\n]\n\n# visualize(\n# [{\"material\": s, \"title\": str(t)} for s, t in zip(film_slabs, film_slab_terminations)],\n# repetitions=[3, 3, 1], rotation=\"-90x\",\n# )\n# visualize(\n# [{\"material\": s, \"title\": str(t)} for s, t in zip(substrate_slabs, substrate_slab_terminations)],\n# repetitions=[3, 3, 1], rotation=\"-90x\",\n# )", + "metadata": { + "trusted": true + }, + "id": "7bae67a0b5821aa7", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 4.3. Create Substrate and Film Slabs", + "metadata": {}, + "id": "a692280e024d5883" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab import SlabConfiguration, SlabBuilder\n\nsubstrate_slab_config = SlabConfiguration.from_parameters(\n material_or_dict=substrate,\n miller_indices=SUBSTRATE_MILLER_INDICES,\n number_of_layers=SUBSTRATE_THICKNESS,\n vacuum=0.0,\n termination_top_formula=SUBSTRATE_TERMINATION_FORMULA,\n use_conventional_cell=USE_CONVENTIONAL_CELL,\n)\nfilm_slab_config = SlabConfiguration.from_parameters(\n material_or_dict=film,\n miller_indices=FILM_MILLER_INDICES,\n number_of_layers=FILM_THICKNESS,\n vacuum=0.0,\n termination_bottom_formula=FILM_TERMINATION_FORMULA,\n use_conventional_cell=USE_CONVENTIONAL_CELL,\n)\n\nsubstrate_slab = SlabBuilder().get_material(substrate_slab_config)\nfilm_slab = SlabBuilder().get_material(film_slab_config)", + "metadata": { + "trusted": true + }, + "id": "89f15267bd2f6f66", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 5. Find Interfaces with ZSL Strain Matching\n\n### 5.1. Initialize ZSL Analyzer", + "metadata": {}, + "id": "4c4b81397d2eb68" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.analyze.interface import ZSLInterfaceAnalyzer\n\nzsl_analyzer = ZSLInterfaceAnalyzer(\n substrate_slab_configuration=substrate_slab_config,\n film_slab_configuration=film_slab_config,\n max_area=MAX_AREA,\n max_area_ratio_tol=MAX_AREA_TOLERANCE,\n max_length_tol=MAX_LENGTH_TOLERANCE,\n max_angle_tol=MAX_ANGLE_TOLERANCE,\n reduce_result_cell=False,\n)", + "metadata": { + "trusted": true + }, + "id": "b04a8543e8fcf455", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 5.2. Generate and Plot Matches", + "metadata": {}, + "id": "a1956430738a9a9a" + }, + { + "cell_type": "code", + "source": "from utils.plot import plot_strain_vs_area\n\nPLOT_SETTINGS = {\n \"HEIGHT\": 600,\n \"X_SCALE\": \"log\",\n \"Y_SCALE\": \"log\",\n}\n\nmatches = zsl_analyzer.zsl_match_holders\nprint(f\"Found {len(matches)} matches\")\n# plot_strain_vs_area(matches, PLOT_SETTINGS)", + "metadata": { + "trusted": true + }, + "id": "8682bbecf48aa30b", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 5.3. Select the Interface\n\nChoose the match index from the plot above (index 0 has the lowest strain).", + "metadata": {}, + "id": "245b45e7bb4a7ac3" + }, + { + "cell_type": "code", + "source": "selected_index = 0", + "metadata": { + "trusted": true + }, + "id": "a83a9d7a43391187", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 6. Create the Interface", + "metadata": {}, + "id": "5360346b47cf21ed" + }, + { + "cell_type": "code", + "source": "interface = create_interface_zsl_between_slabs(\n substrate_slab=substrate_slab,\n film_slab=film_slab,\n gap=INTERFACE_DISTANCE,\n vacuum=INTERFACE_VACUUM,\n match_id=selected_index,\n max_area=MAX_AREA,\n max_area_ratio_tol=MAX_AREA_TOLERANCE,\n max_length_tol=MAX_LENGTH_TOLERANCE,\n max_angle_tol=MAX_ANGLE_TOLERANCE,\n reduce_result_cell_to_primitive=REDUCE_RESULT_CELL_TO_PRIMITIVE,\n)", + "metadata": { + "trusted": true + }, + "id": "4d413413dfaa3f6d", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 6.1. Visualize Interface", + "metadata": {}, + "id": "24cc0a761f161676" + }, + { + "cell_type": "code", + "source": "from utils.visualize import ViewersEnum\n\n# visualize([{\"material\": interface, \"title\": interface.name}], viewer=ViewersEnum.wave)\n# visualize(interface, repetitions=[1, 1, 1], rotation=\"-90x\")", + "metadata": { + "trusted": true + }, + "id": "fcbb4e6c1de21233", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 7. Apply Relaxation\n### 7.1. Relax with MACE", + "metadata": {}, + "id": "e64688fc18c49bb6" + }, + { + "cell_type": "code", + "source": "await micropip.install(\"orjson\")\nawait micropip.install(\"anywidget\")\n# await micropip.install(\"matscipy\")\nimport os\nimport plotly.graph_objs as go\nfrom IPython.display import display\nfrom plotly.subplots import make_subplots\n\nfrom mat3ra.made.tools.convert import to_ase\nfrom ase.optimize import BFGS\nfrom mace.calculators import mace_mp\n\n\n\nmodel_path = await download_mace_model_pyodide(MACE_MODEL)\ncalculator = mace_mp(model=model_path, dispersion=False, default_dtype=\"float32\", device=\"cpu\")\n\nase_interface = to_ase(interface)\nase_interface.set_calculator(calculator)\ndyn = BFGS(ase_interface)\n\nsteps = []\nenergies = []\n\nfig = make_subplots(rows=1, cols=1, specs=[[{\"type\": \"scatter\"}]])\nscatter = go.Scatter(x=[], y=[], mode=\"lines+markers\", name=\"Energy\")\nfig.add_trace(scatter)\nfig.update_layout(title_text=\"Real-time Optimization Progress\", xaxis_title=\"Step\", yaxis_title=\"Energy (eV)\")\n\nf = go.FigureWidget(fig)\ndisplay(f)\n\n\ndef plotly_callback():\n step = dyn.nsteps\n energy = ase_interface.get_total_energy()\n steps.append(step)\n energies.append(energy)\n print(f\"Step: {step}, Energy: {energy:.4f} eV\")\n with f.batch_update():\n f.data[0].x = steps\n f.data[0].y = energies\n\n\ndyn.attach(plotly_callback, interval=1)\ndyn.run(fmax=RELAXATION_PARAMETERS[\"FMAX\"])\n\nase_original_interface = to_ase(interface)\nase_original_interface.set_calculator(calculator)\nase_final_interface = ase_interface\n\noriginal_energy = ase_original_interface.get_total_energy()\nrelaxed_energy = ase_interface.get_total_energy()\n\n# print(\"Original structure:\\n\", ase_to_poscar(ase_original_interface))\n# print(\"\\nRelaxed structure:\\n\", ase_to_poscar(ase_final_interface))\nprint(f\"The final energy is {float(relaxed_energy):.3f} eV.\")", + "metadata": { + "jupyter": { + "is_executing": true + }, + "trusted": true + }, + "id": "3d8746a77f71bab5", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 7.2. View Structure Before and After Relaxation", + "metadata": {}, + "id": "abfa372909a96bf8" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.convert import from_ase\n\n\ndef atoms_to_material(atoms, title):\n material = Material.create(from_ase(atoms))\n material.name = title\n return material\n\n\nmaterial_original = atoms_to_material(ase_original_interface, f\"Original E={original_energy:.3f} eV\")\nmaterial_relaxed = atoms_to_material(ase_final_interface, f\"Relaxed E={relaxed_energy:.3f} eV\")\n\nvisualize(\n [\n {\"material\": material_original, \"title\": material_original.name},\n {\"material\": material_relaxed, \"title\": material_relaxed.name},\n ],\n viewer=ViewersEnum.wave,\n)", + "metadata": { + "trusted": true + }, + "id": "9565d0931b198f63", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 7.4. Output interlayer distance before and after relaxation", + "metadata": {}, + "id": "e4b49774283e5517" + }, + { + "cell_type": "code", + "source": "from mat3ra.made.tools.analyze.other import get_average_interlayer_distance\n\nprint(f\"Interlayer distance before relaxation: {get_average_interlayer_distance(material_original, 0, 1):.4f} Å\")\nprint(f\"Interlayer distance after relaxation: {get_average_interlayer_distance(material_relaxed, 0, 1):.4f} Å\")", + "metadata": { + "trusted": true + }, + "id": "6dd00402bc2e9d59", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "### 7.4. Calculate Interface Energy", + "metadata": {}, + "id": "7ee3d26311a30687" + }, + { + "cell_type": "code", + "source": "def filter_atoms_by_tag(atoms, material_index):\n return atoms[atoms.get_tags() == material_index]\n\n\ndef calculate_energy(atoms, calc):\n atoms.set_calculator(calc)\n return atoms.get_total_energy()\n\n\ndef calculate_delta_energy(total_energy, *component_energies):\n return total_energy - sum(component_energies)\n\n\nsubstrate_original = filter_atoms_by_tag(ase_original_interface, SUBSTRATE_INDEX)\nlayer_original = filter_atoms_by_tag(ase_original_interface, FILM_INDEX)\nsubstrate_relaxed = filter_atoms_by_tag(ase_final_interface, SUBSTRATE_INDEX)\nlayer_relaxed = filter_atoms_by_tag(ase_final_interface, FILM_INDEX)\n\noriginal_substrate_energy = calculate_energy(substrate_original, calculator)\noriginal_layer_energy = calculate_energy(layer_original, calculator)\nrelaxed_substrate_energy = calculate_energy(substrate_relaxed, calculator)\nrelaxed_layer_energy = calculate_energy(layer_relaxed, calculator)\n\ndelta_original = calculate_delta_energy(original_energy, original_substrate_energy, original_layer_energy)\ndelta_relaxed = calculate_delta_energy(relaxed_energy, relaxed_substrate_energy, relaxed_layer_energy)\n\narea = ase_original_interface.get_volume() / ase_original_interface.cell[2, 2]\nn_interface = ase_final_interface.get_global_number_of_atoms()\nn_substrate = substrate_relaxed.get_global_number_of_atoms()\nn_layer = layer_relaxed.get_global_number_of_atoms()\neffective_delta_relaxed = (\n relaxed_energy / n_interface\n - (relaxed_substrate_energy / n_substrate + relaxed_layer_energy / n_layer)\n ) / (2 * area)\n\nprint(f\"Original Substrate energy: {original_substrate_energy:.4f} eV\")\nprint(f\"Relaxed Substrate energy: {relaxed_substrate_energy:.4f} eV\")\nprint(f\"Original Layer energy: {original_layer_energy:.4f} eV\")\nprint(f\"Relaxed Layer energy: {relaxed_layer_energy:.4f} eV\")\nprint(\"\\nDelta between interface energy and sum of component energies\")\nprint(f\"Original Delta: {delta_original:.4f} eV\")\nprint(f\"Relaxed Delta: {delta_relaxed:.4f} eV\")\nprint(f\"Original Delta per area: {delta_original / area:.4f} eV/Ang^2\")\nprint(f\"Relaxed Delta per area: {delta_relaxed / area:.4f} eV/Ang^2\")\nprint(f\"Relaxed interface energy: {relaxed_energy:.4f} eV\")\nprint(\n f\"Effective relaxed Delta per area: {effective_delta_relaxed:.4f} eV/Ang^2 ({effective_delta_relaxed / 0.16:.4f} J/m^2)\")", + "metadata": { + "trusted": true + }, + "id": "79ea902feda4a8d3", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## References\n\n[1] mat3ra-made interface builder: https://github.com/Exabyte-io/made \n[2] MACE-MP-0 foundation model: https://github.com/ACEsuit/mace?tab=readme-ov-file#foundation-models ", + "metadata": {}, + "id": "2f60fdb73e44c09c" + } + ] +} From 7e880616e7e091e499ea0e19086b884568d0ff82 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 5 Apr 2026 20:36:46 -0700 Subject: [PATCH 3/5] update: use mace model --- ...terface_with_relaxation_mace_pyodide.ipynb | 142 ++++++++++++++---- 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb b/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb index 9d271089..dd48696f 100644 --- a/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb +++ b/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb @@ -91,23 +91,23 @@ " _pyodide_pyfetch = None\n", "\n", "# Mirrors mace.calculators.foundations_models.mace_mp_urls (mace-torch 0.3.x).\n", - "_MACE_MP_URLS = {\n", + "MODEL_PATHS_MAP = {\n", "# \"small\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-10-mace-128-L0_energy_epoch-249.model\",\n", - " \"small\": \"http://localhost:8800/2023-12-10-mace-128-L0_energy_epoch-249.model\",\n", - " \"medium\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-03-mace-128-L1_epoch-199.model\",\n", - " \"large\": \"http://localhost:8800/MACE_MPtrj_2022.9.model\",\n", + "# \"small\": \"/drive/packages/models/2023-12-10-mace-128-L0_energy_epoch-249.model\",\n", + "# \"medium\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-03-mace-128-L1_epoch-199.model\",\n", + " \"large\": \"/drive/packages/models/MACE_MPtrj_2022.9.model\",\n", "# \"large\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/MACE_MPtrj_2022.9.model\",\n", - " \"small-0b\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_small.model\",\n", - " \"medium-0b\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_medium.model\",\n", - " \"small-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-small-density-agnesi-stress.model\",\n", - " \"medium-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-medium-density-agnesi-stress.model\",\n", - " \"large-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-large-density-agnesi-stress.model\",\n", - " \"medium-0b3\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b3/mace-mp-0b3-medium.model\",\n", - " \"medium-mpa-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mpa_0/mace-mpa-0-medium.model\",\n", - " \"small-omat-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_omat_0/mace-omat-0-small.model\",\n", - " \"medium-omat-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_omat_0/mace-omat-0-medium.model\",\n", - " \"mace-matpes-pbe-0\": \"https://github.com/ACEsuit/mace-foundations/releases/download/mace_matpes_0/MACE-matpes-pbe-omat-ft.model\",\n", - " \"mace-matpes-r2scan-0\": \"https://github.com/ACEsuit/mace-foundations/releases/download/mace_matpes_0/MACE-matpes-r2scan-omat-ft.model\",\n", + "# \"small-0b\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_small.model\",\n", + "# \"medium-0b\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_medium.model\",\n", + "# \"small-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-small-density-agnesi-stress.model\",\n", + "# \"medium-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-medium-density-agnesi-stress.model\",\n", + "# \"large-0b2\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b2/mace-large-density-agnesi-stress.model\",\n", + "# \"medium-0b3\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b3/mace-mp-0b3-medium.model\",\n", + "# \"medium-mpa-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_mpa_0/mace-mpa-0-medium.model\",\n", + "# \"small-omat-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_omat_0/mace-omat-0-small.model\",\n", + "# \"medium-omat-0\": \"https://github.com/ACEsuit/mace-mp/releases/download/mace_omat_0/mace-omat-0-medium.model\",\n", + "# \"mace-matpes-pbe-0\": \"https://github.com/ACEsuit/mace-foundations/releases/download/mace_matpes_0/MACE-matpes-pbe-omat-ft.model\",\n", + "# \"mace-matpes-r2scan-0\": \"https://github.com/ACEsuit/mace-foundations/releases/download/mace_matpes_0/MACE-matpes-r2scan-omat-ft.model\",\n", "}\n", "\n", "def _matscipy_neighbour_list_compat(quantities, atoms=None, cutoff=None, positions=None, cell=None, pbc=None, **_):\n", @@ -240,21 +240,6 @@ "\n", "\n", "\n", - " # # torch_dftd (dispersion correction) is a C-extension unavailable in Pyodide.\n", - " # # Stub it so mace.calculators imports succeed; instantiating TorchDFTD3Calculator\n", - " # # raises a clear RuntimeError directing the user to set dispersion=False.\n", - " # if \"torch_dftd\" not in sys.modules:\n", - " # _torch_dftd = types.ModuleType(\"torch_dftd\")\n", - " # _torch_dftd.__path__ = []\n", - " # _torch_dftd3_mod = types.ModuleType(\"torch_dftd.torch_dftd3_calculator\")\n", - " # _torch_dftd3_mod.TorchDFTD3Calculator = _TorchDFTD3CalculatorStub\n", - " # _torch_dftd.torch_dftd3_calculator = _torch_dftd3_mod\n", - " # sys.modules[\"torch_dftd\"] = _torch_dftd\n", - " # sys.modules[\"torch_dftd.torch_dftd3_calculator\"] = _torch_dftd3_mod\n", - "\n", - "\n", - "\n", - "\n", "async def download_mace_model_pyodide(model = None) -> str:\n", " \"\"\"Download a MACE foundation model in Pyodide using the browser's fetch API.\n", "\n", @@ -269,7 +254,7 @@ " \"\"\"\n", " if _pyodide_pyfetch is None:\n", " raise RuntimeError(\"pyodide.http.pyfetch unavailable; call mace_mp() directly in standard Python.\")\n", - " url = _MACE_MP_URLS.get(model or \"medium-mpa-0\", model)\n", + " url = MODEL_PATHS_MAP.get(model or \"medium-mpa-0\", model)\n", " if url is None or not url.startswith(\"http\"):\n", " return model # already a local path\n", " cache_dir = os.path.join(os.path.expanduser(\"~\"), \".cache\", \"mace\")\n", @@ -294,7 +279,44 @@ }, { "cell_type": "code", - "source": "import sys\n\nif sys.platform == \"emscripten\":\n import micropip\n\n\n await micropip.install(\"mat3ra-api-examples\", deps=False)\n await micropip.install(\"mat3ra-utils\")\n from mat3ra.utils.jupyterlite.packages import install_packages\n \n import types\n\n await install_packages(\"api_examples|torch\")\n await micropip.install(\"torch-dftd\")\n\n await micropip.install(\"opt_einsum\")\n await micropip.install(\"opt_einsum_fx\", deps=False)\n await micropip.install(\"e3nn==0.4.4\", deps=False)\n\n # MACE training deps\n await micropip.install(\"prettytable\")\n await micropip.install(\"torch_ema\", deps=False)\n await micropip.install(\"lightning-utilities\", deps=False)\n await micropip.install(\"torchmetrics\", deps=False)\n \n #\n await micropip.install(\"ssl\")\n await micropip.install(\"h5py\")\n await micropip.install(\"lmdb\")\n \n # MACE\n await micropip.install(\"mace-torch\", deps=False)\n patch_mace_for_pyodide()\n", + "source": [ + "import sys\n", + "\n", + "if sys.platform == \"emscripten\":\n", + " import micropip\n", + "\n", + "\n", + " await micropip.install(\"mat3ra-api-examples\", deps=False)\n", + " await micropip.install(\"mat3ra-utils\")\n", + " from mat3ra.utils.jupyterlite.packages import install_packages\n", + " \n", + " import types\n", + "\n", + " await install_packages(\"api_examples|torch\")\n", + " await micropip.install(\"torch-dftd\")\n", + "\n", + " await micropip.install(\"opt_einsum\")\n", + " await micropip.install(\"opt_einsum_fx\", deps=False)\n", + " await micropip.install(\"e3nn==0.4.4\", deps=False)\n", + "\n", + " # MACE training deps\n", + " await micropip.install(\"prettytable\")\n", + " await micropip.install(\"torch_ema\", deps=False)\n", + " await micropip.install(\"lightning-utilities\", deps=False)\n", + " await micropip.install(\"torchmetrics\", deps=False)\n", + " \n", + " #\n", + " await micropip.install(\"ssl\")\n", + " await micropip.install(\"h5py\")\n", + " await micropip.install(\"lmdb\")\n", + "\n", + " await micropip.install(\"orjson\")\n", + " await micropip.install(\"anywidget\")\n", + " \n", + " # MACE\n", + " await micropip.install(\"mace-torch\", deps=False)\n", + " patch_mace_for_pyodide()\n" + ], "metadata": { "trusted": true }, @@ -506,7 +528,61 @@ }, { "cell_type": "code", - "source": "await micropip.install(\"orjson\")\nawait micropip.install(\"anywidget\")\n# await micropip.install(\"matscipy\")\nimport os\nimport plotly.graph_objs as go\nfrom IPython.display import display\nfrom plotly.subplots import make_subplots\n\nfrom mat3ra.made.tools.convert import to_ase\nfrom ase.optimize import BFGS\nfrom mace.calculators import mace_mp\n\n\n\nmodel_path = await download_mace_model_pyodide(MACE_MODEL)\ncalculator = mace_mp(model=model_path, dispersion=False, default_dtype=\"float32\", device=\"cpu\")\n\nase_interface = to_ase(interface)\nase_interface.set_calculator(calculator)\ndyn = BFGS(ase_interface)\n\nsteps = []\nenergies = []\n\nfig = make_subplots(rows=1, cols=1, specs=[[{\"type\": \"scatter\"}]])\nscatter = go.Scatter(x=[], y=[], mode=\"lines+markers\", name=\"Energy\")\nfig.add_trace(scatter)\nfig.update_layout(title_text=\"Real-time Optimization Progress\", xaxis_title=\"Step\", yaxis_title=\"Energy (eV)\")\n\nf = go.FigureWidget(fig)\ndisplay(f)\n\n\ndef plotly_callback():\n step = dyn.nsteps\n energy = ase_interface.get_total_energy()\n steps.append(step)\n energies.append(energy)\n print(f\"Step: {step}, Energy: {energy:.4f} eV\")\n with f.batch_update():\n f.data[0].x = steps\n f.data[0].y = energies\n\n\ndyn.attach(plotly_callback, interval=1)\ndyn.run(fmax=RELAXATION_PARAMETERS[\"FMAX\"])\n\nase_original_interface = to_ase(interface)\nase_original_interface.set_calculator(calculator)\nase_final_interface = ase_interface\n\noriginal_energy = ase_original_interface.get_total_energy()\nrelaxed_energy = ase_interface.get_total_energy()\n\n# print(\"Original structure:\\n\", ase_to_poscar(ase_original_interface))\n# print(\"\\nRelaxed structure:\\n\", ase_to_poscar(ase_final_interface))\nprint(f\"The final energy is {float(relaxed_energy):.3f} eV.\")", + "source": [ + "\n", + "import os\n", + "import plotly.graph_objs as go\n", + "from IPython.display import display\n", + "from plotly.subplots import make_subplots\n", + "\n", + "from mat3ra.made.tools.convert import to_ase\n", + "from ase.optimize import BFGS\n", + "\n", + "from mace.calculators import MACECalculator\n", + "\n", + "calculator = MACECalculator(model_path=MODEL_PATHS_MAP[\"large\"], dispersion=False, default_dtype=\"float32\", device=\"cpu\")\n", + "\n", + "ase_interface = to_ase(interface)\n", + "ase_interface.set_calculator(calculator)\n", + "dyn = BFGS(ase_interface)\n", + "\n", + "steps = []\n", + "energies = []\n", + "\n", + "fig = make_subplots(rows=1, cols=1, specs=[[{\"type\": \"scatter\"}]])\n", + "scatter = go.Scatter(x=[], y=[], mode=\"lines+markers\", name=\"Energy\")\n", + "fig.add_trace(scatter)\n", + "fig.update_layout(title_text=\"Real-time Optimization Progress\", xaxis_title=\"Step\", yaxis_title=\"Energy (eV)\")\n", + "\n", + "f = go.FigureWidget(fig)\n", + "display(f)\n", + "\n", + "\n", + "def plotly_callback():\n", + " step = dyn.nsteps\n", + " energy = ase_interface.get_total_energy()\n", + " steps.append(step)\n", + " energies.append(energy)\n", + " print(f\"Step: {step}, Energy: {energy:.4f} eV\")\n", + " with f.batch_update():\n", + " f.data[0].x = steps\n", + " f.data[0].y = energies\n", + "\n", + "\n", + "dyn.attach(plotly_callback, interval=1)\n", + "dyn.run(fmax=RELAXATION_PARAMETERS[\"FMAX\"])\n", + "\n", + "ase_original_interface = to_ase(interface)\n", + "ase_original_interface.set_calculator(calculator)\n", + "ase_final_interface = ase_interface\n", + "\n", + "original_energy = ase_original_interface.get_total_energy()\n", + "relaxed_energy = ase_interface.get_total_energy()\n", + "\n", + "# print(\"Original structure:\\n\", ase_to_poscar(ase_original_interface))\n", + "# print(\"\\nRelaxed structure:\\n\", ase_to_poscar(ase_final_interface))\n", + "print(f\"The final energy is {float(relaxed_energy):.3f} eV.\")" + ], "metadata": { "jupyter": { "is_executing": true From a6835b2e84895eed57c3e447c513e8673add4915 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 5 Apr 2026 20:47:56 -0700 Subject: [PATCH 4/5] update: move to experimetns jl --- .../create_interface_with_relaxation_mace.ipynb} | 0 other/experiments/jupyterlite/uploads/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename other/experiments/{create_interface_with_relaxation_mace_pyodide.ipynb => jupyterlite/create_interface_with_relaxation_mace.ipynb} (100%) create mode 100644 other/experiments/jupyterlite/uploads/.gitkeep diff --git a/other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb b/other/experiments/jupyterlite/create_interface_with_relaxation_mace.ipynb similarity index 100% rename from other/experiments/create_interface_with_relaxation_mace_pyodide.ipynb rename to other/experiments/jupyterlite/create_interface_with_relaxation_mace.ipynb diff --git a/other/experiments/jupyterlite/uploads/.gitkeep b/other/experiments/jupyterlite/uploads/.gitkeep new file mode 100644 index 00000000..e69de29b From f78139b60d84c815c7dd76c21e6fa494442450c9 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 5 Apr 2026 21:05:06 -0700 Subject: [PATCH 5/5] update: commit config --- config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config.yml b/config.yml index fcd3a261..bb4b54c7 100644 --- a/config.yml +++ b/config.yml @@ -72,3 +72,8 @@ notebooks: - mat3ra-ade - mat3ra-wode - mat3ra-prode + - name: torch + packages_pyodide: + - emfs:/drive/packages/torch-2.1.0a0-cp311-cp311-emscripten_3_1_45_wasm32.whl + # Needed by MACE + - ssl