Skip to content

Entropy Visualizer — Swift source

Visualize the randomness quality of any data. See Shannon entropy, byte frequency distribution, chi-squared score, and a visual entropy heatmap.

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// entropy-visualizer — pure byte-level randomness analysis.
//
// Language: Swift 5.9+ (Foundation)
// Ported from src/lib/entropy-visualizer.ts
// display source — part of CosmoDev's polyglot tool pages
//
// Everything is deterministic: the same bytes always produce the same numbers.
// Shannon entropy, Pearson χ² with its upper-tail p-value (Lanczos ln Γ +
// Numerical Recipes gammaQ), the `ent` tool's circular serial correlation,
// and a Monte Carlo π estimate — all in Double arithmetic, matching the TS
// reference operation for operation.

import Foundation

// MARK: - Types

enum EntropyVerdict: String {
    case excellent
    case good
    case suspicious
    case low
}

/// The full analysis result (EntropyAnalysis in the TS reference).
struct EntropyAnalysis {
    /// Shannon entropy in bits per byte (0 = one repeating byte, 8 = perfectly uniform).
    let shannonEntropy: Double
    /// χ² statistic against the uniform 256-bin expectation.
    let chiSquared: Double
    /// Upper-tail p-value for χ² with 255 degrees of freedom (1 = perfectly plausible).
    let chiSquaredPValue: Double
    /// Circular serial correlation between consecutive bytes (-1 … +1, 0 = uncorrelated).
    let serialCorrelation: Double
    /// Monte Carlo π estimate from consecutive byte pairs (≈3.14159 for random data).
    let monteCarloPi: Double
    /// Occurrence count per byte value 0-255 (always 256 entries).
    let byteFrequencies: [Int]
    /// Shannon entropy (bits/byte) of each BLOCK_SIZE-byte block, for the heatmap.
    let blockEntropies: [Double]
    let verdict: EntropyVerdict
}

/// Bytes per heatmap block.
let BLOCK_SIZE = 16

/// Degrees of freedom for the byte-frequency χ² test (256 bins - 1 constraint).
let CHI_SQUARED_DF = 255

// MARK: - Histogram + entropy

/// Count occurrences of each byte value 0-255.
func countBytes(_ data: [UInt8]) -> [Int] {
    var freq = [Int](repeating: 0, count: 256)
    for b in data { freq[Int(b)] += 1 }
    return freq
}

/// Shannon entropy H = -Σ p(x)·log2(p(x)) in bits per byte, computed from a
/// frequency histogram. Zero-count bins contribute nothing. `total` must be
/// the sum of `frequencies`.
func shannonBitsPerByte(_ frequencies: [Int], _ total: Int) -> Double {
    if total <= 0 { return 0 }
    var h = 0.0
    for count in frequencies where count != 0 {
        let p = Double(count) / Double(total)
        h -= p * log2(p)
    }
    return h
}

/// Pearson χ² comparing observed byte counts against a uniform expectation
/// E = total/256 per bin.
func chiSquaredStatistic(_ frequencies: [Int], _ total: Int) throws -> Double {
    if total <= 0 { throw NSError(domain: "entropy", code: 1,
                                  userInfo: [NSLocalizedDescriptionKey: "chi-squared needs a positive sample size"]) }
    let expected = Double(total) / 256
    var chi2 = 0.0
    for observed in frequencies {
        let diff = Double(observed) - expected
        chi2 += diff * diff / expected
    }
    return chi2
}

// MARK: - χ² p-value

/// Lanczos approximation (g=7, 9 coefficients) of ln Γ(x).
func lnGamma(_ x: Double) -> Double {
    let g: [Double] = [
        0.99999999999980993, 676.5203681218851, -1259.1392167224028,
        771.32342877765313, -176.61502916214059, 12.507343278686905,
        -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7,
    ]
    if x < 0.5 {
        // Reflection formula: Γ(x)·Γ(1-x) = π / sin(πx)
        return log(.pi / sin(.pi * x)) - lnGamma(1 - x)
    }
    let x = x - 1
    var a = g[0]
    let t = x + 7.5
    for i in 1..<9 { a += g[i] / (x + Double(i)) }
    return 0.5 * log(2 * .pi) + (x + 0.5) * log(t) - t + log(a)
}

/// Regularized upper incomplete gamma function Q(a, x) = Γ(a,x)/Γ(a), via the
/// power series (x < a+1) or the Lentz continued fraction (otherwise).
/// Numerical Recipes §6.2. (NaN inputs mirror the TS reference: non-finite.)
func gammaQ(_ a: Double, _ x: Double) -> Double {
    if a <= 0 || x.isNaN || a.isNaN { return Double.nan }
    if x < 0 { return Double.nan }
    if x == 0 { return 1 }
    if x < a + 1 {
        // Series for P(a,x); Q = 1 - P
        var ap = a
        var sum = 1 / a
        var del = sum
        for _ in 0..<1000 {
            ap += 1
            del *= x / ap
            sum += del
            if abs(del) < abs(sum) * 1e-15 { break }
        }
        return min(1, max(0, 1 - sum * exp(-x + a * log(x) - lnGamma(a))))
    }
    // Continued fraction for Q(a,x)
    let FPMIN = 1e-300
    var b = x + 1 - a
    var c = 1 / FPMIN
    var d = 1 / b
    var h = d
    for i in 1...1000 {
        let an = -Double(i) * (Double(i) - a)
        b += 2
        d = an * d + b
        if abs(d) < FPMIN { d = FPMIN }
        c = b + an / c
        if abs(c) < FPMIN { c = FPMIN }
        d = 1 / d
        let del = d * c
        h *= del
        if abs(del - 1) < 1e-15 { break }
    }
    return min(1, max(0, exp(-x + a * log(x) - lnGamma(a)) * h))
}

/// Upper-tail p-value for a χ² statistic with `df` degrees of freedom.
func chiSquaredP(_ chi2: Double, df: Int = CHI_SQUARED_DF) -> Double {
    if chi2 < 0 || df <= 0 || chi2.isNaN { return Double.nan }
    return gammaQ(Double(df) / 2, chi2 / 2)
}

// MARK: - Correlation + Monte Carlo

/// Circular serial correlation between consecutive bytes (the `ent` tool's
/// metric): scc = (Σxy - (Σx)²/n) / (Σx² - (Σx)²/n) over the pair sequence
/// (x₀,x₁), (x₁,x₂), …, (xₙ₋₁,x₀). 0 = uncorrelated, ±1 = perfectly
/// (anti)correlated. Constant input has a zero denominator → reported as 0
/// (nothing to correlate); inputs shorter than 2 bytes are also 0.
func serialCorrelationCoefficient(_ data: [UInt8]) -> Double {
    let n = data.count
    if n < 2 { return 0 }
    var sum = 0.0
    var sumSq = 0.0
    var sumXY = 0.0
    for i in 0..<n {
        let x = Double(data[i])
        let y = Double(data[(i + 1) % n])
        sum += x
        sumSq += x * x
        sumXY += x * y
    }
    let meanSq = sum * sum / Double(n)
    let denom = sumSq - meanSq
    if denom == 0 { return 0 }
    return (sumXY - meanSq) / denom
}

/// Monte Carlo π estimate: consecutive byte pairs are (x, y) points in a
/// 256×256 square; the fraction inside the inscribed circle (center 127.5,
/// radius 128) times 4 estimates π. Inputs with fewer than 2 bytes → 0.
func monteCarloPiEstimate(_ data: [UInt8]) -> Double {
    let pairs = data.count / 2
    if pairs == 0 { return 0 }
    var inside = 0
    for i in 0..<pairs {
        let dx = Double(data[2 * i]) - 127.5
        let dy = Double(data[2 * i + 1]) - 127.5
        if dx * dx + dy * dy <= 128 * 128 { inside += 1 }
    }
    return Double(4 * inside) / Double(pairs)
}

/// Shannon entropy (bits/byte) of each consecutive `blockSize`-byte block.
func blockEntropies(_ data: [UInt8], blockSize: Int = BLOCK_SIZE) throws -> [Double] {
    if blockSize < 1 { throw NSError(domain: "entropy", code: 2,
                                     userInfo: [NSLocalizedDescriptionKey: "blockSize must be at least 1"]) }
    var blocks: [Double] = []
    var off = 0
    while off < data.count {
        var counts = [Int](repeating: 0, count: 256)
        var n = 0
        let end = min(off + blockSize, data.count)
        for i in off..<end {
            counts[Int(data[i])] += 1
            n += 1
        }
        blocks.append(shannonBitsPerByte(counts, n))
        off += blockSize
    }
    return blocks
}

/// Map a Shannon entropy (bits/byte) to the verdict scale.
func verdictFromShannon(_ bitsPerByte: Double) -> EntropyVerdict {
    if bitsPerByte > 7.5 { return .excellent }
    if bitsPerByte > 6.0 { return .good }
    if bitsPerByte > 4.0 { return .suspicious }
    return .low
}

// MARK: - Analysis

/// Full analysis of a byte sequence. Throws on empty input — there is nothing
/// to measure and every metric would be undefined.
func analyzeEntropy(_ data: [UInt8]) throws -> EntropyAnalysis {
    if data.isEmpty {
        throw NSError(domain: "entropy", code: 3,
                      userInfo: [NSLocalizedDescriptionKey: "Nothing to analyze - provide at least 1 byte of data."])
    }
    let total = data.count
    let freq = countBytes(data)
    let shannon = shannonBitsPerByte(freq, total)
    let chi2 = try chiSquaredStatistic(freq, total)
    return EntropyAnalysis(
        shannonEntropy: shannon,
        chiSquared: chi2,
        chiSquaredPValue: chiSquaredP(chi2),
        serialCorrelation: serialCorrelationCoefficient(data),
        monteCarloPi: monteCarloPiEstimate(data),
        byteFrequencies: freq,
        blockEntropies: try blockEntropies(data),
        verdict: verdictFromShannon(shannon))
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →