Piecing (super-large structures)¶
Voxel library substitution for super-large structures (“piecing”).
Why this exists¶
Atom-HiFi selects a minimal representative subset of structures from a library
by DECAF-clustering atomic environments and running a greedy set-cover, scoring
each candidate subset with the fidelity metric F = L / R. This works when
the library holds many small-to-medium structures.
It breaks when the library is a handful of very large structures (e.g. ~11
cells of ~1000 atoms). Each giant structure already contains nearly every
microstate, so a single structure covers the whole motif vocabulary: the
set-cover returns |S| = 1, the bandwidth factor c* inflates, and F
saturates. This is an artefact of unit granularity, not chemistry — the
selection unit (a whole 1000-atom cell) is far coarser than the thing being
covered (a local atomic environment).
The fix: cut each large structure into many small, spatially local pieces
and run the selection with pieces as the library units. A bulk piece carries
bulk motifs, a defect piece carries defect motifs — the units are now
heterogeneous, set-cover regains a meaningful gradient, and F behaves.
Pieces are a statistics-layer cut only¶
Descriptors are never recomputed. They are computed once on the intact
periodic cell (the descriptor’s own r_cut already encodes the local
environment), and a piece simply selects rows of that pre-computed matrix.
Physically carving atoms out would change the chemical environment of boundary
atoms (severed bonds, fake surfaces) and corrupt their descriptors — so we do
not do it. A piece is pure index bookkeeping plus a row permutation; the
numbers fed to DECAF are bit-identical to the intact-cell numbers.
Because the cut touches only which unit owns each atom (never the descriptor
values), the whole downstream stack — Judge, HiFiWorkflow, the 1-D /
grid / NSGA-II scans, the set-cover — runs unmodified and unaware that pieces
exist. This module is the container boundary: the runner calls
make_pieces() to wrap, runs the normal workflow, then calls
assign_back() to unwrap the per-atom group labels back onto the original
structures.
How a structure is cut: the voxel grid¶
Pieces are disjoint sub-cells of a fractional-coordinate grid:
Estimate the structure’s atom density
ρ = N / V(V= cell volume).Target voxel edge
L = (target_atoms / ρ) ** (1/3)— the cube edge that would, at this density, holdtarget_atomsatoms.Divide each cell vector i into
n_i = max(1, round(|cell_i| / L))parts. The grid lives in fractional coordinates, so it is automatically PBC-correct and triclinic-safe (sub-cells are parallelepipeds congruent with the cell, not axis-aligned cubes).Each atom’s wrapped scaled position lands it in exactly one voxel. Every non-empty voxel becomes one piece; empty voxels are dropped; sparse voxels (a few atoms near a surface) are kept as small pieces — harmless, they just cover little.
target_atoms is therefore a granularity dial, not a hard size: actual
piece populations vary with local density. Exposing atom-count rather than a
voxel edge in Å keeps the knob system-size- and density-independent.
Spatial locality is mandatory. The partition must be spatial — chopping the atom list into arbitrary index chunks would put a uniform random sample of microstates in every chunk, re-creating the very artefact this module fixes.
Pass-through¶
Structures with len(atoms) <= 4 * target_atoms are already fine-grained and
pass through uncut (one piece == the whole structure). Mixed libraries (a few
giants among many small structures) work transparently.
Reporting note¶
When piecing is active, |S| and fidelity reports count pieces, not
structures. Representative output contains unique selected original parents,
with every focus atom annotated. assign_back() returns annotated copies
of every original structure (motif_id, piece_id, in_selection); when
a fitted Judge is available the runner writes these copies to diagnosed.xyz
and their three per-atom arrays to diagnosed.npz.
- class atom_hifi.piecing.PieceMap(parent_idx, atom_idx, n_orig, target_atoms)[source]¶
Bases:
objectBookkeeping to invert a
make_pieces()cut.- Variables:
parent_idx (list[int]) –
parent_idx[p]= index of the original structure piece p came from.atom_idx (list[np.ndarray]) –
atom_idx[p]= atom indices (into the parent structure’s full-atom array, ascending) that belong to piece p. Piece-local atom j corresponds to parent atomatom_idx[p][j].n_orig (int) – Number of original structures.
target_atoms (int) – The granularity dial used for the cut (recorded for provenance).
- Parameters:
- atom_hifi.piecing.make_pieces(lib, desc_list, target_atoms)[source]¶
Cut large structures into disjoint voxel pieces (bookkeeping only).
- Parameters:
lib (list[ase.Atoms]) – Original structures (full atoms, intact cells).
desc_list (list[np.ndarray]) – Per-structure descriptor arrays,
desc_list[i]of shape(len(lib[i]), D)— one row per atom, in structure order. Computed on the intact cell; never recomputed here.target_atoms (int) – Granularity dial (approx. atoms per voxel).
- Returns:
pieces_lib (list[ase.Atoms]) – One
Atomsper non-empty piece (subset of a parent; parent cell/pbc retained). Drop-in replacement forlib.pieces_desc_list (list[np.ndarray]) – Matching per-piece descriptor rows (selected from
desc_list, never recomputed). Drop-in replacement for the per-structure descriptor list.piece_map (PieceMap) – Inverse-mapping bookkeeping for
assign_back().
- atom_hifi.piecing.original_representatives(piece_map, lib, selected_piece_idx, per_el_group_libs, species, meta_by_group=None, noise_by_group=None)[source]¶
Return selected full parents with every focus atom annotated.
Selection remains piece-wise. Parent structures are deduplicated in first selected-piece order, while group ids are gathered from all sibling pieces belonging to each selected parent.
- atom_hifi.piecing.assign_back(piece_map, lib, per_el_group_libs, species, selected_piece_idx, ids_are_global=False)[source]¶
Map per-piece group labels back onto the original structures.
Pure inverse bookkeeping — no inference, no OOD. Each piece’s per-element DECAF group ids (in piece-local ascending atom order, exactly as the focus filter produced them) are scattered onto the parent structure’s atoms.
- Parameters:
piece_map (PieceMap) – From
make_pieces().lib (list[ase.Atoms]) – The original structures (not pieces).
per_el_group_libs (dict[str, list]) –
per_el_group_libs[el][p]= list of group ids for element el’s atoms of piece p (e.g.judge.per_el_group_libs).species (list[str]) – Focus elements, in the order used to build the joint group space.
selected_piece_idx (iterable[int]) – Piece indices chosen by set-cover (the representative subset).
ids_are_global (bool) – True when
per_el_group_libsalready carries refined/sorted global group ids (Judge.apply_refined_group_ids, issue #40) — element offsets are skipped so ids are not offset twice.
- Returns:
Copies of the original structures, each carrying integer
arrays:motif_id(joint-encoded group id; -1 for non-focus atoms),piece_id(global piece index owning the atom), andin_selection(1 if the owning piece was selected, else 0).- Return type:
list[ase.Atoms]