Skip to content

Distribution arithmetic: derived random variables, conditionals, and general P(…) - #79

Merged
aantthony merged 6 commits into
mainfrom
claude/distribution-arithmetic-variables-71ac54
Aug 8, 2026
Merged

Distribution arithmetic: derived random variables, conditionals, and general P(…)#79
aantthony merged 6 commits into
mainfrom
claude/distribution-arithmetic-variables-71ac54

Conversation

@aantthony

@aantthony aantthony commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What

Random variables become first-class values you can compute with:

Row Meaning
X ~ Normal(m, s), Uniform(lo, hi), Exponential(rate) (aliases N/U/Exp; bare X ~ N = standard) exact density curve (piecewise pdfs compile to GLSL)
Y = {X > 0: X^2, 1}, S = X1 + X2 derived random variable; density + μ/σ readout
X + Y as a bare row density of the expression, unnamed
P(-1 < X < 2) exact CDF + shader-shaded region
P(Y > 0.5), P(Y > X) Monte Carlo estimate over the joint samples (single-variable bounds also shade under the density)

Distinct names are independent — X1 + X2 is the convolution — while a repeated name stays dependent: X + X is exactly 2X, and P(Y > X) sees Y's dependence on X. The examples menu gains a central-limit-theorem demo (sum of four uniforms hugging the matching normal) and a conditional-variable graph.

How

  • Sampling model (lib/dist.ts): each base variable owns a deterministic stratified uniform stream (equal-mass quantile midpoints, shuffled by a hash of its name — a Latin-hypercube pairing across names), transformed through its quantile function. Derived variables evaluate column-wise over 131,072 joint samples. Fixed streams mean reproducible results and smooth response to slider drags (common random numbers).
  • Exact where a closed form exists: an affine combination of normal bases (Z = (X + Y)/2, a X + 1, chains through other derived names) reduces symbolically to its exact normal — mean Σcᵢμᵢ + d, sd √(Σ(cᵢσᵢ)²), shared names accumulating coefficients (the covariance accounting). Those rows draw through the shader with slider uniforms, report exact μ = …, σ = …, and their P(…) uses the closed-form CDF. Nonlinear transforms, products, conditionals, and non-normal mixes take the sampled path.
  • Density display: linear-binned Gaussian KDE at 1.4× Silverman bandwidth — measured on these stratified columns to lower both sup-error (1.22% → 0.95% of peak) and curve roughness (second-difference energy ÷2.4) versus the iid-optimal 0.9 factor.
  • Caching: per-variable, keyed by the serialized definition closure plus the values of the constants it transitively references — a slider tick resamples only what it touches (~6 ms measured for a two-normal sum), a static scene never resamples, and an edited definition can never serve stale samples.
  • Worker parity: worker/graph.ts previously rejected every ~ row; MCP validation and og previews now accept the whole family through the same shared lib code. worker/og.ts draws density polylines and shaded P-regions; the stack VM gains piecewise/comparisons/erf and 3-arg normalpdf/normalcdf.

Reviewer notes

  • Y = X^2 rows are diverted from the definition system by a textual pre-scan (scanRandomRows, transitive to Z = Y + 1) — it must run before parsing because these rows must not become constant definitions. Reserved/function names are excluded, so e = X stays an equation.
  • Error paths: cycles report on their rows and ripple to dependents; X < 2 as a bare row suggests try P(X < 2); distribution parameters may not reference random variables.
  • P readouts: exact values show 4 decimals (≈ 0.3103), Monte Carlo shows 3 (≈ 0.740, noise ~±0.001 at this sample count).
  • 518 tests pass (vitest run), including statistical checks against closed forms (conditional P(Y > 0.5) vs Φ, product/joint events, KDE sup-error, cache-invalidation invariants) and og pixel probes; npm run typecheck clean on all three tsconfigs. Verified interactively in the browser (CLT demo, conditional densities, slider-driven resampling).
  • Discrete distributions (Binomial/Poisson) are intentionally out of scope — they want the phase-3 stem/bar renderer per docs/math-objects.md.

Also in this PR: E(…) rows and a general symbolic integrator

Two follow-on features landed on this branch (commits bf9038a, 0a0b002):

E(expr) expectation rows — the mean of any expression in the declared variables, drawn as a vertical marker at x = E under the expression's density. Exact under a derivable law; otherwise integrated or sampled (below).

General symbolic integration (lib/integrate.ts) — deliberately not probability-specific:

Row Meaning
int[a..b] f(x) dx (also ) definite integral; constant results show a value readout
int(f(x) dx) antiderivative, plottable (y = int(f(x) dx))
int[-inf..x] exp(t) dt, int[-inf..inf] exp(-x^2) dx improper integrals — limits via the antiderivative, checked against real quadrature
y = int[0..x] sin(t)/t dt non-elementary: falls back to a Gauss–Legendre sum expanded like Σ, so it still plots through the shaders/VM with no new AST kind

The engine tries linearity, power rules, the complete rational-function algorithm (Horowitz–Ostrogradsky in exact rational arithmetic + log/atan terms over poly.ts's certified roots), elementary families, Gaussians via erf, u-substitution, parts, and trig rewrites — and verifies every candidate by differentiating back and sampling before returning it. Definite values are cross-checked against adaptive Gauss–Kronrod, so FTC is never trusted across a non-integrable pole (int[-1..1] 1/x^2 dx refuses the confident −2). Iterated int … int … dx dy pairs inside-out; d/dt inside a body consumes its own dt; bounds take sliders, t, and the plot variable.

The probability system consumes the integrator: for Y = g(X) with a single base dependency, moments come from ∫ g(x)·pdf(x) dx by adaptive quadrature (~9 significant digits where the sample mean gave ~3), with undefined regions dropping out of numerator and mass alike. Joint expressions (E(X·W)) still use the stratified sampler. 584 tests pass; typecheck clean on all three tsconfigs; verified interactively (Si/erf shader curves, ∫₋∞ˣ eᵗ dt overlaying , slider-driven bounds).

🤖 Generated with Claude Code


Migrated from equation-src#59 as part of the open-source move.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
equation 2eb74ed Commit Preview URL

Branch Preview URL
Aug 02 2026, 12:36 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
equation 5174ae3 Commit Preview URL

Branch Preview URL
Aug 08 2026, 03:19 AM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes random variables first-class across the app and worker backends (derived RVs, probabilities P(…), expectations E(…)), and adds a general symbolic integration system (int / ) that resolves to verified closed forms or expanded quadrature expressions so existing shader/VM pipelines can render them.

Changes:

  • Extend the worker OG renderer and stack VM to support piecewise conditionals/comparisons, sampled density rendering, shaded probability regions, and expectation markers.
  • Introduce a new symbolic integration engine with verification + quadrature fallback, and wire it through expression parsing/resolution and UI readouts.
  • Refactor RV parsing/building into a shared RVSystem flow and update docs/tests/MCP validation to cover RV arithmetic, P(…), E(…), and rows.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
worker/vm.ts Adds VM opcodes for comparisons/selection and compiles piecewise expressions.
worker/og.ts Renders density/prob/expect plots in OG previews using the shared RV/density logic.
worker/og.test.ts Adds OG pixel-probe tests covering RV densities, P shading, E markers, and ∫ curves.
worker/og.coverage.test.ts Extends OG renderability coverage to E(…) and integral rows.
worker/mcp.ts Updates MCP tool help strings and row-kind reporting for expectation/integral support.
worker/mcp.test.ts Adds MCP validation tests for RV rows, P(…), E(…), and rows.
worker/graph.ts Mirrors web behavior: RV rows resolved outside defs; adds prob/expect classification + readouts; returns rvs in analysis.
web/public/llms.txt Updates public syntax docs with integrals, derived RVs, expectations, and new examples.
web/main.ts Integrates RV arithmetic/expectations/integrals into the main recompile/render pipeline and examples menu.
lib/poly.ts Exposes rational/poly helpers needed by the integrator (and adds extended GCD).
lib/plot.ts Adds plot types for sampled density, Monte Carlo probability shading, and expectation markers.
lib/integrate.ts New symbolic integration engine with verification and quadrature expansion fallback.
lib/integrate.test.ts New test suite for antiderivatives, quadrature, definite verification, and resolve-time ∫ syntax.
lib/expr.ts Adds int/ parsing, alias handling, and exports EVAL_FNS for shared evaluation tables.
lib/dist.test.ts Updates/expands distribution tests for new RV system, exact laws, sampling, atoms, P, E, etc.
lib/diff.ts Extends differentiation to handle normalpdf/normalcdf with full chain rule.
lib/defs.ts Resolves int/ at resolve-time using integrate.ts (closed form or quadrature expansion) + adds usesIntegral.
docs/math-objects.md Documents derived RVs, Monte Carlo probabilities, and other math-object rendering semantics.
Suppressed comments (1)

worker/og.ts:355

  • Same issue as above for E(…) markers: analysis.constEnv does not contain t, so expectation markers for time-dependent variables will be skipped rather than evaluated at t = 0 for the preview.
    const name = cls.plot.rv;
    const m = analysis.rvs.mean(name, analysis.constEnv);
    if (!Number.isFinite(m)) return;
    const exact = analysis.rvs.exactDist(name);
    let h: number;
    try {
      h = exact
        ? evaluate(pdfExpr(exact, { kind: 'num', value: m }), analysis.constEnv)
        : (c => (c ? densityAt(c, m) : 0))(analysis.rvs.curve(name, analysis.constEnv));

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread worker/og.ts
Comment on lines +312 to +332
const shade = cls.plot.type === 'prob' ? cls.plot.shade : undefined;
if (cls.plot.type === 'prob' && !shade) return; // readout-only row
const name = cls.plot.type === 'density' ? cls.plot.rv : shade!.rv;
const curve = analysis.rvs.curve(name, analysis.constEnv);
if (!curve) return;
if (cls.plot.type === 'density') {
// Point masses draw as probability stems (height = mass, not density).
for (const a of curve.atoms ?? []) {
const ax = toScreenX(r, v, a.x);
drawLine(r, ax, toScreenY(r, v, 0), ax, toScreenY(r, v, a.p), color);
drawDisc(r, ax, toScreenY(r, v, a.p), 3.5, color);
}
}
if (curve.pts.length < 4) return;
const pts = shade
? shadePolygon(
curve,
shade.lo ? evaluate(shade.lo, analysis.constEnv) : undefined,
shade.hi ? evaluate(shade.hi, analysis.constEnv) : undefined,
)
: curve.pts;
aantthony and others added 6 commits August 8, 2026 13:17
…ral P(…)

Random variables are now first-class values. Beyond the existing
X ~ Normal(m, s) rows (joined by Uniform(lo, hi) and Exponential(rate),
with aliases N/U/Exp and bare-name standard parameters):

- Y = g(X, …) declares a derived random variable — sums, products,
  piecewise conditionals ({X > 0: X^2, 1}) — and a bare expression row
  (X + Y) plots that density unnamed. Distinct names are independent, so
  X1 + X2 is the convolution; a repeated name stays dependent, so X + X
  is exactly 2X.
- P(…) takes any inequality over the variables: single-variable bounds
  on a closed-form distribution stay exact (CDF + shader region);
  everything else — P(Y > 0.5), P(Y > X) — estimates from joint samples.
- Affine combinations of normal bases (Z = (X + Y)/2, a X + 1) reduce
  symbolically to their exact normal, drawn via the shader with slider
  uniforms and exact μ/σ readouts; shared names accumulate coefficients,
  which is the covariance accounting.

Engine (lib/dist.ts): per-name deterministic stratified uniform streams
(Latin-hypercube pairing) transformed by quantile functions; derived
variables evaluate column-wise over 131072 joint samples; densities are
linear-binned Gaussian KDEs (1.4× Silverman — measured to lower both
sup-error and curve roughness on these stratified columns). Caches are
per-variable and definition-aware, so a slider drag resamples only what
it touches and edits can never serve stale samples.

The worker mirrors the pipeline: graph.ts previously rejected every
~ row, so MCP validation and og previews now accept the whole family;
og draws density polylines and shaded regions; the VM gains piecewise,
comparisons, erf, and 3-arg normalpdf/normalcdf.

Examples (CLT: four uniforms vs the matching normal), llms.txt row
docs, and the math-objects inventory are updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lution

A bare `X` row (or `2X + 1`) over a uniform base used to re-enter the
sampled KDE and draw rounded shoulders where the box has corners. The
affine analyzer now works over every base family and reduces to an exact
law wherever one exists:

- one term: c·Uniform+d is Uniform again (min/max endpoints keep a
  negative or slider-driven c honest); c·Exponential with a positive
  literal c is Exponential(λ/c); identity passes any base through. These
  draw through the shader like base rows, with exact μ/σ readouts.
- several uniform terms: an exact piecewise-polynomial convolution
  (uniform is degree 0; each convolution raises the degree by one and
  merges pairwise support sums), evaluated numerically per parameter
  values — breakpoint ordering depends on slider values, so the numeric
  form sidesteps the case analysis a symbolic one would need. The
  triangle's apex is now a true corner, Irwin–Hall is exact, and
  P(lo < S < hi) integrates the polynomial instead of counting samples.

P(…) also accepts bounds around one inline expression —
`P(0.5 < X + Y < 1.5)` registers the expression as an anonymous derived
variable, so exactness and shading work without naming it.

The identifier scan for derived rows now follows the tokenizer's rule (a
maximal run starting with a letter) instead of \b word boundaries, which
missed `2X` — both 2 and X are word characters, so `W = 2X + 1` never
became a random variable at all.

Products, nonlinear and piecewise transforms, and mixed families keep
the sampled path; a Risch-style general integrator was considered and
rejected — for these densities the antiderivatives are power-rule
trivial and the work is support bookkeeping, while the integrands that
are hard (Gaussians, products) have no elementary antiderivative for it
to find.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A purely discrete result — Y = {X > 0: 1, 2}, floor(4X), a constant —
was smeared by the kernel estimator into narrow Gaussian bumps whose
height read as density (~3.3) where the truth is two atoms of mass ½.
Exactly repeated sample values give atoms away: estimateCurve splits
them out (mass threshold 0.2%, a duplicate probe over a 4096-sample
prefix keeps continuous columns on the fast path — stratified streams
make their values distinct), and the app and og renderers draw them as
stems of height = probability with a dot, the standard pmf picture.
Mixed distributions get both: {X > 0: X^2, 1} draws the χ²-shaped
continuous half plus a stem at (1, ½). Atom masses come out exact
because the streams are stratified: exactly half the midpoints sit on
each side of the median.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A truncated variable like Y = {X > 1: X, 0} drew a smooth ramp rising
from ~0.75 to a rounded peak past 1, where the truth is nothing below 1
and a jump to phi(1) = 0.242. That is textbook kernel boundary bias: the
kernel spills mass across an edge it does not know about, halving the
height at the edge and pushing the mode inward.

estimateCurve now recognizes an edge — an end of the drawn range the
tail trim did not reach is the observed support itself — and at those
ends only:

- clamps the drawn range to the support and steps down to the axis, so
  the cut is vertical rather than a ramp;
- fits a local *line* rather than a local mean (kernel moments a0/a1/a2
  over the part of the window inside the support), which reproduces a
  linear density exactly and so keeps full height on a sloped edge;
- doubles the edge grid point, whose linear-binning catchment is only
  half a cell wide and therefore read half the density.

A merely trimmed tail keeps the uncorrected estimate — its data
continues past the window, so there is no edge to correct.

Measured against the closed forms: the jump lands at 0.2412 vs phi(1) =
0.2420, sup error over (1,3) falls 0.0185 -> 0.0008, and a half-normal's
edge reads 0.7997 vs 0.7979. Interior accuracy is unchanged.

The curve is finally rescaled so its area is the probability of the
range it covers — the promise a density plot makes. That matters where
no local fit can be right: X^2 near 0 has an integrable singularity, and
without it the corrected spike carried 12% too much mass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… quadrature fallback

A general integration engine (lib/integrate.ts), not tied to any consumer:
linearity, power rules, the complete rational-function algorithm
(Horowitz–Ostrogradsky reduction in exact rational arithmetic, log/atan
terms over poly.ts's certified real roots), elementary families with linear
arguments, Gaussians via erf, u-substitution, integration by parts, and trig
product/power rewrites. Every candidate is verified by differentiating back
and sampling before it escapes; definite values are additionally checked
against adaptive Gauss–Kronrod quadrature, so the fundamental theorem is
never trusted across a non-integrable pole.

Syntax expands at resolve time like Σ: int[a..b] f(x) dx, int(f(x) dx), the
∫ glyph, iterated integrals pairing inside-out, and ±inf/∞ bounds (limits
via the antiderivative, the standard rational change of variables for the
numeric path). Where no closed form is found the integral becomes a
composite Gauss–Legendre sum in ordinary Expr form — so shaders, the worker
VM and every other consumer evaluate non-elementary integrals (Si, tail
integrals) with no new AST kind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
E(expr) takes any expression in the declared random variables: exact under a
derivable law, and drawn as a vertical marker at x = E under the
expression's density. Integral rows surface through the app and worker —
non-elementary curves (Si, erf) plot via the expanded quadrature sums, and a
row whose ∫ resolves to a constant shows its value as a readout.

The probability system now consumes the general integrator: for Y = g(X)
with a single base dependency, moments come from ∫ g(x)·pdf(x) dx by
adaptive quadrature (~9 significant digits where the sample mean gives ~3),
with the ±inf machinery carrying the normal and exponential tails. Joint
expressions like E(X·W) still fall to the stratified sampler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aantthony
aantthony force-pushed the claude/distribution-arithmetic-variables-71ac54 branch from 0a0b002 to 5174ae3 Compare August 8, 2026 03:18
@aantthony
aantthony merged commit f8df08c into main Aug 8, 2026
2 checks passed
aantthony added a commit that referenced this pull request Aug 8, 2026
* Update all dependencies to latest

Bumps devDependencies: @cloudflare/vite-plugin 1.45.1→1.51.1,
@types/node 24→26, playwright 1.61.1→1.62.1, vite 7→8,
wrangler 4.112.0→4.120.0. pnpm audit reports no vulnerabilities.
Typecheck, all 629 tests, and the production build pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Showcase the distribution arithmetic and integration features on /about

Four new gallery cards for what #79 shipped: sums as real convolutions
(four uniforms hugging the matching normal), derived random variables with
point masses and P(…) shading, running integrals plotting as curves, and
the non-elementary Si(x) drawn through the quadrature fallback. Shots
rendered with npm run shots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants