Count without enumerating

Use count_inequivalent when you want to know "how many structures would I get" without actually generating them. Instead of building every coloring and throwing away the duplicates, it works out how many distinct ones there must be directly from the supercell's symmetry group — the Pólya count, an application of Burnside's lemma, unpacked in Pólya counting. The cost is O(|G| · n) per supercell: sub-second across the full reference corpus, even at sizes where the matching enumerate(...) call would take minutes.

That makes it the natural pre-flight check: run it first, look at the number, then decide whether to enumerate, narrow the volume range, or add a concentration constraint. The count is exact, not an estimate — it matches length(enumerate(...)) for the same arguments.

Setup

julia> using Enumlib

julia> p = ParentLattice([0.0 0.5 0.5; 0.5 0.0 0.5; 0.5 0.5 0.0]);    # FCC primitive

julia> sites = Sites([Site([0.0, 0.0, 0.0], [0, 1])]);               # binary

A single number

julia> count_inequivalent(p, sites; supercells = VolumeRange(4:4))   # FCC binary, n=4
19

julia> count_inequivalent(p, sites; supercells = VolumeRange(12:12)) # the canonical n=12 reference
7140

The matching length(enumerate(...)) calls would return the same numbers — but enumerate actually allocates the structures, while count_inequivalent is closed-form.

A breakdown across volumes

Pass breakdown = true to get an InequivalentCount with per-volume and per-HNF breakdowns:

julia> ic = count_inequivalent(p, sites; supercells = VolumeRange(2:6), breakdown = true);

julia> ic.total
135

julia> ic.by_volume                                                  # sorted (n, count) tuples
5-element Vector{Tuple{Int64, BigInt}}:
 (2, 2)
 (3, 6)
 (4, 19)
 (5, 28)
 (6, 80)

ic.by_hnf carries one (hnf, count) entry per inequivalent HNF — useful for diagnosing which supercells dominate the structure count.

Reading by_volume before enumerating is the cheapest way to find where a volume sweep turns expensive: the per-volume count typically grows by a large factor per step, so the last volume in the range usually dominates the total.

Heterogeneous Sites

When each sublattice carries its own species — zinc-blende, half/full Heusler, perovskite, or any site pinned to a single label — the count is computed with the label-restricted Pólya formulas, which intersect allowed_labels across each orbit rather than assuming every position is free over all k species. Nothing about the call changes; the dispatch is automatic.

Zinc-blende: an FCC parent with a binary cation sublattice at the origin and a binary anion sublattice at (¼, ¼, ¼).

julia> zb = ParentLattice([0.0 0.5 0.5; 0.5 0.0 0.5; 0.5 0.5 0.0],
                          [[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]]);

julia> zb_sites = Sites([Site([0.0,  0.0,  0.0 ], [0, 1]),    # cations
                          Site([0.25, 0.25, 0.25], [2, 3])]); # anions

Heterogeneous sublattices require a concentration — Enumlib refuses unrestricted enumeration on them (see Enumerate on a multilattice parent). "No constraint" is spelled as a ConcentrationRange that leaves every species free:

julia> free4 = ConcentrationRange([(0//1, 1//1) for _ in 1:4]);

julia> [count_inequivalent(zb, zb_sites; supercells = VolumeRange(n:n),
                            concentration = free4) for n in 1:4]
4-element Vector{BigInt}:
   4
  11
  52
 290

Those are the numbers enumerate produces, not an upper bound on them:

julia> length(enumerate(zb, zb_sites; supercells = VolumeRange(3:3), concentration = free4))
52

Sizing a run that spans several volumes is one call:

julia> count_inequivalent(zb, zb_sites; supercells = VolumeRange(1:4), concentration = free4)
357
Per-site allowed labels change the count

Treating each position as free over all four labels would give 16, 204, 2960, and 64196 at these volumes instead of 4, 11, 52, and 290. estimate_cost prices a run with the same count, so the restriction is what keeps the resource check from refusing a request that is actually small. See Pólya counting for the math.

Unconstrained ranges and by_concentration

A ConcentrationRange covering [0, 1] for every species constrains nothing, so count_inequivalent collapses it to a single Burnside evaluation instead of walking every composition. The total is exact either way — every coloring has exactly one concentration — but the per-concentration breakdown is not materialized:

julia> free2 = ConcentrationRange([(0//1, 1//1), (0//1, 1//1)]);

julia> ic_free = count_inequivalent(p, sites; supercells = VolumeRange(4:4),
                                     concentration = free2, breakdown = true);

julia> ic_free.total
19

julia> ic_free.by_concentration       # empty: the walk that would fill it is what we skipped
Tuple{Concentration, BigInt}[]

Pass a narrower range when you want that slice. Here the same 19 structures come back split three ways:

julia> ic_band = count_inequivalent(p, sites; supercells = VolumeRange(4:4),
                                     concentration = ConcentrationRange([(1//4, 3//4), (1//4, 3//4)]),
                                     breakdown = true);

julia> ic_band.by_concentration
3-element Vector{Tuple{Concentration, BigInt}}:
 (Concentration(1//4, 3//4), 7)
 (Concentration(1//2, 1//2), 5)
 (Concentration(3//4, 1//4), 7)

total, by_volume, and by_hnf are unaffected by the collapse. The shortcut is internal to count_inequivalent; enumerate still decomposes an unconstrained range into partitions, because it has to produce the structures — see Sweep concentration ranges for the partition gate that governs it.

Super-periodic policy

By default, count_inequivalent reports the primitive (aperiodic) count — orbits whose stabilizer in the supercell's translation group is trivial. Pass include_superperiodic = true to get the raw Burnside count, which includes orbits that are actually derivatives of smaller supercells:

julia> count_inequivalent(p, sites; supercells = VolumeRange(4:4))                          # primitive only
19

julia> count_inequivalent(p, sites; supercells = VolumeRange(4:4), include_superperiodic = true)  # all orbits
41

The 22 extra structures at n=4 are the super-periodic orbits — labelings whose true period divides 4. See Handle super-periodicity for when each branch is right.

See also