POSCAR I/O and result attachment

Public API for to_poscar, write_enumeration_archive, read_results, and attach_results.

Enumlib.to_poscarFunction
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 — the include_superperiodic policy 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} — additional key=value pairs inserted before the energy_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 in enumeration.structures and 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)
true
source
Enumlib.write_enumeration_archiveFunction
write_enumeration_archive(path, enumeration::Enumeration{D,L};
                           super_periodic::Bool,
                           species_symbols::Vector{String} = String[],
                           label::AbstractString = "",
                           keep_directory::Bool = false) -> String

Write 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 leading NNNNN is zero-padded to fit length(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 same radius= value and an enumlib_id=NNNNN matching the filename's leading id, plus an empty energy_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 full sites list (each with position and allowed_labels), any equivalence_classes, the enumlib_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. If path ends in .tar.gz or .tgz, the file is written exactly there. Otherwise path is treated as a directory and the writer auto-names the tarball inside it as enumlib_<label>_<yyyy-mm-ddTHH-MM-SS>.tar.gz (timestamped to make collisions impossible).

Required kwargs

  • super_periodic::Bool — the include_superperiodic policy 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 to enumeration.structures (length must match). Each entry is a key => value mapping merged into that structure's [structure.<idx>] table in enumeration.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, reorder enumeration.structures before calling (POSCAR ids and manifest keys follow that order) and pass extra_per_structure in 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"
source
Enumlib.read_resultsFunction
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:

  • path is a .tar.gz, .tgz, or .tar file → extracts to a temp dir, reads, cleans up.
  • path is 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 fitting
source
Enumlib.attach_resultsFunction
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.

source