Tutorial: Read a dump and classify#
Prerequisites#
Lua 5.4 on
PATHmeson >= 1.3, ninja, a C++20 compiler, pkg-config
This repository, with work from the repository root so the dump path resolves
The Quickstart build, or the meson / Nix steps in Step 1
Learning Objectives#
By the end of this tutorial you will be able to:
Call
require("dseams")from a stockluaprocessRead one LAMMPS dump frame into a
PointCloudLabel each oxygen with
dseams.chill_plusFlag hexagonal-cage and double-diamond-cage membership with
dseams.cagesSee why those two tables answer different questions
This repository does not pin the CHILL+ histogram. Print the counts
you get. Do not treat a water-heavy split as a failed run.
Step 1: Build the library#
Meson. LUA_PATH finds lua/dseams.lua. LUA_CPATH finds
dseams_core.so.
meson setup bbdir --wrap-mode=nofallback
meson compile -C bbdir
export LUA_PATH="$PWD/lua/?.lua;;"
export LUA_CPATH="$PWD/bbdir/?.so;;"
Nix sets the same two variables to the installed library:
nix build
nix develop
If you already ran the Quickstart, the exports (or the Nix shell) are enough. Skip the compile.
Step 2: Require and read#
input/traj/exampleTraj.lammpstrj is one frame of 750 atoms: 500
of LAMMPS type 1 (hydrogen) and 250 of type 2 (oxygen). Ice
helpers classify oxygen.
The shipped reader is example_lua/library/read.lua:
local dseams = require("dseams")
local cloud = dseams.read("input/traj/exampleTraj.lammpstrj", {type = 2})
assert(cloud.nop > 0, "empty cloud")
print(string.format("dseams_lib nop=%d", cloud.nop))
lua example_lua/library/read.lua
You should see:
dseams_lib nop=250
dseams.read keeps one atom type. Pass {type = 2} on this dump.
chill_plus and cages build neighbour graphs with
opts.type or 1, so they need type = 2 as well. A cloud of
oxygens plus a type-1 neighbour filter is an empty graph.
Step 3: Label with CHILL+#
Save this as /tmp/read-and-classify.lua. It is the rest of the
tutorial.
local dseams = require("dseams")
local cloud = dseams.read("input/traj/exampleTraj.lammpstrj", {type = 2})
assert(cloud.nop > 0, "empty cloud")
local box = cloud:box()
print(string.format("nop=%d frame=%d box={%.2f, %.2f, %.2f}",
cloud.nop, cloud.currentFrame, box[1], box[2], box[3]))
local types = dseams.chill_plus(cloud, {cutoff = 3.5, type = 2})
assert(#types == cloud.nop)
local counts = {}
for i = 1, #types do
counts[types[i]] = (counts[types[i]] or 0) + 1
end
local parts = {}
for name, n in pairs(counts) do
parts[#parts + 1] = string.format("%s=%d", name, n)
end
table.sort(parts)
print("chill_plus " .. table.concat(parts, " "))
local aff = dseams.cages(cloud, {type = 2})
assert(#aff.hc == cloud.nop and #aff.ddc == cloud.nop)
local function nflag(flags)
local n = 0
for i = 1, #flags do
if flags[i] and flags[i] ~= 0 then
n = n + 1
end
end
return n
end
print(string.format("cages hc=%d ddc=%d nop=%d",
nflag(aff.hc), nflag(aff.ddc), cloud.nop))
lua /tmp/read-and-classify.lua
The first line is the 250 oxygens, frame 1, and the box
(40 x 40 x 180 Angstrom, a long z). The ice-nanotube examples in
example_lua/iceNanotube/ use this dump.
dseams.chill_plus builds a 3.5 Angstrom cutoff neighbour list,
runs getCorrelPlus, and returns getIceTypePlusNoPrint. No file
is written. The cloud is mutated. The return value is a 1-based
array of state names, length cloud.nop.
Names you can see: cubic, hexagonal, water, interfacial,
clathrate, interClathrate, unclassified.
CHILL+ looks at the four-neighbour shell. A bond with correlation <= -0.8 is staggered; a bond in [-0.35, 0.25] is eclipsed.
4 staggered: cubic
3 staggered and 1 eclipsed: hexagonal
4 eclipsed: clathrate
3 eclipsed: interClathrate
mixed ice-like bonds: interfacial
anything else, or not 4-coordinated: water
Print the histogram. Add the counts. They sum to 250. This tree
does not assert a particular split. A confined water dump can
land mostly in water and interfacial. That is a
classification, not a failed run.
Step 4: Flag cages#
dseams.cages builds two 4-nearest graphs (mutual = strict,
union = permissive; candidate cutoff 5.0), keeps six-membered
rings, and returns seededCageAffiliation: a table
{hc = ..., ddc = ...} of per-atom flags, each length
cloud.nop.
hc[i]is true when oxygenibelongs to an accepted hexagonal cage.ddc[i]is true when it belongs to an accepted double-diamond cage.
HC and DDC are bulk ice motifs (two basal six-rings plus prisms; one equatorial six-ring plus six peripherals). Seeded affiliation accepts a permissive-graph atom only when its affiliated component contains a mutual-graph seed. An empty strict pass accepts nothing.
Print hc and ddc counts. Zero is a valid answer: this dump
is confined water, not a bulk ice lattice.
Step 5: Read both tables#
chill_plus is a local bond-order label. cages is six-ring
membership. A cubic oxygen need not sit in a DDC. An HC oxygen
need not be labeled hexagonal. Read both tables; do not expect
them to match.
The helpers live in lua/dseams.lua. The compiled names they
call are on dseams.core; see require(“dseams”).
Complete Example Script#
The block in Step 3 is the complete script. Save it as
/tmp/read-and-classify.lua and run it from the repository root
with LUA_PATH and LUA_CPATH set.
Fennel#
(require :dseams) is the same Lua table. Names stay
snake_case. The vendored compiler is
src/include/external/fennel/fennel.lua.
(local dseams (require :dseams))
(local cloud (dseams.read "input/traj/exampleTraj.lammpstrj" {:type 2}))
(local types (dseams.chill_plus cloud {:cutoff 3.5 :type 2}))
(local aff (dseams.cages cloud {:type 2}))
(print (string.format "nop=%d ntypes=%d nhc=%d nddc=%d"
cloud.nop (length types) (length aff.hc)
(length aff.ddc)))
Save the snippet as /tmp/read-and-classify.fnl, or run the
shipped file example_lua/library/chill.fnl (that one stops
after chill_plus):
lua src/include/external/fennel/fennel.lua /tmp/read-and-classify.fnl
lua src/include/external/fennel/fennel.lua example_lua/library/chill.fnl
How Fennel finds dseams, and how lua/dseams.fnl adds
chill-plus, is in Call dseams from Fennel.
Troubleshooting#
module ‘dseams’ not found#
LUA_PATH does not contain lua/?.lua. From the repository
root:
export LUA_PATH="$PWD/lua/?.lua;;"
module ‘dseams_core’ not found#
LUA_CPATH does not contain the build directory that holds
dseams_core.so:
export LUA_CPATH="$PWD/bbdir/?.so;;"
empty cloud, or nop=0#
You omitted {type = 2} on a dump whose oxygens are type 2, or
you ran the script from a directory where
input/traj/exampleTraj.lammpstrj does not exist. Work from the
repository root.
cages hc=0 ddc=0#
Zero is a valid answer on this dump. Confined water need not contain an accepted hexagonal cage or double-diamond cage.
Import-style globals (readFrameOnlyOne is nil)#
Scripts that mention readFrameOnlyOne, functionScript, or
trajectory as a global expect the 2020 driver. Rewrite them to
require("dseams"). See The library, not a second CLI.
Next Steps#
Call dseams from Fennel :: Lisp syntax on the same table
Embed in a Lua host :: Load the
.sofrom your own processrequire(“dseams”) :: Helpers and
dseams.corenamesInstall the library :: How the
.sois built and found
Summary#
You learned to:
✓ Call
require("dseams")from stocklua✓ Read one dump frame into a
PointCloud✓ Label oxygens with
dseams.chill_plus✓ Flag cage membership with
dseams.cages✓ Treat the two tables as different questions
The helpers stay in lua/dseams.lua. The compiled registrations
stay on dseams.core.