Periodicity

Not everything worth describing is a crystal. A slab is periodic in two directions, a nanowire in one, a molecule in none. A Cell records which of its three directions actually repeat, so httk can represent all of them and reject operations that only make sense for a crystal.

from httk.atomistic import Cell, Structure

slab = Structure(
    Cell([[3, 0, 0], [0, 3, 0], [0, 0, 1]], periodicity=(True, True, False)),
    sites, species, species_at_sites,
)
slab.periodicity            # (True, True, False)
slab.nperiodic_dimensions   # 2

The default is (True, True, True), representing a fully periodic crystal.

The basis is a frame, not a box

This is the idea the rest follows from. A basis row flagged periodic is a lattice vector, in the usual sense. A row flagged non-periodic is not a lattice vector — it is only a coordinate frame, saying what a fractional coordinate means along that direction.

Three consequences, and they are the whole model:

  • Coordinates there are unbounded. Freely below 0 or above 1, and never wrapped. An atom at z = 1.05 is a real position, one frame-vector out; it is not another way of writing z = 0.05.

  • There is no vacuum and no padding. The frame vector is not a wall and the atoms are not inside a box. Nothing is being separated from its periodic image, because there is no periodic image.

  • A unit frame vector makes the coordinate a length. Set the row to a unit vector and the fractional coordinate along it simply is a displacement in the basis’s units.

That last point is what makes a molecule fall out for free. Give it the identity basis and nothing periodic, and its “fractional” coordinates are literally its ångström coordinates:

water = Structure(
    Cell([[1, 0, 0], [0, 1, 0], [0, 0, 1]], periodicity=(False, False, False)),
    sites=[[0, 0, 0], [0.7576, 0.5865, 0], [-0.7576, 0.5865, 0]],   # angstrom
    species=[oxygen, hydrogen],
    species_at_sites=["O", "H", "H"],
)
water.cartesian_sites().to_floats() == water.sites.reduced_coords.to_floats()   # True

Note the negative coordinate surviving. In a crystal it would have been folded to the far side of the cell; here there is no far side.

All three basis vectors are still required

A cell is always a full, non-degenerate 3×3 basis, whatever its periodicity. A frame vector must be a real vector — a unit one, not a zero one — or fractional coordinates along it mean nothing and the basis cannot be inverted. This is also exactly what OPTIMADE mandates: three lattice vectors regardless of dimension_types, with the ones in non-periodic directions carrying “no significance beyond fulfilling these requirements”.

What changes

Wrapping

Only the periodic directions are wrapped into [0, 1). That matters most in same_crystal(), where folding a non-periodic direction would call the top of a slab the same place as the bottom, and in the minimum-separation cap behind structure_tolerance(), where it would report two atoms 9 Å apart as close neighbours and derive a tolerance far tighter than the data justifies.

Identity

Periodicity takes part in ==, on Cell and on Structure, and in same_crystal. It is not provenance the way a recorded precision is — it says which rows are lattice vectors at all, so a slab is never the same cell as the bulk that happens to share its basis.

The comparison stays exact in the frame as written. Two descriptions of the same slab that differ in their non-periodic frame vector compare unequal, because reconciling them would need an origin convention that same_crystal deliberately does not have.

Volume

volume raises for anything but a full 3D crystal. The determinant of a reduced-periodicity basis mixes real lattice vectors with frame vectors, so it changes when a frame vector is rescaled even though nothing about the material did — and any density or packing fraction derived from it would inherit that.

periodic_measure is the quantity that is always defined: the size of the repeating unit, whatever its dimension.

periodicity

periodic_measure

3D

the cell volume (identical to volume)

2D

the area of the periodic plane

1D

the length of the periodic direction

0D

1 — the empty product, dimensionless

It is exact in the crystallographic case, through the same machinery as lengths: the area of a hexagonal slab of a = 3 comes back as (9/2)·√3 in the surd field, not as a float near it.

What is refused

Space groups describe three-dimensionally periodic lattices, and every part of the symmetry machinery here is built on that — 3×3 rational operations, coordinates reduced modulo , Wyckoff membership as a congruence in all three directions, and tables holding the 230 space groups. A slab or a wire has symmetry too, but it is a layer group or a rod group, which is different tabulated data and different mathematics. httk does not have either.

So these refuse a reduced-periodicity structure outright, rather than answering with machinery whose assumptions it violates:

>>> recognize_asu(slab)
ValueError: recognize_asu requires a fully 3D-periodic structure, but this one is periodic
in 2 of 3 directions ((True, True, False)). ...

Rejecting these operations prevents silent failures: an orbit generated across a non-periodic direction could delete atoms, and a distance folded across one could make an unsymmetric structure pass as symmetric.

Everything else keeps working. lengths, angles, metric() and cartesian_sites() are honest geometry of whatever the three vectors are; only their crystallographic reading changes.

Marking a structure as a slab

Neither CIF nor POSCAR can tell you a structure is not a crystal. VASP expresses a slab purely by convention — a lot of empty space along c — and nothing in the file says so. The periodicity has to come from you:

from httk.atomistic import Cell, Structure, load_structure

relaxed = load_structure("relaxed.POSCAR")
slab = Structure(
    Cell(
        relaxed.cell.unscaled_basis,
        relaxed.cell.scale,
        relaxed.cell.precision,
        (True, True, False),
    ),
    relaxed.sites,
    relaxed.species,
    relaxed.species_at_sites,
)

Rebuilding the Cell through unscaled_basis/scale rather than basis keeps the exact factoring, and passing precision through keeps the stated data precision — see Data precision.

Note what this does not do: it does not remove the empty space. A relaxed VASP slab keeps whatever c it was computed with, and that is correct — the vector is still a perfectly good frame, and the atoms sit where they sit. Marking it non-periodic only stops httk from treating that direction as repeating.

Serving it

nperiodic_dimensions and dimension_types are standard OPTIMADE properties, served from the structure’s own periodicity. dimension_types is ordered by lattice vector, exactly as Cell.periodicity is — entry i refers to basis row i, not to the Cartesian x, y, z.

nperiodic_dimensions carries query-support all mandatory, so it filters:

/v1/structures?filter=nperiodic_dimensions<=2

which is OPTIMADE’s own documented example.

The space-group properties come out null for anything that is not a 3D crystal, which is what OPTIMADE requires. That is not enforced separately — it falls out, because symmetry is only ever served for an ASUStructure and one of those cannot hold a reduced-periodicity cell.

site_coordinate_span stays unit_cell while anything is periodic, since translating the given sites by the lattice vectors does reconstruct the whole system. With nothing periodic there is no periodic system to reconstruct, and the molecular_* values do not fit either — each additionally requires bonded atoms to be placed at a bond distance, which httk does not know — so a molecule is served as other.

See also