Atomistic Simulation

Original Code

Dependencies
from ase.optimize import LBFGS
from mace.calculators import mace_mp
from rdkit2ase import pack, smiles2conformers

model = mace_mp()

frames = smiles2conformers(smiles="CCO", numConfs=32)
box = pack(data=[frames], counts=[32], density=789)

box.calc = model

dyn = LBFGS(box, trajectory="optim.traj")
dyn.run(fmax=0.5)

Converted Workflow with ZnTrack

To ensure reproducibility, we convert this workflow into a directed graph structure, where each step is represented as a Node. Nodes define their inputs, outputs, and the computational logic to execute.

Here’s the graph structure for our example:

        flowchart LR

Smiles2Conformers --> Pack --> StructureOptimization
MACE_MP --> StructureOptimization
    
from dataclasses import dataclass
from pathlib import Path

import ase.io
from ase.optimize import LBFGS
from mace.calculators import mace_mp
from rdkit2ase import pack, smiles2conformers

import zntrack


class Smiles2Conformers(zntrack.Node):
    smiles: str = zntrack.params()  # A required parameter
    numConfs: int = zntrack.params(32)  # A parameter with a default value

    frames_path: Path = zntrack.outs_path(zntrack.nwd / "frames.xyz")  # Output file path

    def run(self) -> None:
        frames = smiles2conformers(smiles=self.smiles, numConfs=self.numConfs)
        ase.io.write(self.frames_path, frames)

    @property
    def frames(self) -> list[ase.Atoms]:
        # Load the frames from the output file using the node's filesystem
        with self.state.fs.open(self.frames_path, "r") as f:
            return list(ase.io.iread(f, ":", format="extxyz"))


class Pack(zntrack.Node):
    data: list[list[ase.Atoms]] = zntrack.deps()  # Input dependency (list of ASE Atoms)
    counts: list[int] = zntrack.params()  # Parameter (list of counts)
    density: float = zntrack.params()  # Parameter (density value)

    frames_path: Path = zntrack.outs_path(zntrack.nwd / "frames.xyz")  # Output file path

    def run(self) -> None:
        box = pack(data=self.data, counts=self.counts, density=self.density)
        ase.io.write(self.frames_path, box)

    @property
    def frames(self) -> list[ase.Atoms]:
        # Load the packed structure from the output file
        with self.state.fs.open(self.frames_path, "r") as f:
            return list(ase.io.iread(f, ":", format="extxyz"))


# We could hardcode the MACE_MP model into the StructureOptimization Node, but we
# can also define it as a dependency. Since the model doesn't require a `run` method,
# we define it as a `@dataclass`.


@dataclass
class MACE_MP:
    model: str = "medium"  # Default model type

    def get_calculator(self, **kwargs):
        return mace_mp(model=self.model)


class StructureOptimization(zntrack.Node):
    model: MACE_MP = zntrack.deps()  # Dependency (MACE_MP model)
    data: list[ase.Atoms] = zntrack.deps()  # Dependency (list of ASE Atoms)
    data_id: int = zntrack.params()  # Parameter (index of the structure to optimize)
    fmax: float = zntrack.params(0.05)  # Parameter (force convergence threshold)

    frames_path: Path = zntrack.outs_path(zntrack.nwd / "frames.traj")  # Output file path

    def run(self):
        atoms = self.data[self.data_id]
        atoms.calc = self.model.get_calculator()
        dyn = LBFGS(atoms, trajectory=self.frames_path.as_posix())
        dyn.run(fmax=0.5)

    @property
    def frames(self) -> list[ase.Atoms]:
        # Load the optimization trajectory from the output file
        with self.state.fs.open(self.frames_path, "rb") as f:
            return list(ase.io.iread(f, ":", format="traj"))
from src import MACE_MP, Pack, Smiles2Conformers, StructureOptimization

import zntrack

# Initialize the ZnTrack project
project = zntrack.Project()

# Define the MACE-MP model
model = MACE_MP()

# Build the workflow graph
with project:
    etoh = Smiles2Conformers(smiles="CCO", numConfs=32)
    box = Pack(data=[etoh.frames], counts=[32], density=789)
    optm = StructureOptimization(model=model, data=box.frames, data_id=-1, fmax=0.5)

# Execute the workflow
project.repro()

Generated configuration files

dvc.yaml File
stages:
  Pack:
    cmd: zntrack run src.Pack --name Pack
    deps:
    - nodes/Smiles2Conformers/frames.xyz
    metrics:
    - nodes/Pack/node-meta.json:
        cache: false
    outs:
    - nodes/Pack/frames.xyz
    params:
    - Pack
  Smiles2Conformers:
    cmd: zntrack run src.Smiles2Conformers --name Smiles2Conformers
    metrics:
    - nodes/Smiles2Conformers/node-meta.json:
        cache: false
    outs:
    - nodes/Smiles2Conformers/frames.xyz
    params:
    - Smiles2Conformers
  StructureOptimization:
    cmd: zntrack run src.StructureOptimization --name StructureOptimization
    deps:
    - nodes/Pack/frames.xyz
    metrics:
    - nodes/StructureOptimization/node-meta.json:
        cache: false
    outs:
    - nodes/StructureOptimization/frames.traj
    params:
    - StructureOptimization
params.yaml File
Pack:
  counts:
  - 32
  density: 789
Smiles2Conformers:
  numConfs: 32
  smiles: CCO
StructureOptimization:
  data_id: -1
  fmax: 0.5
  model:
    _cls: src.MACE_MP
    model: medium
zntrack.json File
{
    "Smiles2Conformers": {
        "nwd": {
            "_type": "pathlib.Path",
            "value": "nodes/Smiles2Conformers"
        },
        "frames_path": {
            "_type": "pathlib.Path",
            "value": "$nwd$/frames.xyz"
        }
    },
    "Pack": {
        "nwd": {
            "_type": "pathlib.Path",
            "value": "nodes/Pack"
        },
        "data": [
            {
                "_type": "znflow.Connection",
                "value": {
                    "instance": {
                        "_type": "zntrack.Node",
                        "value": {
                            "module": "src",
                            "name": "Smiles2Conformers",
                            "cls": "Smiles2Conformers",
                            "remote": null,
                            "rev": null
                        }
                    },
                    "attribute": "frames",
                    "item": null
                }
            }
        ],
        "frames_path": {
            "_type": "pathlib.Path",
            "value": "$nwd$/frames.xyz"
        }
    },
    "StructureOptimization": {
        "nwd": {
            "_type": "pathlib.Path",
            "value": "nodes/StructureOptimization"
        },
        "model": {
            "_type": "@dataclasses.dataclass",
            "value": {
                "module": "src",
                "cls": "MACE_MP"
            }
        },
        "data": {
            "_type": "znflow.Connection",
            "value": {
                "instance": {
                    "_type": "zntrack.Node",
                    "value": {
                        "module": "src",
                        "name": "Pack",
                        "cls": "Pack",
                        "remote": null,
                        "rev": null
                    }
                },
                "attribute": "frames",
                "item": null
            }
        },
        "frames_path": {
            "_type": "pathlib.Path",
            "value": "$nwd$/frames.traj"
        }
    }
}