Concentrations

Public API for the concentration constructors, ConcentrationRange, and the multinomial-hash primitives.

Enumlib.ConcentrationType
Concentration(fractions::AbstractVector{<:Rational})

A single concentration: per-species fractions summing to 1, stored as Rational{Int} for exact arithmetic.

Three named constructors — pick the one that matches what you're holding in your head:

  • Concentration([1//4, 3//4]) — canonical: explicit fractions, fully specified.
  • concentration_ratio([1, 3])scale-free integer ratio: 1:3 → [1//4, 3//4]. Non-coprime inputs work too — [2, 6] or [3, 9] all produce the same Concentration(1//4, 3//4) after normalization. Use when you care about the proportion, not the cell size.
  • concentration_count([3, 9]; n_total = 12)anchored literal counts: 3 of A and 9 of B in a 12-cell[1//4, 3//4]. Validates sum(counts) == n_total. Use when you've committed to a specific supercell size.

A fourth constructor — Concentration(sites, per_sublattice) — is documented separately below; it's the natural form for Regime C (heterogeneous sublattices).

source
Enumlib.concentration_ratioFunction
concentration_ratio(integers::AbstractVector{<:Integer}) -> Concentration

Convenience constructor: treats the integer vector as a ratio. Each Concentration fraction is integers[i] // sum(integers).

Examples

julia> concentration_ratio([2, 4])   # 2:4 reduces to 1:2 → fractions in lowest terms
Concentration(1//3, 2//3)

julia> concentration_ratio([1, 1, 2])  # ternary 1:1:2
Concentration(1//4, 1//4, 1//2)
source
Enumlib.concentration_countFunction
concentration_count(counts::AbstractVector{<:Integer}; n_total::Integer) -> Concentration

Literal-counts constructor: interprets the integer vector as exact counts in a cell of size n_total. Validates that sum(counts) == n_total; mismatched n_total throws.

Examples

julia> concentration_count([3, 9]; n_total = 12)   # 3 A + 9 B atoms in a 12-cell
Concentration(1//4, 3//4)

julia> concentration_count([15, 17]; n_total = 32)
Concentration(15//32, 17//32)
source
Enumlib.ConcentrationRangeType
ConcentrationRange(bounds::AbstractVector{Tuple{Rational, Rational}})

Per-species (min, max) bounds. Decomposes at each cell size into a list of Concentrations within the bounds via concentrations_in_range.

Examples

"All binary structures with composition in the 40–60% range" — each species in [2//5, 3//5]. At a 10-site cell that's 3 partitions:

julia> cr = ConcentrationRange([(2//5, 3//5), (2//5, 3//5)]);

julia> concentrations_in_range(cr, 10)
3-element Vector{Concentration}:
 Concentration(2//5, 3//5)
 Concentration(1//2, 1//2)
 Concentration(3//5, 2//5)

"Every ternary stoichiometry where C is at most 1/3" — first two species unconstrained, third species in [0, 1//3]. At a 6-site cell that's 18 partitions:

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

julia> length(concentrations_in_range(cr, 6))
18
source
Enumlib.n_speciesFunction
n_species(c::Concentration)

Number of species in the concentration vector.

source
n_species(cr::ConcentrationRange) -> Int

Number of species (length of the per-species (min, max) bounds vector).

source
Enumlib.multiplicitiesFunction
multiplicities(c::Concentration, n_total::Integer) -> Vector{Int}

Resolve a concentration to integer per-species counts at a specific cell size. Throws EmptyEnumerationError if the fractions don't divide cleanly into n_total (e.g., Concentration([1//3, 2//3]) at n_total = 4).

Examples

julia> c = concentration_count([4, 4]; n_total = 8);

julia> multiplicities(c, 16)
2-element Vector{Int64}:
 8
 8
source
Enumlib.concentrations_in_rangeFunction
concentrations_in_range(cr::ConcentrationRange, n_total::Integer) -> Vector{Concentration}

Enumerate every integer-multiplicity vector (a_1, ..., a_k) with sum(a_i) = n_total and lo_i ≤ a_i / n_total ≤ hi_i, returning the corresponding Concentrations. Used by enumerate(...) to walk a concentration range.

The default partition_threshold = 100 checks the result of this function; if the count exceeds the threshold and on_partition_overflow = :error, enumerate(...) throws a PartitionExplosionError with a "narrow your range" message.

Examples

Asymmetric binary range — A in [25%, 50%], B in [50%, 75%] — at a supercell size of 8 yields 3 partitions:

julia> cr = ConcentrationRange([(1//4, 1//2), (1//2, 3//4)]);

julia> concentrations_in_range(cr, 8)
3-element Vector{Concentration}:
 Concentration(1//4, 3//4)
 Concentration(3//8, 5//8)
 Concentration(1//2, 1//2)
source
Enumlib.multinomial_countFunction
multinomial_count(multiplicities::AbstractVector{<:Integer}) -> BigInt

Compute the multinomial coefficient multinomial(n; a_1, ..., a_k) exactly using the iterative-binomial identity:

multinomial(a_1, ..., a_k) = ∏_{i=1..k} binomial(a_1 + ... + a_i, a_i)

Mathematically equivalent to n! / ∏ a_i!, but evaluates one binomial at a time so factors cancel as we go — no wasteful n! intermediate. Returns BigInt so the result is exact for arbitrary input (binary at n=70 already overflows Int64); the BigInt cost is ~µs per call, negligible since multinomial_count runs once per (supercell, concentration) pair during the enumeration resource check, never in an inner loop.

See docs/notes/chunk6-review.md for the full tradeoff analysis (why not Combinatorics.multinomial + try/catch, why not the factorial form, inner-loop overflow safety proof).

Examples

julia> multinomial_count([2, 2])   # binary, 2 of each in 4 positions: C(4,2) = 6
6

julia> multinomial_count([3, 5])   # binary 3:5 in n=8: C(8,3) = 56
56

julia> multinomial_count([2, 2, 2])   # ternary 2:2:2 in n=6: 6!/(2! 2! 2!) = 90
90
source
Enumlib.multinomial_hashFunction
multinomial_hash(coloring::AbstractVector{<:Integer},
                 multiplicities::AbstractVector{<:Integer}) -> Int

Hash a coloring — a length-n vector with values in 0:k-1 and per-color counts matching multiplicities — to its rank in [0, C-1] where C = multinomial(n; multiplicities). Implements the mixed-radix scheme from HF 2012 §3.1.

For each color i = 1..k-1 (processed in order), this:

  1. Identifies the positions where color i-1 sits among the still-unfilled slots.
  2. Computes the colex rank x_i of those positions within the unfilled slots.
  3. Accumulates y += x_i * P where P is the running product of C_j for j < i.

Color k-1 (the last) is determined by what's left, so we don't include it in the hash.

Assumes the coloring satisfies the multiplicity constraint. Behavior is undefined otherwise (caller's responsibility — getUniqueColorings_multinomial only generates valid colorings).

Examples

The 6 colorings of [2, 2] (binary 2:2 in n=4) each get a distinct rank in [0, 5]:

julia> multinomial_hash(Int8[0, 0, 1, 1], [2, 2])
0

julia> multinomial_hash(Int8[0, 1, 0, 1], [2, 2])
1

julia> multinomial_hash(Int8[1, 1, 0, 0], [2, 2])
5
source
Enumlib.multinomial_unhashFunction
multinomial_unhash(idx::Integer, multiplicities::AbstractVector{<:Integer}) -> Vector{Int8}

Inverse of multinomial_hash: given an index idx ∈ [0, C-1], recover the unique coloring with the given multiplicities.

For each color i = 0..k-2 in order, extracts x_i = (idx_remaining ÷ P_so_far) mod C_i, then maps x_i back to a set of a_i positions (within the still-unfilled slots) via inverse-colex. Color k-1 fills whatever's left.

Examples

Round-trip with multinomial_hash — every valid coloring maps to a unique rank and back:

julia> c = Int8[1, 0, 1, 0]; mults = [2, 2];

julia> idx = multinomial_hash(c, mults)
4

julia> multinomial_unhash(idx, mults) == c
true

julia> multinomial_unhash(0, [2, 2])   # the lex-smallest coloring at this multiplicity
4-element Vector{Int8}:
 0
 0
 1
 1
source