Entropy Visualizer — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# Entropy Visualizer — pure byte-level randomness analysis. No DOM, no deps.
#
# Language: Ruby (3.1+, standard library only)
# Source: CosmoDev polyglot showcase port of the Entropy Visualizer tool,
# ported from src/lib/entropy-visualizer.ts (the canonical
# TypeScript implementation).
# License: display source — part of CosmoDev's polyglot tool pages.
#
# Everything is deterministic: the same bytes always produce the same
# numbers. Input is a binary String (or anything whose #bytes are analyzed
# via each_byte).
module EntropyVisualizer
EntropyAnalysis = Struct.new(
:shannon_entropy, :chi_squared, :chi_squared_p_value,
:serial_correlation, :monte_carlo_pi, :byte_frequencies,
:block_entropies, :verdict, keyword_init: true
)
# Bytes per heatmap block.
BLOCK_SIZE = 16
# Degrees of freedom for the byte-frequency chi-squared test
# (256 bins - 1 constraint).
CHI_SQUARED_DF = 255
class << self
# Count occurrences of each byte value 0-255.
def count_bytes(data)
freq = Array.new(256, 0)
data.each_byte { |b| freq[b] += 1 }
freq
end
# Shannon entropy H = -sum 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+.
def shannon_bits_per_byte(frequencies, total)
return 0 if total <= 0
h = 0.0
frequencies.each do |count|
next if count.zero?
p = count.to_f / total
h -= p * Math.log2(p)
end
h
end
# Pearson chi-squared comparing observed byte counts against a uniform
# expectation E = total/256 per bin.
def chi_squared_statistic(frequencies, total)
raise ArgumentError, 'chi-squared needs a positive sample size' if total <= 0
expected = total / 256.0
chi2 = 0.0
frequencies.each do |observed|
diff = observed - expected
chi2 += (diff * diff) / expected
end
chi2
end
# Regularized upper incomplete gamma function Q(a, x) = G(a,x)/G(a), via
# the power series (x < a+1) or the Lentz continued fraction (otherwise).
# Numerical Recipes §6.2.
def gamma_q(a, x)
return Float::NAN if a <= 0 || x < 0
return 1.0 if x.zero?
if x < a + 1
# Series for P(a,x); Q = 1 - P
ap = a.to_f
sum = 1.0 / a
del = sum
1000.times do
ap += 1
del *= x / ap
sum += del
break if del.abs < sum.abs * 1e-15
end
(1 - sum * Math.exp(-x + a * Math.log(x) - ln_gamma(a))).clamp(0.0, 1.0)
else
# Continued fraction for Q(a,x)
fpmin = 1e-300
b = x + 1 - a
c = 1.0 / fpmin
d = 1.0 / b
h = d
(1..1000).each do |i|
an = -i * (i - a)
b += 2
d = an * d + b
d = fpmin if d.abs < fpmin
c = b + an / c
c = fpmin if c.abs < fpmin
d = 1.0 / d
del = d * c
h *= del
break if (del - 1).abs < 1e-15
end
(Math.exp(-x + a * Math.log(x) - ln_gamma(a)) * h).clamp(0.0, 1.0)
end
end
# Upper-tail p-value for a chi-squared statistic with +df+ degrees of
# freedom.
def chi_squared_p(chi2, df = CHI_SQUARED_DF)
return Float::NAN if chi2 < 0 || df <= 0
gamma_q(df / 2.0, chi2 / 2.0)
end
# Circular serial correlation between consecutive bytes (the `ent`
# tool's metric): scc = (sum(xy) - (sum(x))^2/n) / (sum(x^2) -
# (sum(x))^2/n) over the pair sequence (x0,x1), (x1,x2), ...,
# (x(n-1),x0). 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.
def serial_correlation_coefficient(data)
bytes = data.bytes
n = bytes.length
return 0.0 if n < 2
sum = 0
sum_sq = 0
sum_xy = 0
bytes.each_with_index do |x, i|
y = bytes[(i + 1) % n]
sum += x
sum_sq += x * x
sum_xy += x * y
end
mean_sq = (sum * sum).to_f / n
denom = sum_sq - mean_sq
return 0.0 if denom.zero?
(sum_xy - mean_sq) / denom
end
# Monte Carlo pi estimate: consecutive byte pairs are (x, y) points in a
# 256x256 square; the fraction inside the inscribed circle (center
# 127.5, radius 128) times 4 estimates pi. Inputs with fewer than 2
# bytes -> 0.
def monte_carlo_pi_estimate(data)
bytes = data.bytes
pairs = bytes.length / 2
return 0.0 if pairs.zero?
inside = 0
pairs.times do |i|
dx = bytes[2 * i] - 127.5
dy = bytes[2 * i + 1] - 127.5
inside += 1 if dx * dx + dy * dy <= 128 * 128
end
(4.0 * inside) / pairs
end
# Shannon entropy (bits/byte) of each consecutive +block_size+-byte
# block.
def block_entropies(data, block_size = BLOCK_SIZE)
raise ArgumentError, 'blockSize must be at least 1' if block_size < 1
blocks = []
data.bytes.each_slice(block_size) do |chunk|
counts = Array.new(256, 0)
chunk.each { |b| counts[b] += 1 }
blocks << shannon_bits_per_byte(counts, chunk.length)
end
blocks
end
# Map a Shannon entropy (bits/byte) to the verdict scale.
def verdict_from_shannon(bits_per_byte)
return 'excellent' if bits_per_byte > 7.5
return 'good' if bits_per_byte > 6.0
return 'suspicious' if bits_per_byte > 4.0
'low'
end
# Full analysis of a byte sequence. Raises on empty input — there is
# nothing to measure and every metric would be undefined.
def analyze_entropy(data)
if data.bytesize.zero?
raise ArgumentError, 'Nothing to analyze - provide at least 1 byte of data.'
end
total = data.bytesize
freq = count_bytes(data)
shannon = shannon_bits_per_byte(freq, total)
chi2 = chi_squared_statistic(freq, total)
EntropyAnalysis.new(
shannon_entropy: shannon,
chi_squared: chi2,
chi_squared_p_value: chi_squared_p(chi2),
serial_correlation: serial_correlation_coefficient(data),
monte_carlo_pi: monte_carlo_pi_estimate(data),
byte_frequencies: freq,
block_entropies: block_entropies(data),
verdict: verdict_from_shannon(shannon)
)
end
private
# Lanczos approximation (g=7, 9 coefficients) of ln Gamma(x).
def ln_gamma(x)
g = [
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: Gamma(x)*Gamma(1-x) = pi / sin(pi*x)
return Math.log(Math::PI / Math.sin(Math::PI * x)) - ln_gamma(1 - x)
end
x -= 1
a = g[0]
t = x + 7.5
(1..8).each { |i| a += g[i] / (x + i) }
0.5 * Math.log(2 * Math::PI) + (x + 0.5) * Math.log(t) - t + Math.log(a)
end
end
end
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 →