diff --git a/config.yml b/config.yml index ded2226d2..3ef077674 100644 --- a/config.yml +++ b/config.yml @@ -142,3 +142,22 @@ notebooks: - ssl - h5py - lmdb + - name: sevennet + packages_pyodide: + # Packages with dependencies + - opt_einsum + - orjson + - pyyaml + - setuptools + # Packages without dependencies + - nodeps:opt_einsum_fx + - nodeps:e3nn>=0.5 + - nodeps:ase + - nodeps:monty + - scikit-learn + # SevenNet slim wheel (stripped to 7net-0 model only) + - emfs:/drive/packages/sevenn-0.12.1-py3-none-any.whl + # Stubbed packages (patched by torch_pyodide with include_sevennet=True) + - ssl + - h5py + - lmdb diff --git a/other/experiments/jupyterlite/relax_structure_with_sevennet.ipynb b/other/experiments/jupyterlite/relax_structure_with_sevennet.ipynb new file mode 100644 index 000000000..19762af34 --- /dev/null +++ b/other/experiments/jupyterlite/relax_structure_with_sevennet.ipynb @@ -0,0 +1,296 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "33da1875", + "metadata": {}, + "source": [ + "# Relax Structure with SevenNet \u2014 E(3)-Equivariant GNN Potential\n", + "\n", + "This notebook demonstrates structural relaxation using **SevenNet** (7net-0),\n", + "an E(3)-equivariant graph neural network interatomic potential based on the NequIP architecture.\n", + "\n", + "The model supports all elements up to Z=89 and predicts energies, forces, and stresses." + ] + }, + { + "cell_type": "markdown", + "id": "d3ea9252", + "metadata": {}, + "source": [ + "## 1. Set Input Parameters\n", + "### 1.1. Structure and Relaxation" + ] + }, + { + "cell_type": "code", + "id": "d9ef36f6", + "metadata": {}, + "source": [ + "FOLDER = \"uploads\"\n", + "STRUCTURE_NAME = \"Interface\" # Name of the structure to load from local file\n", + "\n", + "RELAXATION_PARAMETERS = {\n", + " \"FMAX\": 0.05,\n", + "}" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "f5709f12", + "metadata": {}, + "source": [ + "## 2. Install Packages" + ] + }, + { + "cell_type": "code", + "id": "766bc77a", + "metadata": {}, + "source": [ + "from mat3ra.notebooks_utils.packages import install_packages\n", + "\n", + "await install_packages(\"made|api_examples|torch|sevennet\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "0cedf7f5", + "metadata": {}, + "source": [ + "from mat3ra.notebooks_utils.pyodide.packages.torch import apply_all_patches\n", + "\n", + "apply_all_patches(include_sevennet=True)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "b8624f00", + "metadata": {}, + "source": [ + "## 3. Load Materials" + ] + }, + { + "cell_type": "code", + "id": "b6c6efa8", + "metadata": {}, + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.notebooks_utils.material import load_material_from_folder\n", + "from mat3ra.standata.materials import Materials\n", + "\n", + "structure = load_material_from_folder(FOLDER, STRUCTURE_NAME) or Material.create(\n", + " Materials.get_by_name_first_match(STRUCTURE_NAME))\n", + "\n", + "print(f\"INFO: Found: '{structure.name}'\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "b0491324", + "metadata": {}, + "source": [ + "### 3.1. Visualize Input Structure" + ] + }, + { + "cell_type": "code", + "id": "630c1c78", + "metadata": {}, + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import ViewersEnum, visualize_materials as visualize\n", + "\n", + "visualize(structure, repetitions=[1, 1, 1], rotation=\"-90x\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "00170e3a", + "metadata": {}, + "source": [ + "## 4. Apply Relaxation\n", + "### 4.1. Load SevenNet Model and Create Calculator" + ] + }, + { + "cell_type": "code", + "id": "4c06be2b", + "metadata": {}, + "source": [ + "from sevenn.calculator import SevenNetCalculator\n", + "\n", + "# Use the bundled pretrained 7net-0 model\n", + "calculator = SevenNetCalculator(model=\"7net-0\", device=\"cpu\")\n", + "\n", + "print(\"SevenNet 7net-0 model loaded\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "ebaa0f39", + "metadata": {}, + "source": [ + "### 4.2. Relax with SevenNet" + ] + }, + { + "cell_type": "code", + "id": "f17d5d6d", + "metadata": {}, + "source": [ + "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", + "ase_structure = to_ase(structure)\n", + "ase_structure.calc = calculator\n", + "dyn = BFGS(ase_structure)\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", + "try:\n", + " f = go.FigureWidget(fig)\n", + "except ImportError:\n", + " f = go.Figure(fig)\n", + "display(f)\n", + "\n", + "\n", + "def plotly_callback():\n", + " step = dyn.nsteps\n", + " energy = ase_structure.get_total_energy()\n", + " steps.append(step)\n", + " energies.append(energy)\n", + " print(f\"Step: {step}, Energy: {energy:.4f} eV\")\n", + " if hasattr(f, \"batch_update\"):\n", + " with f.batch_update():\n", + " f.data[0].x = steps\n", + " f.data[0].y = energies\n", + " else:\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_structure = to_ase(structure)\n", + "ase_original_structure.calc = calculator\n", + "ase_final_structure = ase_structure\n", + "\n", + "original_energy = ase_original_structure.get_total_energy()\n", + "relaxed_energy = ase_structure.get_total_energy()\n", + "\n", + "print(f\"The final energy is {float(relaxed_energy):.3f} eV.\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "e788e0e3", + "metadata": {}, + "source": [ + "## 5. Analyze Results\n", + "### 5.1. View Structure Before and After Relaxation" + ] + }, + { + "cell_type": "code", + "id": "0391a838", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.convert import from_ase\n", + "\n", + "material_original = Material.create(from_ase(ase_original_structure))\n", + "material_relaxed = Material.create(from_ase(ase_final_structure))\n", + "material_original.name = structure.name\n", + "material_relaxed.name = structure.name + \" (SevenNet Relaxed)\"\n", + "\n", + "visualize([material_original, material_relaxed], viewer=ViewersEnum.wave)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "41bba468", + "metadata": {}, + "source": [ + "### 5.2. Output interlayer distance before and after relaxation" + ] + }, + { + "cell_type": "code", + "id": "3f19487a", + "metadata": {}, + "source": [ + "from mat3ra.made.tools.analyze.other import get_average_interlayer_distance\n", + "\n", + "SUBSTRATE_TAG = 0\n", + "FILM_TAG = 1\n", + "\n", + "print(\n", + " f\"Interlayer distance before relaxation: {get_average_interlayer_distance(material_original, SUBSTRATE_TAG, FILM_TAG):.4f} \u00c5\")\n", + "print(\n", + " f\"Interlayer distance after relaxation: {get_average_interlayer_distance(material_relaxed, SUBSTRATE_TAG, FILM_TAG):.4f} \u00c5\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "8c7ae982", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] SevenNet: https://github.com/MDIL-SNU/SevenNet\n", + "\n", + "[2] Yutack Park et al., \"Scalable Parallel Algorithm for Graph Neural Network Interatomic Potentials,\" J. Chem. Theory Comput. (2024)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (Pyodide)", + "language": "python", + "name": "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": 4, + "nbformat_minor": 5 +} diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 000000000..db269077f --- /dev/null +++ b/packages/README.md @@ -0,0 +1,148 @@ +# Packages for Pyodide/JupyterLite + +This directory contains pre-built Python wheels used by the JupyterLite notebooks. Wheels are loaded at runtime via `emfs:/drive/packages/` in [config.yml](../config.yml). + +## Wheel Sources + +### Built by [Exabyte-io/build-pyodide](https://github.com/Exabyte-io/build-pyodide) + +The following wheels are compiled for the Emscripten/WASM target (`cp311-emscripten_3_1_4x_wasm32`) using the CI workflows in the [build-pyodide](https://github.com/Exabyte-io/build-pyodide) repository: + +| Wheel | Notes | +|-------|-------| +| `torch-2.1.0a0-cp311-cp311-emscripten_3_1_45_wasm32.whl` | PyTorch for Pyodide (WASM build) | +| `pymatgen-2024.*.whl` (emscripten) | Pymatgen with C extensions compiled for WASM | +| `spglib-2.5.0-cp311-cp311-emscripten_3_1_46_wasm32.whl` | Spglib with C extensions compiled for WASM | +| `icet-3.0-cp311-cp311-emscripten_3_1_45_wasm32.whl` | ICET for WASM | +| `numba-0.60.0-cp311-cp311-emscripten_3_1_45_wasm32.whl` | Numba for WASM | +| `pydantic_core-2.18.2-py3-none-any.whl` | Pydantic core (Rust → pure-Python stub) | + +### Downloaded from PyPI (unmodified) + +These are standard pure-Python wheels downloaded directly from [PyPI](https://pypi.org) with no modifications: + +| Wheel | PyPI | +|-------|------| +| `antlr4_python3_runtime-4.9.3-py3-none-any.whl` | [antlr4-python3-runtime](https://pypi.org/project/antlr4-python3-runtime/4.9.3/) | +| `paginate-0.5.6-py3-none-any.whl` | [paginate](https://pypi.org/project/paginate/0.5.6/) | +| `pydantic-2.7.1-py3-none-any.whl` | [pydantic](https://pypi.org/project/pydantic/2.7.1/) | +| `pymatgen-2024.4.13-py3-none-any.whl` | [pymatgen](https://pypi.org/project/pymatgen/2024.4.13/) (pure-Python variant) | +| `ruamel.yaml-0.17.32-py3-none-any.whl` | [ruamel.yaml](https://pypi.org/project/ruamel.yaml/0.17.32/) | +| `spglib-2.0.2-py3-none-any.whl` | [spglib](https://pypi.org/project/spglib/2.0.2/) (pure-Python variant) | +| `watchdog-2.3.1-py3-none-any.whl` | [watchdog](https://pypi.org/project/watchdog/2.3.1/) | + +### Custom-built wheels (not in build-pyodide) + +These wheels required manual modifications to work in Pyodide and are **not** built by the build-pyodide CI. Instructions to reproduce each are below. + +--- + +#### `mattersim-1.1.2-py3-none-any.whl` (70 KB) + +**Source**: [microsoft/mattersim](https://github.com/microsoft/mattersim) v1.1.2 + +**Why custom**: The upstream MatterSim package uses Cython-compiled extensions (`_m3gnet_cython.pyx`) that cannot run in Pyodide's WASM runtime. The custom wheel replaces these with pure NumPy equivalents. + +**How to reproduce**: + +```bash +# 1. Download source +pip download mattersim==1.1.2 --no-deps --no-binary :all: +tar xf mattersim-1.1.2.tar.gz && cd mattersim-1.1.2 + +# 2. Remove Cython build requirement +# Edit pyproject.toml: remove "cython" from build-system.requires + +# 3. Replace Cython extensions with NumPy implementations +# In mattersim/forcefield/m3gnet/modules/: +# - Remove _m3gnet_cython.pyx and any .so/.c compiled files +# - The M3GNet module code falls back to pure NumPy when Cython is unavailable + +# 4. Build pure-Python wheel +pip wheel . --no-deps --no-build-isolation +# Output: mattersim-1.1.2-py3-none-any.whl +``` + +**Runtime patches**: Requires `apply_all_patches(include_mattersim=True)` in [torch.py](../src/py/mat3ra/notebooks_utils/pyodide/packages/torch.py) which stubs `loguru`, `azure`, `e3nn` JIT, `torch_geometric`, and `torch_runstats`. + +--- + +#### `sevenn-0.12.1-py3-none-any.whl` (9.2 MB) + +**Source**: [MDIL-SNU/SevenNet](https://github.com/MDIL-SNU/SevenNet) v0.12.1 (PyPI) + +**Why custom**: The upstream wheel is 42.6 MB and includes 4 pretrained models + a CUDA-only native library (`pair_d3.so`). We strip it down to the single model needed for inference. + +**How to reproduce**: + +```bash +# 1. Download the upstream wheel +pip download sevenn==0.12.1 --no-deps +# sevenn-0.12.1-py3-none-any.whl (~42.6 MB) + +# 2. Extract, strip, and repack +python3 << 'EOF' +import zipfile, os, shutil + +src_whl = "sevenn-0.12.1-py3-none-any.whl" +work_dir = "sevenn_work" + +# Extract +with zipfile.ZipFile(src_whl, 'r') as z: + z.extractall(work_dir) + +pkg = os.path.join(work_dir, "sevenn") + +# Remove CUDA D3 dispersion library (not needed for CPU inference) +for f in ["pair_d3.so"]: + p = os.path.join(pkg, f) + if os.path.exists(p): + os.remove(p) + +# Remove CUDA D3 source code +shutil.rmtree(os.path.join(pkg, "pair_e3gnn"), ignore_errors=True) + +# Keep only 7net-0 (latest), remove other pretrained models +pp = os.path.join(pkg, "pretrained_potentials") +for name in os.listdir(pp): + if name != "SevenNet_0__11Jul2024": + shutil.rmtree(os.path.join(pp, name)) + +# Repack +out_whl = "sevenn-0.12.1-py3-none-any.whl" +with zipfile.ZipFile(out_whl, 'w', zipfile.ZIP_DEFLATED) as zf: + for root, dirs, files in os.walk(work_dir): + for f in files: + full = os.path.join(root, f) + arcname = os.path.relpath(full, work_dir) + zf.write(full, arcname) + +shutil.rmtree(work_dir) +EOF +# Output: sevenn-0.12.1-py3-none-any.whl (~9.2 MB) +``` + +**What was removed** (saves ~33 MB): +- `pair_d3.so` — CUDA-only D3 dispersion native library (1.8 MB) +- `pair_e3gnn/` — C source for D3 dispersion (3.2 MB) +- `pretrained_potentials/SevenNet_0__22May2024/` — older 7net-0 checkpoint (9.8 MB) +- `pretrained_potentials/SevenNet_MF_0/` — multi-fidelity model (9.8 MB) +- `pretrained_potentials/SevenNet_l3i5/` — large model variant (14 MB) + +**What was kept**: +- `pretrained_potentials/SevenNet_0__11Jul2024/checkpoint_sevennet_0.pth` — 7net-0 model (9.8 MB) +- All Python source code (nn/, train/, calculator.py, etc.) + +**Runtime patches**: Requires `apply_all_patches(include_sevennet=True)` in [torch.py](../src/py/mat3ra/notebooks_utils/pyodide/packages/torch.py) which stubs `pandas`, `braceexpand`, `tqdm`, provides a `torch_geometric.data.Data` replacement, and sets `e3nn` to eager mode (no JIT). + +--- + +## Models + +The `models/` subdirectory contains pretrained model checkpoint files: + +| File | Model | Size | +|------|-------|------| +| `mattersim-v1.0.0-1M.pth` | MatterSim M3GNet (1M params) | ~4 MB | + +> **Note**: The SevenNet 7net-0 model is bundled inside the `sevenn` wheel itself under `pretrained_potentials/`. diff --git a/packages/sevenn-0.12.1-py3-none-any.whl b/packages/sevenn-0.12.1-py3-none-any.whl new file mode 100644 index 000000000..09ba2efca --- /dev/null +++ b/packages/sevenn-0.12.1-py3-none-any.whl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffe87dc08cd72213e049c4c152bc9d392361e1d6b1d0e671364f1e40178b7622 +size 9607673 diff --git a/src/py/mat3ra/notebooks_utils/pyodide/packages/torch.py b/src/py/mat3ra/notebooks_utils/pyodide/packages/torch.py index 1cf7cf829..15270680b 100644 --- a/src/py/mat3ra/notebooks_utils/pyodide/packages/torch.py +++ b/src/py/mat3ra/notebooks_utils/pyodide/packages/torch.py @@ -959,7 +959,7 @@ def patch_mace_tools(): # ============================================================================== -def apply_all_patches(include_fairchem=False, include_mattersim=False): +def apply_all_patches(include_fairchem=False, include_mattersim=False, include_sevennet=False): """ Apply all torch and model patches for Pyodide in one call. @@ -970,6 +970,9 @@ def apply_all_patches(include_fairchem=False, include_mattersim=False): include_mattersim: If True, also apply MatterSim-specific patches (loguru, azure, e3nn JIT stubs). Set this when using MatterSim / M3GNet models. + include_sevennet: If True, also apply SevenNet-specific patches + (pandas, tqdm, packaging stubs). Set this when + using SevenNet / 7net models. """ patch_torch_linalg() patch_torch_compiler() @@ -986,6 +989,10 @@ def apply_all_patches(include_fairchem=False, include_mattersim=False): patch_torch_distributed() patch_mattersim_deps() + if include_sevennet: + patch_torch_distributed() + patch_sevennet_deps() + print("\n✅ All Pyodide patches applied successfully!") @@ -1207,3 +1214,206 @@ def _scatter_mean(src, index, dim_size=None, dim=0): sys.modules["torch_runstats.scatter"].scatter_mean = _scatter_mean print("✓ MatterSim dependency stubs applied") + + +# ============================================================================== +# SevenNet patches +# ============================================================================== + + +def patch_sevennet_deps(): + """ + Stub heavy dependencies required by SevenNet but not needed for inference. + + Stubs: pandas, braceexpand, tqdm, packaging, and patches + torch_geometric.data.Data for SevenNet's AtomGraphData. + """ + import sys + + # --- packaging (used in __init__.py version check) --- + # Only stub if real packaging is not available + if "packaging.version" not in sys.modules: + try: + import packaging.version # noqa: F401 + except (ImportError, ModuleNotFoundError): + pkg = _make_stub_module("packaging", submodules=["version"]) + + class _Version: + def __init__(self, v): + self._v = str(v) + parts = self._v.split(".") + self.major = int(parts[0]) if parts else 0 + self.minor = int(parts[1]) if len(parts) > 1 else 0 + self.micro = int(parts[2]) if len(parts) > 2 else 0 + + def __lt__(self, other): + return (self.major, self.minor, self.micro) < (other.major, other.minor, other.micro) + + def __ge__(self, other): + return not self.__lt__(other) + + def __repr__(self): + return f"Version('{self._v}')" + + sys.modules["packaging.version"].Version = _Version + sys.modules["packaging.version"].parse = lambda v: _Version(v) + + # --- pandas (used in checkpoint.py, not needed for inference) --- + try: + import pandas # noqa: F401 + except (ImportError, ModuleNotFoundError): + pd = _make_stub_module("pandas", submodules=["core", "core.frame"]) + + class _DataFrame: + def __init__(self, *a, **k): + pass + pd.DataFrame = _DataFrame + sys.modules["pandas.core.frame"].DataFrame = _DataFrame + + # --- braceexpand (used in train/dataload.py) --- + try: + import braceexpand # noqa: F401 + except (ImportError, ModuleNotFoundError): + be = _make_stub_module("braceexpand") + be.braceexpand = lambda s: [s] + + # --- tqdm (used in util.py and train) --- + try: + import tqdm # noqa: F401 + except (ImportError, ModuleNotFoundError): + tqdm_mod = _make_stub_module("tqdm", submodules=["auto"]) + + class _tqdm: + def __init__(self, iterable=None, *a, **k): + self._it = iterable + + def __iter__(self): + return iter(self._it) if self._it else iter([]) + + def __enter__(self): + return self + + def __exit__(self, *a): + pass + + def update(self, *a): + pass + + def close(self): + pass + + tqdm_mod.tqdm = _tqdm + sys.modules["tqdm.auto"].tqdm = _tqdm + + # --- torch_geometric.data.Data needs proper item access for SevenNet --- + import torch + tg_data = sys.modules.get("torch_geometric.data") + if tg_data is None: + _make_stub_module("torch_geometric", submodules=["data", "loader", "utils"]) + tg_data = sys.modules["torch_geometric.data"] + + class _SevenNetData: + """torch_geometric-compatible Data with dict-like access for SevenNet.""" + def __init__(self, x=None, edge_index=None, edge_attr=None, y=None, pos=None, **kwargs): + self._store = {} + if x is not None: + self._store['x'] = x + if edge_index is not None: + self._store['edge_index'] = edge_index + if edge_attr is not None: + self._store['edge_attr'] = edge_attr + if y is not None: + self._store['y'] = y + if pos is not None: + self._store['pos'] = pos + for k, v in kwargs.items(): + self._store[k] = v + + def __setitem__(self, key, value): + self._store[key] = value + + def __getitem__(self, key): + return self._store[key] + + def __contains__(self, key): + return key in self._store + + def __delitem__(self, key): + del self._store[key] + + def __setattr__(self, name, value): + if name == '_store': + super().__setattr__(name, value) + else: + self._store[name] = value + + def __getattr__(self, name): + if name == '_store': + raise AttributeError(name) + try: + return self._store[name] + except KeyError: + raise AttributeError(name) + + def to(self, device): + for k, v in self._store.items(): + if isinstance(v, torch.Tensor): + self._store[k] = v.to(device) + return self + + def to_dict(self): + return dict(self._store) + + def keys(self): + return self._store.keys() + + @classmethod + def from_numpy_dict(cls, d): + """Convert numpy arrays to tensors.""" + import numpy as np + obj = cls() + for k, v in d.items(): + if isinstance(v, np.ndarray): + if v.dtype in (np.float32, np.float64): + obj._store[k] = torch.from_numpy(v).float() + elif v.dtype in (np.int32, np.int64): + obj._store[k] = torch.from_numpy(v).long() + else: + obj._store[k] = torch.from_numpy(v) + elif isinstance(v, (int, float)): + obj._store[k] = v + else: + obj._store[k] = v + return obj + + tg_data.Data = _SevenNetData + + # --- patch e3nn to skip JIT compilation --- + try: + import e3nn + e3nn._SO3_INITIALIZED = True + # Set eager mode to avoid torch.jit.script (not supported in Pyodide) + if hasattr(e3nn, '_OPT_DEFAULTS'): + e3nn._OPT_DEFAULTS["jit_mode"] = "eager" + except Exception: + pass + + # --- patch torch.jit for e3nn --- + if not hasattr(torch.jit, '_original_script'): + _orig_script = torch.jit.script + + def _noop_script(obj=None, *a, **k): + if obj is not None: + return obj + return lambda fn: fn + + torch.jit.script = _noop_script + + # --- patch e3nn compile_mode decorator --- + try: + import e3nn.util.jit + e3nn.util.jit.compile_mode = lambda mode: lambda cls: cls + except Exception: + pass + + print("✓ SevenNet dependency stubs applied") diff --git a/tests/playwright/debug_sevennet.mjs b/tests/playwright/debug_sevennet.mjs new file mode 100644 index 000000000..f1435acbd --- /dev/null +++ b/tests/playwright/debug_sevennet.mjs @@ -0,0 +1,42 @@ +import { chromium } from "playwright"; +const URL = "http://localhost:8000/lab/index.html?path=experiments/relax_structure_with_sevennet.ipynb"; +const b = await chromium.launch({ channel: "chrome", headless: false }); +const p = await b.newPage(); +await p.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 }); +await p.waitForFunction(() => { + const ind = document.querySelector(".jp-Notebook-ExecutionIndicator"); + return ind?.dataset?.status === "idle" && document.querySelectorAll(".jp-CodeCell").length > 5; +}, null, { timeout: 120000, polling: 2000 }); +await p.locator('button[data-command="runmenu:restart-and-run-all"]').click(); +await p.waitForTimeout(1000); +try { await p.locator(".jp-Dialog button", { hasText: /restart/i }).click({ timeout: 3000 }); } catch {} +for (let i = 0; i < 30; i++) { + await p.waitForTimeout(5000); + const s = await p.evaluate(() => { + const k = document.querySelector(".jp-Notebook-ExecutionIndicator")?.dataset?.status; + let running = 0; + document.querySelectorAll(".jp-CodeCell .jp-InputPrompt").forEach(e => { + if (e.textContent.trim() === "[*]:") running++; + }); + return { k, running }; + }); + if (s.k === "idle" && s.running === 0) break; +} +const err = await p.evaluate(() => { + const cells = document.querySelectorAll(".jp-CodeCell"); + const cell = cells[6]; // cell 7 + if (!cell) return "no cell 7"; + // Get just stderr/error outputs, skip plotly JS + let text = ""; + cell.querySelectorAll(".jp-OutputArea-output").forEach((e, i) => { + const t = e.textContent || ""; + // Skip massive plotly JS, only get error outputs + if (t.includes("Traceback") || t.includes("Error") || t.includes("Step:")) { + // Get last 3000 chars to capture the actual error + text += `[output ${i}]: ${t.slice(-3000)}\n`; + } + }); + return text || "No error outputs found"; +}); +console.log(err); +await b.close(); diff --git a/tests/playwright/debug_sevennet_cell7.mjs b/tests/playwright/debug_sevennet_cell7.mjs new file mode 100644 index 000000000..88d69168b --- /dev/null +++ b/tests/playwright/debug_sevennet_cell7.mjs @@ -0,0 +1,47 @@ +import { chromium } from "playwright"; + +const URL = "http://localhost:8000/lab/index.html?path=experiments/relax_structure_with_sevennet.ipynb"; + +const b = await chromium.launch({ channel: "chrome", headless: false }); +const p = await b.newPage(); +await p.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 }); +await p.waitForFunction(() => { + const ind = document.querySelector(".jp-Notebook-ExecutionIndicator"); + return ind?.dataset?.status === "idle" && document.querySelectorAll(".jp-CodeCell").length > 5; +}, null, { timeout: 120000, polling: 2000 }); +await p.locator('button[data-command="runmenu:restart-and-run-all"]').click(); +await p.waitForTimeout(1000); +try { await p.locator(".jp-Dialog button", { hasText: /restart/i }).click({ timeout: 3000 }); } catch {} + +for (let i = 0; i < 30; i++) { + await p.waitForTimeout(5000); + const s = await p.evaluate(() => { + const k = document.querySelector(".jp-Notebook-ExecutionIndicator")?.dataset?.status; + let running = 0; + document.querySelectorAll(".jp-CodeCell .jp-InputPrompt").forEach(e => { + if (e.textContent.trim() === "[*]:") running++; + }); + return { k, running }; + }); + if (s.k === "idle" && s.running === 0) break; +} + +// Get cell 8 error (skip plotly JS) +const err = await p.evaluate(() => { + const cells = document.querySelectorAll(".jp-CodeCell"); + const cell8 = cells[7]; + if (!cell8) return "no cell 8"; + let text = ""; + cell8.querySelectorAll(".jp-OutputArea-output").forEach((e, i) => { + const t = e.textContent || ""; + // Skip plotly JS blobs + if (t.length > 100000) { + text += `[output ${i}: ${t.length} chars, skipped]\n`; + } else { + text += `[output ${i}]: ${t}\n`; + } + }); + return text; +}); +console.log(err); +await b.close(); diff --git a/tests/playwright/test_sevennet_notebook.mjs b/tests/playwright/test_sevennet_notebook.mjs new file mode 100644 index 000000000..b4f400acf --- /dev/null +++ b/tests/playwright/test_sevennet_notebook.mjs @@ -0,0 +1,93 @@ +import { chromium } from "playwright"; + +const URL = + "http://localhost:8000/lab/index.html?path=experiments/relax_structure_with_sevennet.ipynb"; + +(async () => { + const browser = await chromium.launch({ channel: "chrome", headless: false }); + const page = await browser.newPage(); + + console.log("⏳ Navigating to JupyterLite..."); + await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60_000 }); + + // Wait for kernel idle + console.log("⏳ Waiting for Pyodide kernel..."); + await page.waitForFunction( + () => { + const ind = document.querySelector(".jp-Notebook-ExecutionIndicator"); + return ind?.dataset?.status === "idle" && document.querySelectorAll(".jp-CodeCell").length > 5; + }, + null, + { timeout: 120_000, polling: 2_000 } + ); + console.log("✅ Kernel idle!"); + + // Click Restart & Run All + await page.locator('button[data-command="runmenu:restart-and-run-all"]').click(); + await page.waitForTimeout(1_000); + try { + await page.locator(".jp-Dialog button", { hasText: /restart/i }).click({ timeout: 3_000 }); + console.log("✅ Restart & Run All confirmed!"); + } catch { + console.log("⚠ No dialog"); + } + + // Monitor with logging + console.log("\n📊 Monitoring..."); + const start = Date.now(); + + while (Date.now() - start < 300_000) { + await page.waitForTimeout(10_000); + + const s = await page.evaluate(() => { + const cells = document.querySelectorAll(".jp-CodeCell"); + let running = 0, done = 0, errors = 0; + cells.forEach((c) => { + const p = c.querySelector(".jp-InputPrompt")?.textContent?.trim() || ""; + if (p === "[*]:") running++; + else if (/\[\d+\]:/.test(p)) done++; + if (c.querySelector(".jp-RenderedText[data-mime-type='application/vnd.jupyter.stderr']")) errors++; + }); + const k = document.querySelector(".jp-Notebook-ExecutionIndicator")?.dataset?.status; + return { running, done, errors, total: cells.length, k }; + }); + + const t = ((Date.now() - start) / 1000) | 0; + console.log(` [${t}s] kernel=${s.k} running=${s.running} done=${s.done}/${s.total} errors=${s.errors}`); + + if (s.k === "idle" && s.running === 0 && s.done > 0) break; + } + + // Final report + console.log("\n" + "=".repeat(60)); + const results = await page.evaluate(() => + Array.from(document.querySelectorAll(".jp-CodeCell")).map((c, i) => { + let t = ""; + for (const o of c.querySelectorAll(".jp-OutputArea-output")) t += o.textContent; + return { cell: i + 1, err: t.includes("Traceback"), out: t }; + }) + ); + + let failures = 0; + for (const r of results) { + if (r.err) { + failures++; + console.log(`Cell ${r.cell} ❌`); + console.log(r.out.substring(0, 2000)); + console.log(); + } else { + const preview = r.out.trim().substring(0, 120).replace(/\n/g, " | "); + console.log(`Cell ${r.cell} ✅${preview ? ": " + preview : ""}`); + } + } + + console.log("=".repeat(60)); + if (failures > 0) { + console.log(`\n⚠ ${failures} cell(s) failed`); + process.exitCode = 1; + } else { + console.log(`\n🎉 ALL ${results.length} CELLS PASSED!`); + } + + await browser.close(); +})();