Concentrations
Public API for the concentration constructors, ConcentrationRange, and the multinomial-hash primitives.
Enumlib.Concentration — Type
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 sameConcentration(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]. Validatessum(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).
Enumlib.concentration_ratio — Function
concentration_ratio(integers::AbstractVector{<:Integer}) -> ConcentrationConvenience 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)Enumlib.concentration_count — Function
concentration_count(counts::AbstractVector{<:Integer}; n_total::Integer) -> ConcentrationLiteral-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)Enumlib.ConcentrationRange — Type
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))
18Enumlib.n_species — Function
n_species(c::Concentration)Number of species in the concentration vector.
n_species(cr::ConcentrationRange) -> IntNumber of species (length of the per-species (min, max) bounds vector).
Enumlib.multiplicities — Function
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
8Enumlib.concentrations_in_range — Function
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)Enumlib.multinomial_count — Function
multinomial_count(multiplicities::AbstractVector{<:Integer}) -> BigIntCompute 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
90Enumlib.multinomial_hash — Function
multinomial_hash(coloring::AbstractVector{<:Integer},
multiplicities::AbstractVector{<:Integer}) -> IntHash 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:
- Identifies the positions where color
i-1sits among the still-unfilled slots. - Computes the colex rank
x_iof those positions within the unfilled slots. - Accumulates
y += x_i * PwherePis the running product ofC_jforj < 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])
5Enumlib.multinomial_unhash — Function
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