Skip to content

Entropy Visualizer — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Entropy Visualizer — pure byte-level randomness analysis.
//
// Language: Kotlin 1.9+ (JVM), standard library only.
// Ported from src/lib/entropy-visualizer.ts — display source, part of
// CosmoDev's polyglot tool pages. Functionally equivalent to the TS
// reference (this file is the contract the Go CLI twin mirrors): the same
// statistics over the same byte frequencies, including the Lanczos lnΓ and
// the Numerical Recipes regularized gamma Q, ported term for term.
//
// Everything is deterministic: the same bytes always produce the same numbers.

import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.sin

enum class EntropyVerdict { EXCELLENT, GOOD, SUSPICIOUS, LOW }

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

/** Bytes per heatmap block. */
const val BLOCK_SIZE = 16

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

/** Count occurrences of each byte value 0-255. */
fun countBytes(data: ByteArray): List<Int> {
    val freq = IntArray(256)
    for (b in data) freq[b.toInt() and 0xFF]++
    return freq.toList()
}

/**
 * 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`.
 */
fun shannonBitsPerByte(frequencies: List<Int>, total: Long): Double {
    if (total <= 0) return 0.0
    val ln2 = ln(2.0)
    var h = 0.0
    for (count in frequencies) {
        if (count == 0) continue
        val p = count / total.toDouble()
        h -= p * (ln(p) / ln2)
    }
    return h
}

/**
 * Pearson χ² comparing observed byte counts against a uniform expectation
 * E = total/256 per bin.
 */
fun chiSquaredStatistic(frequencies: List<Int>, total: Long): Double {
    require(total > 0) { "chi-squared needs a positive sample size" }
    val expected = total / 256.0
    var chi2 = 0.0
    for (observed in frequencies) {
        val diff = observed - expected
        chi2 += diff * diff / expected
    }
    return chi2
}

/** Lanczos approximation (g=7, 9 coefficients) of ln Γ(x). */
private fun lnGamma(x: Double): Double {
    val g = doubleArrayOf(
        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 ln(PI / sin(PI * x)) - lnGamma(1 - x)
    }
    var xx = x - 1
    var a = g[0]
    val t = xx + 7.5
    for (i in 1 until 9) a += g[i] / (xx + i)
    return 0.5 * ln(2 * PI) + (xx + 0.5) * ln(t) - t + ln(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.
 */
fun gammaQ(a: Double, x: Double): Double {
    if (a <= 0 || x < 0) return Double.NaN
    if (x == 0.0) return 1.0
    if (x < a + 1) {
        // Series for P(a,x); Q = 1 - P
        var ap = a
        var sum = 1 / a
        var del = sum
        var n = 0
        while (n < 1000) {
            ap += 1
            del *= x / ap
            sum += del
            if (abs(del) < abs(sum) * 1e-15) break
            n++
        }
        return (1 - sum * exp(-x + a * ln(x) - lnGamma(a))).coerceIn(0.0, 1.0)
    }
    // Continued fraction for Q(a,x)
    val fpmin = 1e-300
    var b = x + 1 - a
    var c = 1 / fpmin
    var d = 1 / b
    var h = d
    for (i in 1..1000) {
        val an = -i * (i - a).toDouble()
        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
        val del = d * c
        h *= del
        if (abs(del - 1) < 1e-15) break
    }
    return (exp(-x + a * ln(x) - lnGamma(a)) * h).coerceIn(0.0, 1.0)
}

/** Upper-tail p-value for a χ² statistic with `df` degrees of freedom. */
fun chiSquaredP(chi2: Double, df: Int = CHI_SQUARED_DF): Double {
    if (chi2 < 0 || df <= 0) return Double.NaN
    return gammaQ(df / 2.0, chi2 / 2.0)
}

/**
 * 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.
 */
fun serialCorrelationCoefficient(data: ByteArray): Double {
    val n = data.size
    if (n < 2) return 0.0
    var sum = 0.0
    var sumSq = 0.0
    var sumXY = 0.0
    for (i in 0 until n) {
        val x = (data[i].toInt() and 0xFF).toDouble()
        val y = (data[(i + 1) % n].toInt() and 0xFF).toDouble()
        sum += x
        sumSq += x * x
        sumXY += x * y
    }
    val meanSq = sum * sum / n
    val denom = sumSq - meanSq
    if (denom == 0.0) return 0.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.
 */
fun monteCarloPiEstimate(data: ByteArray): Double {
    val pairs = data.size / 2
    if (pairs == 0) return 0.0
    var inside = 0
    for (i in 0 until pairs) {
        val dx = (data[2 * i].toInt() and 0xFF) - 127.5
        val dy = (data[2 * i + 1].toInt() and 0xFF) - 127.5
        if (dx * dx + dy * dy <= 128.0 * 128.0) inside++
    }
    return 4.0 * inside / pairs
}

/** Shannon entropy (bits/byte) of each consecutive `blockSize`-byte block. */
fun blockEntropies(data: ByteArray, blockSize: Int = BLOCK_SIZE): List<Double> {
    require(blockSize >= 1) { "blockSize must be at least 1" }
    val blocks = mutableListOf<Double>()
    var off = 0
    while (off < data.size) {
        val counts = IntArray(256)
        var n = 0
        val end = minOf(off + blockSize, data.size)
        for (i in off until end) {
            counts[data[i].toInt() and 0xFF]++
            n++
        }
        blocks.add(shannonBitsPerByte(counts.toList(), n.toLong()))
        off += blockSize
    }
    return blocks
}

/** Map a Shannon entropy (bits/byte) to the verdict scale. */
fun verdictFromShannon(bitsPerByte: Double): EntropyVerdict = when {
    bitsPerByte > 7.5 -> EntropyVerdict.EXCELLENT
    bitsPerByte > 6.0 -> EntropyVerdict.GOOD
    bitsPerByte > 4.0 -> EntropyVerdict.SUSPICIOUS
    else -> EntropyVerdict.LOW
}

/**
 * Full analysis of a byte sequence. Throws on empty input — there is nothing
 * to measure and every metric would be undefined.
 */
fun analyzeEntropy(data: ByteArray): EntropyAnalysis {
    require(data.isNotEmpty()) { "Nothing to analyze - provide at least 1 byte of data." }
    val total = data.size.toLong()
    val freq = countBytes(data)
    val shannon = shannonBitsPerByte(freq, total)
    val chi2 = chiSquaredStatistic(freq, total)
    return EntropyAnalysis(
        shannonEntropy = shannon,
        chiSquared = chi2,
        chiSquaredPValue = chiSquaredP(chi2),
        serialCorrelation = serialCorrelationCoefficient(data),
        monteCarloPi = monteCarloPiEstimate(data),
        byteFrequencies = freq,
        blockEntropies = 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 →