A draw as a uniform, live¶
A seeded draw's 32 bits as a uniform in [0, 1): the top 24 bits over 2²⁴, (drawn >> 8) / 2²⁴, exact in binary32 (lusine.error.uniform), so the device's float and the host's seeded error hold the same number. Below, the library's uniform runs on your GPU as you read: on draws whose uniforms the host's seeded error gives, its limits among them, and each step on sixteen new draws from one seed (lusine.error.draw, the stream "jack", indices 0 to 15 at the step), which are binned into twenty bins of [0, 1) and summed.
This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.
What it shows¶
- The strip above the controls is green while every check holds, and turns red for good once one fails.
- The spread: the first chart draws each bin's draws so far over a fair draw's, a twentieth of them, against the bin's centre, one bin a step and round again, beside 1 and 4.5 standard errors either side of it, where a fair draw's bins fall. The second draws the chi-square of the twenty bins against an even spread, a point a step, beside its bound, 50.8, the value a fair draw passes only once in ten thousand times (the 0.9999 quantile of the chi-square law with 19 degrees of freedom). Below them, the uniforms drawn, their mean, their variance about ½ and the chi-square now.
- The checks: the host's uniforms, each case the host's exactly: the limits 0 and 255 giving 0, 256 giving 2⁻²⁴, 2³¹ giving ½ and 2³² − 1 giving 1 − 2⁻²⁴, the largest, below 1, and six of the host's draws; in [0, 1), every uniform drawn falling in one of the bins; and, once sixteen steps (256 draws) are drawn, at every step: a flat spread, the chi-square within its bound; mean ½ and variance 1/12, each within 4.5 standard errors of a fair uniform's.
Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The seed and the steps before the spread is held are the program's settings, which the host may write; this page offers no control to change them yet.
The program¶
The host's cases, the draws, the bins and the checks, the panels the page draws, and the one procedure, main, run each step: the cases, the step's sixteen draws binned and summed, and the checks.
// The seeded error's uniform's live page: the library's uniform held to the host's seeded error's values bit for bit
// and to its limits, and sixteen draws a step from one seed binned into twenty bins and held to a flat spread by a
// chi-square bound, with their mean and variance held to a fair uniform's, with the page's checks computed here on the
// GPU and shown by the panels the program declares.
namespace uniform;
lusine.version version = { { number = 1 } };
// The reader's settings: the seed the draws are made from, and the steps before the spread is held to its bounds.
host struct { integer seed = 20261005; integer settle = 16; } settings;
// Draws and their uniforms as the host's seeded error gives them, each draw as its signed 32-bit integer (two's
// complement: -1 is 2^32 - 1): the limits first, 0 and 255 to 0, 256 to 2^-24, 2^31 to 1/2 and 2^32 - 1 to
// 1 - 2^-24; then the host's draws for seed 20261005, steps 0 and 1, "jack"; seed 7, step 12, "red/normal", indices
// 0 and 1; seed 42, step 3, "cafe" with its accent, index 5; and 2^32 - 1 for the seed, the step and the index of
// "jack". Each uniform is a binary32 exactly, so it is written as the host gives it.
struct case { host integer drawn; host float want; float got; bool held; };
table<case> cases(capacity = 16) = {
{ drawn = 0, want = 0.0 },
{ drawn = 255, want = 0.0 },
{ drawn = 256, want = 5.960464477539063e-08 },
{ drawn = -2147483648, want = 0.5 },
{ drawn = -1, want = 0.9999999403953552 },
{ drawn = -63416339, want = 0.9852346777915955 },
{ drawn = 173216405, want = 0.04033005237579346 },
{ drawn = 1288819362, want = 0.30007660388946533 },
{ drawn = -609698014, want = 0.8580436110496521 },
{ drawn = 478915069, want = 0.11150604486465454 },
{ drawn = 1386905939, want = 0.322914183139801 },
};
// Sixteen draws a step, at indices 0 to 15 of the stream "jack", and each one's uniform.
struct lane { integer index; float u; };
table<lane> lanes(capacity = 16) = {
{ index = 0 }, { index = 1 }, { index = 2 }, { index = 3 }, { index = 4 }, { index = 5 }, { index = 6 },
{ index = 7 }, { index = 8 }, { index = 9 }, { index = 10 }, { index = 11 }, { index = 12 }, { index = 13 },
{ index = 14 }, { index = 15 },
};
// Twenty bins of [0, 1), each a twentieth, and the uniforms each has held so far.
struct bin { integer number; float low; float high; integer held; };
table<bin> bins(capacity = 20) = {
{ number = 0, low = 0.0, high = 0.05 }, { number = 1, low = 0.05, high = 0.1 },
{ number = 2, low = 0.1, high = 0.15 }, { number = 3, low = 0.15, high = 0.2 },
{ number = 4, low = 0.2, high = 0.25 }, { number = 5, low = 0.25, high = 0.3 },
{ number = 6, low = 0.3, high = 0.35 }, { number = 7, low = 0.35, high = 0.4 },
{ number = 8, low = 0.4, high = 0.45 }, { number = 9, low = 0.45, high = 0.5 },
{ number = 10, low = 0.5, high = 0.55 }, { number = 11, low = 0.55, high = 0.6 },
{ number = 12, low = 0.6, high = 0.65 }, { number = 13, low = 0.65, high = 0.7 },
{ number = 14, low = 0.7, high = 0.75 }, { number = 15, low = 0.75, high = 0.8 },
{ number = 16, low = 0.8, high = 0.85 }, { number = 17, low = 0.85, high = 0.9 },
{ number = 18, low = 0.9, high = 0.95 }, { number = 19, low = 0.95, high = 1.0 },
};
// The step, the uniforms drawn so far, their sum and their squares' distance from 1/2 summed; their mean and their
// variance about 1/2; the chi-square of the bins against a flat spread and its bound, the 0.9999 quantile of the
// chi-square law with 19 degrees of freedom; and, for the chart, one bin a step, its centre and its draws over a fair
// draw's, n / 20, beside 1 and 4.5 standard errors either side of it, sqrt(19 / n).
struct {
integer step; integer drawn; float sum; float squares; float mean; float variance; float chi_square;
float bound = 50.8; float centre; float share; float fair = 1.0; float low; float high;
} spread;
// The checks: every case the host's uniform exactly; every uniform drawn falling in one of the bins, so in [0, 1);
// and, once the settle's steps are drawn, at every step the bins' chi-square within its bound and the mean and the
// variance each within 4.5 standard errors of a fair uniform's, 1/2 and 1/12.
struct { bool host = true; bool range = true; bool flat = true; bool mean = true; bool variance = true; } checks;
// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);
vector<float, 4> lamp_colour(bool holds)
{
return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}
integer missed(table<bool> held) { return count(held) where !held; }
float drawn(integer seed, integer step, integer index)
{
return lusine.error.uniform(drawn = lusine.error.draw(seed = bits(seed), step = bits(step), key = fnv1a("jack"),
index = bits(index)));
}
integer landed(table<float> u, float low, float high)
{
let from = low;
let to = high;
return count(u) where u >= from && u < to;
}
integer binned(table<integer> held) { return sum(held); }
float total(table<float> u) { return sum(u); }
float squares(table<float> u) { return sum((u - 0.5) * (u - 0.5)); }
float chi_square(table<integer> held, integer drawn)
{
let expected = float(drawn) / 20.0;
return sum((float(held) - expected) * (float(held) - expected)) / expected;
}
float share_of(table<integer> held, table<integer> number, integer which, integer drawn)
{
return float(sum(held) where number == which) / (float(drawn) / 20.0);
}
output table<lusine.widget> panels = {
lusine.series { { panel = "the spread", chart = "draws over a fair draw's", quantity = spread.share, steps = 20,
against = spread.centre, label = "a bin" } },
lusine.series { { panel = "the spread", chart = "draws over a fair draw's", quantity = spread.fair, steps = 20,
against = spread.centre, label = "fair" } },
lusine.series { { panel = "the spread", chart = "draws over a fair draw's", quantity = spread.low, steps = 20,
against = spread.centre, label = "-4.5 se" } },
lusine.series { { panel = "the spread", chart = "draws over a fair draw's", quantity = spread.high, steps = 20,
against = spread.centre, label = "+4.5 se" } },
lusine.series { { panel = "the spread", chart = "chi-square", quantity = spread.chi_square, steps = 400,
label = "chi-square" } },
lusine.series { { panel = "the spread", chart = "chi-square", quantity = spread.bound, steps = 400,
label = "its bound" } },
lusine.value { { panel = "the spread", quantity = spread.drawn, label = "uniforms drawn" } },
lusine.value { { panel = "the spread", quantity = spread.mean, label = "mean" } },
lusine.value { { panel = "the spread", quantity = spread.variance, label = "variance" } },
lusine.value { { panel = "the spread", quantity = spread.chi_square, label = "chi-square, 19 degrees" } },
lusine.value { { panel = "the checks", quantity = checks.host, label = "the host's uniforms" } },
lusine.value { { panel = "the checks", quantity = checks.range, label = "in [0, 1)" } },
lusine.value { { panel = "the checks", quantity = checks.flat, label = "a flat spread" } },
lusine.value { { panel = "the checks", quantity = checks.mean, label = "mean 1/2" } },
lusine.value { { panel = "the checks", quantity = checks.variance, label = "variance 1/12" } },
};
void main()
{
cases.got = lusine.error.uniform(drawn = bits(cases.drawn));
cases.held = cases.got == cases.want;
spread.step = spread.step + 1;
lanes.u = drawn(settings.seed, spread.step, lanes.index);
bins.held = bins.held + landed(lanes.u, bins.low, bins.high);
spread.drawn = spread.drawn + 16;
spread.sum = spread.sum + total(lanes.u);
spread.squares = spread.squares + squares(lanes.u);
spread.mean = spread.sum / float(spread.drawn);
spread.variance = spread.squares / float(spread.drawn);
spread.chi_square = chi_square(bins.held, spread.drawn);
spread.centre = 0.025 + 0.05 * float((spread.step - 1) % 20);
spread.low = 1.0 - 4.5 * sqrt(19.0 / float(spread.drawn));
spread.high = 1.0 + 4.5 * sqrt(19.0 / float(spread.drawn));
spread.share = share_of(bins.held, bins.number, which = (spread.step - 1) % 20, spread.drawn);
checks.host = checks.host && missed(cases.held) == 0;
checks.range = checks.range && binned(bins.held) == spread.drawn;
checks.flat = checks.flat && (spread.step < settings.settle || spread.chi_square <= spread.bound);
// A fair uniform's mean has the variance 1/(12 n), and the mean of its (u - 1/2)^2 the variance (1/80 - 1/144) / n.
checks.mean = checks.mean && (spread.step < settings.settle
|| (spread.mean - 0.5) * (spread.mean - 0.5)
<= 20.25 / (12.0 * float(spread.drawn)));
checks.variance = checks.variance && (spread.step < settings.settle
|| (spread.variance - 1.0 / 12.0) * (spread.variance - 1.0 / 12.0)
<= 20.25 * (1.0 / 80.0 - 1.0 / 144.0) / float(spread.drawn));
lamps.colour = lamp_colour(holds = checks.host && checks.range && checks.flat && checks.mean && checks.variance);
}
The uniform's other proof (twenty thousand draws against the host's, two hundred thousand binned, the normal's inputs) is on its proof page.