POSCAR I/O and result attachment
Public API for to_poscar, write_enumeration_archive, read_results, and attach_results.
Enumlib.to_poscar — Function
to_poscar(io::IO, structure::EnumeratedStructure{D,L},
parent::ParentLattice{D}, hnf::HNF{D};
super_periodic::Bool,
species_symbols::Vector{String} = String[],
comment_extras::Vector{String} = String[],
enumlib_id::Integer = 0,
hnf_idx::Integer = structure.supercell_id) where {D,L}Write a single POSCAR (VASP-5+ format) for structure to io.
Line 1 is a comment carrying identity metadata and an empty energy_eV= slot the calculator fills in after DFT/MLIP. Lines 2 onwards are standard VASP-5+ POSCAR.
Required kwargs:
super_periodic::Bool— theinclude_superperiodicpolicy this structure was enumerated under (true= full Burnside;false= primitive only). Recorded in the header so a reader knows the orbit-policy provenance.
Optional kwargs:
species_symbols::Vector{String}— length ≥ k. Default is["A", "B", "C", ...](one letter per color). Override with real chemistry symbols at the call site if known. Ordering: color 0 → speciessymbols[1], color 1 → speciessymbols[2], etc.comment_extras::Vector{String}— additionalkey=valuepairs inserted before theenergy_eV=slot (e.g.,["author=alice", "calc_id=run42"]).enumlib_id::Integer— the structure's flat ID. Bulk callers (write_enumeration_archive) compute the index inenumeration.structuresand pass it; single-call users can leave at 0 if the ID isn't meaningful.hnf_idx::Integer— the HNF index in the manifest. Default =structure.supercell_id.
Example
julia> using Enumlib
julia> parent = ParentLattice([0.5 0.5 0.0; 0.5 0.0 0.5; 0.0 0.5 0.5]);
julia> sites = Sites([Site([0.0, 0.0, 0.0], [0, 1])]);
julia> e = enumerate(parent, sites; supercells = VolumeRange(2:2));
julia> io = IOBuffer();
julia> hnf_for_structure = e.supercells[e.structures[1].supercell_id].hnf;
julia> to_poscar(io, e.structures[1], parent, hnf_for_structure;
super_periodic = false, enumlib_id = 1);
julia> line1 = first(split(String(take!(io)), '\n'));
julia> startswith(line1, "# radius=") && occursin(" enumlib_id=1 hnf=1 super_periodic=false energy_eV=", line1)
trueEnumlib.write_enumeration_archive — Function
write_enumeration_archive(path, enumeration::Enumeration{D,L};
super_periodic::Bool,
species_symbols::Vector{String} = String[],
label::AbstractString = "",
keep_directory::Bool = false) -> StringWrite every structure in enumeration to a single .tar.gz deliverable at path. Returns the actual tarball path written (may differ from path if path was a directory and the writer auto-named the file).
The tarball contains:
NNNNN_<radius>_<hnf>.POSCAR— one per structure. The leadingNNNNNis zero-padded to fitlength(enumeration.structures)(width =max(5, ndigits(N)), so id-sorted directory listings stay numerically correct up to ~1M structures) so directory listings sort numerically.<radius>is the supercell's average corner-to-center distance in cartesian units (5 sig figs), and<hnf>is the HNF index (no padding). Each POSCAR's header line 1 carries the sameradius=value and anenumlib_id=NNNNNmatching the filename's leading id, plus an emptyenergy_eV=slot the calculator fills in.enumeration.toml— manifest mapping each filename to its structure metadata, plus a[enumeration]top-level section recording the parent lattice (parent_basis_columns), the fullsiteslist (each withpositionandallowed_labels), anyequivalence_classes, theenumlib_version, species symbols, super-periodicity policy, etc. Sufficient to re-enumerate the same set of structures from the manifest alone.
A sidecar <stem>.toml (same contents as the in-tarball enumeration.toml) is also written next to the tarball, so the caller retains local provenance after shipping the tarball to a collaborator.
Arguments
path— output tarball path. Ifpathends in.tar.gzor.tgz, the file is written exactly there. Otherwisepathis treated as a directory and the writer auto-names the tarball inside it asenumlib_<label>_<yyyy-mm-ddTHH-MM-SS>.tar.gz(timestamped to make collisions impossible).
Required kwargs
super_periodic::Bool— theinclude_superperiodicpolicy this enumeration was produced under. Recorded in every POSCAR header and in the manifest's[enumeration]section.
Optional kwargs
species_symbols::Vector{String}— length ≥ k. Default["A", "B", "C", ...](one ASCII letter per color). Override at the call site if real chemistry is known at enumeration time; the calculator can also override at DFT-prep time.label::AbstractString— descriptive component of the auto-named filename (e.g.,"FCC_AgPt_n32_15-17"). If empty, defaults to"enum".keep_directory::Bool = false— if true, also keep the assembled directory next to the tarball at<tarball-stem>/. Useful for inspection or for users who'd rather not extract before editing.extra_per_structure::Union{Nothing,AbstractVector} = nothing— optional per-structure extra manifest fields, parallel toenumeration.structures(length must match). Each entry is akey => valuemapping merged into that structure's[structure.<idx>]table inenumeration.toml. Reserved keys the writer sets itself (hnf_idx,concentration,radius,poscar_filename,hnf_matrix_columns) are not overwritten. Intended for downstream callers (e.g. JuCE) to record per-config selection features — ordinal,v,r,cv— next to the geometry. To ship configs in a chosen order, reorderenumeration.structuresbefore calling (POSCAR ids and manifest keys follow that order) and passextra_per_structurein the same order.
Example
parent = ParentLattice([0.5 0.5 0.0; 0.5 0.0 0.5; 0.0 0.5 0.5])
sites = Sites([Site([0.0, 0.0, 0.0], [0, 1])])
e = enumerate(parent, sites; supercells = VolumeRange(4:4))
out = write_enumeration_archive("./batch1/", e;
super_periodic = false,
species_symbols = ["Ag", "Pt"],
label = "FCC_AgPt_n4")
# out: "./batch1/enumlib_FCC_AgPt_n4_2026-05-08T14-30-00.tar.gz"Enumlib.read_results — Function
read_results(path::AbstractString;
manifest_filename::AbstractString = "enumeration.toml") -> Dict{Int, Float64}Read back DFT/MLIP results from a directory of POSCARs (where collaborators have filled in the energy_eV= slot on each POSCAR's line 1) or from a tarball produced by write_enumeration_archive.
Auto-detects the input form:
pathis a.tar.gz,.tgz, or.tarfile → extracts to a temp dir, reads, cleans up.pathis a directory → reads directly.
Returns a Dict{Int, Float64} mapping each filled-in enumlib_id to its energy in eV. POSCARs whose energy_eV= slot is still empty (calculator hasn't filled them in) are skipped with an @info message listing the missing IDs.
Throws ArgumentError if any POSCAR's line 1 doesn't match the expected format (missing enumlib_id=, missing energy_eV=, or unparseable energy value).
Hand-edit and shell-script workflows both work: collaborators can run e.g. sed -i 's/energy_eV=$/energy_eV=-123.45/' 00042_*.POSCAR after their DFT job, then ship the directory or tarball back.
Example
results = read_results("./batch1_filled.tar.gz")
# Dict(1 => -123.45, 2 => -123.46, ...)
pairs = attach_results(enumeration, results)
# Vector{Tuple{EnumeratedStructure, Float64}}, ready for CE fittingEnumlib.attach_results — Function
attach_results(enumeration::Enumeration{D,L}, results::Dict{Int, Float64})
-> Vector{Tuple{EnumeratedStructure{D,L}, Float64}}Pair each enumlib_id → energy entry in results with the corresponding EnumeratedStructure from enumeration. Returns a flat Vector{Tuple{EnumeratedStructure, Float64}} ready for downstream cluster-expansion or MLIP fitting.
Throws KeyError if any ID in results is out of range [1, length(enumeration.structures)]. Emits an @info message naming structure IDs that appear in the enumeration but not in results (calculator hasn't filled in energy yet — usually expected mid-batch).
A typed EnrichedEnumeration wrapper is intentionally NOT introduced here; the flat tuple list is the simplest interchange shape, and downstream consumers (JuCE.jl's CE fitter, MLIP training pipelines) expect it.