Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Git and version control
.git
.gitignore
.gitattributes

# IDE and editor files
.vscode
.cursor
.continue
.claude
.omc
.playwright-cli
.research
.env
.idea
*.swp
*.swo
*~
.DS_Store

# Python
__pycache__
*.py[cod]
*$py.class
*.egg-info
dist
build
.egg
.pytest_cache
.mypy_cache
.ruff_cache
*.egg
venv
env
.venv

# Test and coverage
coverage
htmlcov
.coverage
.coverage.*

# Data and artifacts
*.log
.tmp
tmp
*.pth
*.pt
*.pickle
*.pkl
checkpoint*
dataset
output
training_*.log

# Docker
.dockerignore
Dockerfile

# CI/CD
.github
.gitlab-ci.yml

# Documentation build
docs/_build
site

# Misc
*.md
!README.md
!docs/DOCKER_RESEARCH.md
!START_HERE.md
!QUICK_START.md
!docs/*.md
!build_heterojunctions/README.md
.env.local
.env.*.local
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INTERFACEML_PORT=8001
JUPYTER_PORT=8888
INTERFACEML_WORKSPACE=./.research
INTERFACEML_UID=1000
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main, develop]
branches: [main, devel]
pull_request:
branches: [main]
branches: [main, devel]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ dmypy.json
*.rej
nohup.out
webserver.log
.env
.research/
output/playwright/
.playwright-cli/

# Output files (keep examples but ignore test outputs)
examples/output/*.vasp
Expand Down
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM python:3.11-slim@sha256:e41613d42d4891e4930f79523f93f81bbc7632584ec65e36ab055f41a800b41e

WORKDIR /app

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
MPLCONFIGDIR=/tmp/matplotlib \
OMP_NUM_THREADS=2

RUN apt-get update && apt-get install -y --no-install-recommends libgomp1 git \
&& rm -rf /var/lib/apt/lists/*

COPY requirements-research.lock ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --require-hashes -r requirements-research.lock

COPY . .
RUN pip install --no-cache-dir --no-deps .

ARG USER_ID=1000
RUN useradd -m -u "${USER_ID}" interfaceml \
&& mkdir -p /workspace \
&& chown interfaceml:interfaceml /workspace
USER interfaceml

HEALTHCHECK --interval=15s --timeout=10s --start-period=60s --retries=5 \
CMD python -c "import json, urllib.request; data = json.load(urllib.request.urlopen('http://localhost:5000/api/health')); assert data['status'] == 'ok' and data['core_available']"

EXPOSE 5000 8888

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "1", "--threads", "4", "--timeout", "300", "interfaceml.web.app:app"]
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ InterfaceML provides a complete suite of tools for heterojunction modeling with

## Installation

For an ARM64 CPU environment with the web application and JupyterLab, see
[Research Containers](docs/DOCKER_RESEARCH.md).

### Prerequisites
- Python 3.9 or higher
- pip or conda package manager
Expand Down
33 changes: 6 additions & 27 deletions active_learning/dft_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,36 +321,15 @@ def _build_constraint_block(

def _get_fixed_atoms_from_poscar(poscar_path: Path) -> List[int]:
"""Extract fixed atom indices (0-based) from POSCAR selective dynamics."""
from pymatgen.io.vasp import Poscar

try:
lines = poscar_path.read_text().splitlines()
except Exception:
flags = Poscar.from_file(str(poscar_path), check_for_potcar=False).selective_dynamics
except (OSError, ValueError, IndexError):
return []

# Find selective dynamics line
sd_line_idx = None
for i, line in enumerate(lines):
if line.strip().lower().startswith("selective"):
sd_line_idx = i
break

if sd_line_idx is None:
if flags is None:
return []

# Coordinate lines start after the selective dynamics line
coord_start = sd_line_idx + 1
fixed = []
atom_idx = 0
for line in lines[coord_start:]:
parts = line.split()
if len(parts) < 6:
break
# Selective dynamics flags are the last 3 columns (T/F)
flags = parts[-3:]
if all(f.upper() == "F" for f in flags):
fixed.append(atom_idx)
atom_idx += 1

return fixed
return [index for index, movable in enumerate(flags) if not np.any(movable)]


def _write_submit_all(output_base: Path, jobs: List[dict]) -> None:
Expand Down
45 changes: 45 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: interfaceml-research

x-runtime: &runtime
platform: linux/arm64
image: interfaceml-research:local
build:
context: .
args:
USER_ID: ${INTERFACEML_UID:-1000}
init: true
restart: unless-stopped
volumes:
- ${INTERFACEML_WORKSPACE:-./.research}:/workspace
environment:
PYTHONPATH: /app
INTERFACEML_FULLERENE_PATH: /app/fullerene_e3gen
INTERFACEML_UPLOAD_FOLDER: /workspace/uploads
INTERFACEML_FULLERENE_CHECKPOINT: /workspace/models/egnn/best_model.pt
INTERFACEML_PAINN_FM_CHECKPOINT: /workspace/models/painn_fm/best_model.pt

services:
interfaceml:
<<: *runtime
ports:
- "127.0.0.1:${INTERFACEML_PORT:-8001}:5000"

research:
<<: *runtime
profiles: [research]
ports:
- "127.0.0.1:${JUPYTER_PORT:-8888}:8888"
working_dir: /workspace
command:
- jupyter
- lab
- --ip=0.0.0.0
- --port=8888
- --no-browser
- --ServerApp.root_dir=/workspace
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8888/login').read()"]
interval: 15s
timeout: 10s
start_period: 60s
retries: 5
105 changes: 105 additions & 0 deletions docs/DOCKER_RESEARCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Research Containers

This environment targets Linux ARM64 on Apple Silicon, with CPU PyTorch.
The application and JupyterLab share one image and a persistent research directory.
CP2K input generation is included; a CP2K executable and cluster access are separate.

## Start

Set these optional values in a local `.env` file:

```dotenv
INTERFACEML_PORT=8001
JUPYTER_PORT=8888
INTERFACEML_WORKSPACE=./.research
INTERFACEML_UID=1000
```

For a host-owned research directory, use its absolute path and set
`INTERFACEML_UID` to the numeric result of `id -u`. `.env` is excluded from Git
and from the Docker build context. Existing containers from other projects are
not used by this Compose project.

```bash
docker compose --profile research up --build -d
docker compose --profile research ps
```

- InterfaceML: <http://localhost:8001/app>
- JupyterLab: <http://localhost:8888/lab>
- Health: <http://localhost:8001/api/health>

JupyterLab keeps token authentication enabled. Get its local access URL with:

```bash
docker compose exec research jupyter server list
```

The host research directory is mounted at `/workspace` in both services.
Notebooks, uploaded structures, and calculation outputs stored there survive
container recreation. The application upload directory is `/workspace/uploads`.
Download links for DFT archives are currently session-scoped; the ZIP files remain
in the research directory after a web-process restart.

## Work

```bash
docker compose exec research python
docker compose exec research bash
docker compose exec research python -m pip check
docker compose exec -w /app research python -m pytest tests/ -q
```

The environment includes NumPy, SciPy, pandas, pymatgen, PyTorch, PyTorch Geometric,
Matplotlib, Flask, JupyterLab, pytest, and Ruff. The container imports the same
application sources used by the web service through `/app`.

Optional trained models can be placed in the research directory:

```text
models/egnn/best_model.pt
models/painn_fm/best_model.pt
```

Restart the web service after adding a checkpoint. Without checkpoints, structure
building, analysis, and DFT preparation remain available; AI generation reports
that its models are unavailable. CPU containers do not expose Apple Metal/MPS.

## Stop And Restart

```bash
docker compose --profile research stop
docker compose --profile research start
docker compose --profile research down
```

The bind-mounted research directory is retained by these commands.

## Rebuild Dependencies

The Python base image is pinned by digest. Python package versions and hashes are
recorded in `requirements-research.lock`; the PyTorch CPU index supplies its CPU
wheel. Regenerate the lock after changing `requirements.txt` or
`requirements-research.in`:

```bash
uv pip compile requirements-research.in --python-version 3.11 \
--python-platform aarch64-unknown-linux-gnu --torch-backend cpu \
--index-strategy unsafe-best-match \
--default-index https://pypi.org/simple --emit-index-url --generate-hashes \
--output-file requirements-research.lock
docker compose --profile research build
docker compose --profile research up -d
```

This lock and image target ARM64 CPU. An x86 or NVIDIA GPU deployment requires
its own dependency resolution and runtime validation.

## Scientific Scope

The coherent-interface builder uses pymatgen lattice matching and strains the film
to the substrate. Bidirectional matching changes the comparison direction; it does
not distribute strain between the two materials. A subsequent arbitrary film
rotation is a geometric operation and has not been validated as a commensurate
periodic supercell. Generated CP2K/SLURM files still require material parameters,
cluster settings, and convergence validation appropriate to the calculation.
1 change: 1 addition & 0 deletions interfaceml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# Import available core modules
try:
from interfaceml.core import io, layering

__all__ = ["io", "layering"]
except ImportError:
__all__ = []
Loading
Loading