Distribution arithmetic: derived random variables, conditionals, and general P(…) - #79
Merged
Merged
Conversation
Deploying with
|
| 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 |
Deploying with
|
| 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 |
There was a problem hiding this comment.
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
RVSystemflow 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.constEnvdoes not containt, so expectation markers for time-dependent variables will be skipped rather than evaluated att = 0for 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 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; |
…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
force-pushed
the
claude/distribution-arithmetic-variables-71ac54
branch
from
August 8, 2026 03:18
0a0b002 to
5174ae3
Compare
This was referenced Aug 8, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Random variables become first-class values you can compute with:
X ~ Normal(m, s),Uniform(lo, hi),Exponential(rate)(aliasesN/U/Exp; bareX ~ N= standard)Y = {X > 0: X^2, 1},S = X1 + X2X + Yas a bare rowP(-1 < X < 2)P(Y > 0.5),P(Y > X)Distinct names are independent —
X1 + X2is the convolution — while a repeated name stays dependent:X + Xis exactly2X, andP(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
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).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 theirP(…)uses the closed-form CDF. Nonlinear transforms, products, conditionals, and non-normal mixes take the sampled path.worker/graph.tspreviously rejected every~row; MCP validation and og previews now accept the whole family through the same shared lib code.worker/og.tsdraws density polylines and shaded P-regions; the stack VM gains piecewise/comparisons/erfand 3-argnormalpdf/normalcdf.Reviewer notes
Y = X^2rows are diverted from the definition system by a textual pre-scan (scanRandomRows, transitive toZ = Y + 1) — it must run before parsing because these rows must not become constant definitions. Reserved/function names are excluded, soe = Xstays an equation.X < 2as a bare row suggeststry P(X < 2); distribution parameters may not reference random variables.Preadouts: exact values show 4 decimals (≈ 0.3103), Monte Carlo shows 3 (≈ 0.740, noise ~±0.001 at this sample count).vitest run), including statistical checks against closed forms (conditionalP(Y > 0.5)vsΦ, product/joint events, KDE sup-error, cache-invalidation invariants) and og pixel probes;npm run typecheckclean on all three tsconfigs. Verified interactively in the browser (CLT demo, conditional densities, slider-driven resampling).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:int[a..b] f(x) dx(also∫)int(f(x) dx)y = int(f(x) dx))int[-inf..x] exp(t) dt,int[-inf..inf] exp(-x^2) dxy = int[0..x] sin(t)/t dtThe 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 dxrefuses the confident −2). Iteratedint … int … dx dypairs inside-out;d/dtinside a body consumes its owndt; 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) dxby 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ᵗ dtoverlayingeˣ, slider-driven bounds).🤖 Generated with Claude Code
Migrated from equation-src#59 as part of the open-source move.