Atomistic Simulation¶
Original Code¶
Dependencies
For this example, you will need:
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"
}
}
}