1.6 signac-flow HOOMD-blue Example

About

This notebook contains a minimal example for running a signac-flow project from scratch. The example demonstrates how to compare an ideal gas with a Lennard-Jones (LJ) fluid by calculating a p-V phase diagram.

This examples uses the general-purpose simulation toolkit HOOMD-blue for the execution of the molecular-dynamics (MD) simulations.

Author

Carl Simon Adorf, Bradley Dice

Before you start

This example requires signac, signac-flow, HOOMD-blue, gsd, and numpy. You can install these package for example via conda:

conda install -c conda-forge gsd hoomd numpy signac signac-flow
[1]:
import itertools
import os
import warnings

import flow
import gsd.hoomd
import hoomd
import numpy as np
import signac

project_path = "projects/tutorial-signac-flow-hoomd"


class MyProject(flow.FlowProject):
    pass

We want to generate a pressure-volume phase diagram for a Lennard-Jones fluid with molecular dynamics (MD) using the general-purpose simulation toolkit HOOMD-blue (http://glotzerlab.engin.umich.edu/hoomd-blue/).

We start by defining two functions, one for the initialization of our simulation and one for the actual execution.

[2]:
from contextlib import contextmanager
from math import ceil


def init(N):
    frame = gsd.hoomd.Frame()
    frame.particles.N = N
    frame.particles.types = ["A"]
    frame.particles.typeid = np.zeros(N)

    n = ceil(pow(N, 1 / 3))
    assert n**3 == N
    spacing = 1.2
    L = n * spacing
    frame.configuration.box = [L, L, L, 0.0, 0.0, 0.0]
    x = np.linspace(-L / 2, L / 2, n, endpoint=False)
    position = list(itertools.product(x, repeat=3))
    frame.particles.position = position
    with gsd.hoomd.open("init.gsd", "w") as traj:
        traj.append(frame)


@contextmanager
def get_sim(N, sigma, seed, kT, tau, p, tauP, r_cut):
    sim = hoomd.Simulation(hoomd.device.CPU())
    state_fn = "init.gsd"
    if os.path.exists("restart.gsd"):
        state_fn = "restart.gsd"
    sim.create_state_from_gsd(state_fn)
    sim.operations += hoomd.write.GSD(
        filename="restart.gsd", truncate=True, trigger=100, filter=hoomd.filter.All()
    )
    lj = hoomd.md.pair.LJ(default_r_cut=r_cut, nlist=hoomd.md.nlist.Cell(0.4))
    lj.params[("A", "A")] = {"epsilon": 1.0, "sigma": sigma}
    integrator = hoomd.md.Integrator(
        dt=0.005,
        methods=[
            hoomd.md.methods.ConstantPressure(
                filter=hoomd.filter.All(),
                S=p,
                tauS=tauP,
                couple="xyz",
                thermostat=hoomd.md.methods.thermostats.MTTK(kT=kT, tau=tau),
            )
        ],
        forces=[lj],
    )
    sim.operations.integrator = integrator
    logger = hoomd.logging.Logger(categories=("scalar", "string"))
    logger["Volume"] = (lambda: sim.state.box.volume, "scalar")

    # This context manager keeps the log file open while the simulation runs.
    with open("dump.log", "w+") as outfile:
        table = hoomd.write.Table(
            trigger=100, logger=logger, output=outfile, pretty=False
        )
        sim.operations += table
        yield sim

We want to use signac to manage our simulation data and signac-flow to define a workflow acting on the data space.

Now that we have defined the core simulation logic above, it’s time to embed those functions into a general workflow. For this purpose we add labels and operations with preconditions/postconditions to MyProject, a subclass of flow.FlowProject.

The estimate operation stores an ideal gas estimation of the volume for the given system. The sample operation actually executes the MD simulation as defined in the previous cell.

[3]:
@MyProject.label
def estimated(job):
    return "V" in job.document


@MyProject.label
def sampled(job):
    return job.document.get("sample_step", 0) >= 5000


@MyProject.post(estimated)
@MyProject.operation
def estimate(job):
    sp = job.statepoint()
    job.document["V"] = sp["N"] * sp["kT"] / sp["p"]


@MyProject.post(sampled)
@MyProject.operation
def sample(job):
    with job:
        with get_sim(**job.statepoint()) as sim:
            try:
                sim.run(5_000)
            finally:
                job.document["sample_step"] = sim.timestep

Now it’s time to actually generate some data! Let’s initialize the data space!

[4]:
project = MyProject.init_project(project_path)

# Uncomment the following two lines if you want to start over!
# for job in project:
#     job.remove()

for p in np.linspace(0.5, 5.0, 10):
    sp = dict(
        N=512, sigma=1.0, seed=42, kT=1.0, p=float(p), tau=1.0, tauP=1.0, r_cut=2.5
    )
    job = project.open_job(sp).init()
    with job:
        init(N=sp["N"])

The print_status() function allows to get a quick overview of our project’s status:

[5]:
project.print_status(detailed=True, parameters=["p"])

Overview: 10 jobs/aggregates, 10 jobs/aggregates with eligible operations.

label
-------


operation/group      number of eligible jobs  submission status
-----------------  -------------------------  -------------------
estimate                                  10  [U]: 10
sample                                    10  [U]: 10

Detailed View:

job id                            operation/group      p  labels
--------------------------------  -----------------  ---  --------
18c063206c37ab03d06de130c6ae2b70  estimate [U]       1
                                  sample [U]         1
2a05a487f254db90b08f576d199a83b7  estimate [U]       5
                                  sample [U]         5
4ef567c185064a4b27b73f62124991c0  estimate [U]       3
                                  sample [U]         3
5b3c8f0fbd9458a304222d83bb82bc30  estimate [U]       3.5
                                  sample [U]         3.5
6681f083e2b03f5b9816f35502e411f7  estimate [U]       1.5
                                  sample [U]         1.5
6ff6b952b42f0ad3e1315e137cf67cdb  estimate [U]       4
                                  sample [U]         4
99f5397cf2f6250afe34927606aa2f37  estimate [U]       4.5
                                  sample [U]         4.5
a5d4427e8d285c78239005f740a2625b  estimate [U]       2
                                  sample [U]         2
a89bc9f5cceaf268f18efed936a70333  estimate [U]       0.5
                                  sample [U]         0.5
c342fff540bfa0a2128efac700fc5aef  estimate [U]       2.5
                                  sample [U]         2.5

[U]:unknown [R]:registered [I]:inactive [S]:submitted [H]:held [Q]:queued [A]:active [E]:error [GR]:group_registered [GI]:group_inactive [GS]:group_submitted [GH]:group_held [GQ]:group_queued [GA]:group_active [GE]:group_error


The next cell will attempt to execute all eligible operations.

[6]:
with warnings.catch_warnings():
    # Hide deprecation warnings from hoomd
    warnings.simplefilter("ignore")
    project.run()

Let’s double check the project status.

[7]:
project.print_status(detailed=True)

Overview: 10 jobs/aggregates, 0 jobs/aggregates with eligible operations.

label      ratio
---------  ----------------------------------------------------------
estimated  |████████████████████████████████████████| 10/10 (100.00%)
sampled    |████████████████████████████████████████| 10/10 (100.00%)

operation/group
-----------------


Detailed View:

job id                            operation/group    labels
--------------------------------  -----------------  ------------------
18c063206c37ab03d06de130c6ae2b70  [ ]                estimated, sampled
2a05a487f254db90b08f576d199a83b7  [ ]                estimated, sampled
4ef567c185064a4b27b73f62124991c0  [ ]                estimated, sampled
5b3c8f0fbd9458a304222d83bb82bc30  [ ]                estimated, sampled
6681f083e2b03f5b9816f35502e411f7  [ ]                estimated, sampled
6ff6b952b42f0ad3e1315e137cf67cdb  [ ]                estimated, sampled
99f5397cf2f6250afe34927606aa2f37  [ ]                estimated, sampled
a5d4427e8d285c78239005f740a2625b  [ ]                estimated, sampled
a89bc9f5cceaf268f18efed936a70333  [ ]                estimated, sampled
c342fff540bfa0a2128efac700fc5aef  [ ]                estimated, sampled

[U]:unknown [R]:registered [I]:inactive [S]:submitted [H]:held [Q]:queued [A]:active [E]:error [GR]:group_registered [GI]:group_inactive [GS]:group_submitted [GH]:group_held [GQ]:group_queued [GA]:group_active [GE]:group_error


After running all operations we can make a brief examination of the collected data.

[8]:
def get_volume(job):
    log = np.genfromtxt(job.fn("dump.log"), names=True)
    N = len(log)
    return log[int(0.5 * N) :]["Volume"].mean(axis=0)


for job in project:
    print(job.statepoint["p"], get_volume(job), job.document.get("V"))
3.5 633.09071616 146.28571428571428
0.5 1238.75831864 1024.0
1.5 789.8835313200001 341.3333333333333
2.0 732.7477800800001 256.0
4.0 616.6703303999999 128.0
1.0 931.94641252 512.0
4.5 603.5544676 113.77777777777777
5.0 592.8023312 102.4
3.0 663.6705322 170.66666666666666
2.5 676.3909288000001 204.8

For a better presentation of the results we need to aggregate all results and sort them by pressure.

The following code requires matplotlib.

[9]:
%matplotlib inline
# Display plots within the notebook

from matplotlib import pyplot as plt

V = dict()
V_idg = dict()

for job in project:
    V[job.statepoint()["p"]] = get_volume(job)
    V_idg[job.statepoint()["p"]] = job.document["V"]

p = sorted(V.keys())
V = [V[p_] for p_ in p]
V_idg = [V_idg[p_] for p_ in p]

plt.plot(p, V, label="LJ")
plt.plot(p, V_idg, label="idG")
plt.xlabel(r"pressure [$\epsilon / \sigma^3$]")
plt.ylabel(r"volume [$\sigma^3$]")
plt.legend()
plt.show()
../../_images/examples_notebooks_signac_106_signac-flow_HOOMD-blue_Example_17_0.png

Uncomment and execute the following line to remove all data and start over.

[10]:
# %rm -r projects/tutorial-signac-flow-hoomd-blue/workspace