Skip to content

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

/*
 * entropy-visualizer — byte-level randomness analysis: Shannon entropy, a χ²
 *                      uniformity test, serial correlation and a Monte Carlo π
 *                      estimate.
 *
 * Language: C (C11, standard library only — <math.h> covers everything here)
 * 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.
 * The statistics are the classic `ent`-style battery — high Shannon entropy
 * alone does not prove randomness, which is why the χ² p-value, the serial
 * correlation and the π estimate are reported alongside it. Compressed data
 * scores ~7.99 bits/byte yet fails χ² badly.
 *
 * Build: cc -std=c11 entropy-visualizer.c -lm
 */

#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* --------------------------------------------------------------- constants --- */

/* Bytes per heatmap block. */
#define BLOCK_SIZE 16

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

typedef enum {
    VERDICT_EXCELLENT,
    VERDICT_GOOD,
    VERDICT_SUSPICIOUS,
    VERDICT_LOW
} entropy_verdict;

const char *verdict_name(entropy_verdict v)
{
    switch (v) {
        case VERDICT_EXCELLENT:  return "excellent";
        case VERDICT_GOOD:       return "good";
        case VERDICT_SUSPICIOUS: return "suspicious";
        default:                 return "low";
    }
}

typedef struct {
    /* Shannon entropy in bits per byte (0 = one repeating byte, 8 = uniform). */
    double shannon_entropy;
    /* χ² statistic against the uniform 256-bin expectation. */
    double chi_squared;
    /* Upper-tail p-value for χ² with 255 df (1 = perfectly plausible). */
    double chi_squared_p_value;
    /* Circular serial correlation between consecutive bytes (-1 … +1). */
    double serial_correlation;
    /* Monte Carlo π estimate from consecutive byte pairs (≈3.14159 if random). */
    double monte_carlo_pi;
    /* Occurrence count per byte value 0-255. */
    long byte_frequencies[256];
    /* Shannon entropy of each BLOCK_SIZE-byte block, for the heatmap. */
    double *block_entropies;
    size_t  block_count;
    entropy_verdict verdict;
} entropy_analysis;

/* -------------------------------------------------------------- histograms --- */

/* Count occurrences of each byte value 0-255. */
void count_bytes(const uint8_t *data, size_t len, long freq[256])
{
    memset(freq, 0, 256 * sizeof freq[0]);
    for (size_t i = 0; i < len; i++) {
        freq[data[i]]++;
    }
}

/*
 * Shannon entropy H = -Σ p(x)·log2(p(x)) in bits per byte, from a frequency
 * histogram. Zero-count bins contribute nothing. `total` must be the sum of
 * `frequencies`.
 */
double shannon_bits_per_byte(const long *frequencies, size_t bins, double total)
{
    double h = 0.0;

    if (total <= 0.0) {
        return 0.0;
    }
    for (size_t i = 0; i < bins; i++) {
        double p;

        if (frequencies[i] == 0) {
            continue;
        }
        p = (double) frequencies[i] / total;
        h -= p * log2(p);
    }
    return h;
}

/*
 * Pearson χ² comparing observed byte counts against a uniform expectation
 * E = total/256 per bin. Returns NAN for a non-positive sample size (the TS
 * reference throws; a C caller checks with isnan()).
 */
double chi_squared_statistic(const long *frequencies, double total)
{
    double expected, chi2 = 0.0;

    if (total <= 0.0) {
        return NAN; /* chi-squared needs a positive sample size */
    }
    expected = total / 256.0;
    for (size_t i = 0; i < 256; i++) {
        double diff = (double) frequencies[i] - expected;
        chi2 += (diff * diff) / expected;
    }
    return chi2;
}

/* --------------------------------------------------------- gamma functions --- */

/* Lanczos approximation (g=7, 9 coefficients) of ln Γ(x). */
static double ln_gamma(double x)
{
    static const double g[9] = {
        0.99999999999980993,  676.5203681218851,   -1259.1392167224028,
        771.32342877765313,  -176.61502916214059,     12.507343278686905,
        -0.13857109526572012,   9.9843695780195716e-6, 1.5056327351493116e-7
    };
    double a, t;

    if (x < 0.5) {
        /* Reflection formula: Γ(x)·Γ(1-x) = π / sin(πx) */
        return log(M_PI / sin(M_PI * x)) - ln_gamma(1.0 - x);
    }
    x -= 1.0;
    a = g[0];
    t = x + 7.5;
    for (int i = 1; i < 9; i++) {
        a += g[i] / (x + i);
    }
    return 0.5 * log(2.0 * M_PI) + (x + 0.5) * log(t) - t + log(a);
}

static double clamp01(double v)
{
    return (v < 0.0) ? 0.0 : (v > 1.0) ? 1.0 : v;
}

/*
 * 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.
 */
double gamma_q(double a, double x)
{
    if (a <= 0.0 || x < 0.0) {
        return NAN;
    }
    if (x == 0.0) {
        return 1.0;
    }

    if (x < a + 1.0) {
        /* Series for P(a,x); Q = 1 - P */
        double ap = a;
        double sum = 1.0 / a;
        double del = sum;

        for (int n = 0; n < 1000; n++) {
            ap += 1.0;
            del *= x / ap;
            sum += del;
            if (fabs(del) < fabs(sum) * 1e-15) {
                break;
            }
        }
        return clamp01(1.0 - sum * exp(-x + a * log(x) - ln_gamma(a)));
    }

    /* Continued fraction for Q(a,x) */
    {
        const double FPMIN = 1e-300;
        double b = x + 1.0 - a;
        double c = 1.0 / FPMIN;
        double d = 1.0 / b;
        double h = d;

        for (int i = 1; i <= 1000; i++) {
            double an = -i * (i - a);
            double del;

            b += 2.0;
            d = an * d + b;
            if (fabs(d) < FPMIN) d = FPMIN;
            c = b + an / c;
            if (fabs(c) < FPMIN) c = FPMIN;
            d = 1.0 / d;
            del = d * c;
            h *= del;
            if (fabs(del - 1.0) < 1e-15) {
                break;
            }
        }
        return clamp01(exp(-x + a * log(x) - ln_gamma(a)) * h);
    }
}

/* Upper-tail p-value for a χ² statistic with `df` degrees of freedom. */
double chi_squared_p(double chi2, double df)
{
    if (chi2 < 0.0 || df <= 0.0) {
        return NAN;
    }
    return gamma_q(df / 2.0, chi2 / 2.0);
}

/* ---------------------------------------------------------------- measures --- */

/*
 * 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.
 */
double serial_correlation_coefficient(const uint8_t *data, size_t n)
{
    double sum = 0.0, sum_sq = 0.0, sum_xy = 0.0, mean_sq, denom;

    if (n < 2) {
        return 0.0;
    }
    for (size_t i = 0; i < n; i++) {
        double x = data[i];
        double y = data[(i + 1) % n];

        sum    += x;
        sum_sq += x * x;
        sum_xy += x * y;
    }
    mean_sq = (sum * sum) / (double) n;
    denom = sum_sq - mean_sq;
    if (denom == 0.0) {
        return 0.0;
    }
    return (sum_xy - mean_sq) / 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.
 */
double monte_carlo_pi_estimate(const uint8_t *data, size_t len)
{
    size_t pairs = len / 2;
    long inside = 0;

    if (pairs == 0) {
        return 0.0;
    }
    for (size_t i = 0; i < pairs; i++) {
        double dx = (double) data[2 * i]     - 127.5;
        double dy = (double) data[2 * i + 1] - 127.5;

        if (dx * dx + dy * dy <= 128.0 * 128.0) {
            inside++;
        }
    }
    return (4.0 * (double) inside) / (double) pairs;
}

/*
 * Shannon entropy (bits/byte) of each consecutive `block_size`-byte block.
 * Returns a malloc'd array of length *out_count, or NULL when block_size < 1.
 */
double *block_entropies(const uint8_t *data, size_t len, size_t block_size, size_t *out_count)
{
    double *blocks;
    size_t count;

    if (block_size < 1) {
        *out_count = 0;
        return NULL; /* blockSize must be at least 1 */
    }
    count = (len + block_size - 1) / block_size;
    *out_count = count;
    if (count == 0) {
        return NULL;
    }
    blocks = malloc(count * sizeof *blocks);
    if (blocks == NULL) {
        *out_count = 0;
        return NULL;
    }

    for (size_t b = 0; b < count; b++) {
        long counts[256];
        size_t off = b * block_size;
        size_t end = off + block_size;
        size_t n;

        if (end > len) {
            end = len;
        }
        n = end - off;
        count_bytes(data + off, n, counts);
        blocks[b] = shannon_bits_per_byte(counts, 256, (double) n);
    }
    return blocks;
}

/* Map a Shannon entropy (bits/byte) to the verdict scale. */
entropy_verdict verdict_from_shannon(double bits_per_byte)
{
    if (bits_per_byte > 7.5) return VERDICT_EXCELLENT;
    if (bits_per_byte > 6.0) return VERDICT_GOOD;
    if (bits_per_byte > 4.0) return VERDICT_SUSPICIOUS;
    return VERDICT_LOW;
}

/* ---------------------------------------------------------------- analysis --- */

/*
 * Full analysis of a byte sequence. Returns false on empty input — there is
 * nothing to measure and every metric would be undefined. On success the caller
 * owns `out->block_entropies` and must free_entropy_analysis() it.
 */
bool analyze_entropy(const uint8_t *data, size_t len, entropy_analysis *out)
{
    if (data == NULL || len == 0 || out == NULL) {
        return false; /* Nothing to analyze - provide at least 1 byte of data. */
    }
    memset(out, 0, sizeof *out);

    count_bytes(data, len, out->byte_frequencies);
    out->shannon_entropy     = shannon_bits_per_byte(out->byte_frequencies, 256, (double) len);
    out->chi_squared         = chi_squared_statistic(out->byte_frequencies, (double) len);
    out->chi_squared_p_value = chi_squared_p(out->chi_squared, CHI_SQUARED_DF);
    out->serial_correlation  = serial_correlation_coefficient(data, len);
    out->monte_carlo_pi      = monte_carlo_pi_estimate(data, len);
    out->block_entropies     = block_entropies(data, len, BLOCK_SIZE, &out->block_count);
    out->verdict             = verdict_from_shannon(out->shannon_entropy);
    return true;
}

void free_entropy_analysis(entropy_analysis *analysis)
{
    if (analysis == NULL) {
        return;
    }
    free(analysis->block_entropies);
    analysis->block_entropies = NULL;
    analysis->block_count = 0;
}

/* -------------------------------------------------------------------- demo --- */

static void report(const char *label, const uint8_t *data, size_t len)
{
    entropy_analysis a;

    if (!analyze_entropy(data, len, &a)) {
        printf("%-18s (empty - nothing to analyze)\n", label);
        return;
    }
    printf("%-18s shannon %.4f bits/byte  chi2 %9.1f  p %.4f  scc %+.4f  pi %.4f  -> %s\n",
           label, a.shannon_entropy, a.chi_squared, a.chi_squared_p_value,
           a.serial_correlation, a.monte_carlo_pi, verdict_name(a.verdict));
    free_entropy_analysis(&a);
}

int main(void)
{
    uint8_t uniform[4096];
    uint8_t constant[4096];
    uint8_t ascii[4096];

    /* Every byte value equally often: maximal entropy, χ² of exactly 0. */
    for (size_t i = 0; i < sizeof uniform; i++) {
        uniform[i] = (uint8_t) (i % 256);
    }
    /* One repeating byte: zero entropy. */
    memset(constant, 0x41, sizeof constant);
    /* Printable ASCII only: ~6.6 bits/byte — "good" by Shannon alone, yet the
     * χ² p-value exposes it immediately. This is the case the multi-metric
     * battery exists to catch. */
    for (size_t i = 0; i < sizeof ascii; i++) {
        ascii[i] = (uint8_t) (32 + (i * 7 + i / 95) % 95);
    }

    report("uniform ramp:",  uniform,  sizeof uniform);
    report("constant 0x41:", constant, sizeof constant);
    report("printable ASCII:", ascii,  sizeof ascii);

    /* The verdict scale, for reference. */
    printf("\nverdict thresholds (bits/byte): >7.5 excellent, >6.0 good, >4.0 suspicious, else low\n");
    return EXIT_SUCCESS;
}

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 →