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 →