Skip to content

Commit edcf67c

Browse files
committed
feat(compile) added IA functions
1 parent 8a9f76e commit edcf67c

7 files changed

Lines changed: 238 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
preambles using four-corner `exp(exp * ln(base))` evaluation with special cases
1818
for point-integer exponents and `(-1)^n`.
1919

20+
- **`Factorial`, `Gamma`, `GammaLn` for GLSL/WGSL interval targets**: Added
21+
`ia_factorial` (via `ia_gamma(x+1)`) to both GPU targets. Added `ia_gamma`
22+
(Lanczos approximation) and `ia_gammaln` (Stirling asymptotic) to the WGSL
23+
target, matching existing GLSL implementations.
24+
2025
### Bug Fixes
2126

2227
- **Sum/Product with symbolic bounds compiled incorrectly**: Expressions like
@@ -56,12 +61,15 @@
5661
variables are excluded. This also fixes `Block` expressions where locally
5762
assigned variables (via `Assign` or `Declare`) were reported as unknowns.
5863

59-
- **Interval `piecewise` with constant branches**: Fixed `piecewise()` returning
60-
raw `Interval` objects instead of `IntervalResult` when branches evaluated to
61-
constants (e.g. `_IA.point(1)`). This caused `If` expressions like
62-
`\text{if}\; x \geq 0 \;\text{then}\; 1 \;\text{else}\; 0` to return
63-
`undefined` kind on definite conditions and `'entire'` on indeterminate
64-
conditions when compiled to `interval-js`.
64+
- **`Integrate` with symbolic bounds compiled incorrectly**: Same issue as
65+
Sum/Product — `compileIntegrate()` used `normalizeIndexingSet()` which
66+
converted symbolic bounds to `NaN`. Now uses `extractLimits()` and compiles
67+
bounds as expressions.
68+
69+
- **Interval `piecewise` test fix**: Fixed test that incorrectly accessed
70+
`result.lo` directly instead of unwrapping the `IntervalResult` envelope
71+
(`result.value.lo`). The `piecewise()` function correctly returns
72+
`IntervalResult` objects.
6573

6674
## 0.51.1 _2026-02-15_
6775

COMPUTE_ENGINE.md

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Compute Engine Integration
1+
**NOTE** The following document has been authored by the maintainers of a
2+
scientific plotting library that integrates the Compute Engine for LaTeX
3+
expression compilation. It provides context on how CE is used within the
4+
plotting system and the section "Requests for CE Maintainers" outlines specific
5+
features and fixes needed from CE to support the plotting use case.
26

3-
**NOTE** This document has been authored by the maintainers of a scientific
4-
plotting library that integrates the Compute Engine for LaTeX expression
5-
compilation. It provides context on how CE is used within the plotting system
6-
and the section "Requests for CE Maintainers" outlines specific features and
7-
fixes needed from CE to support the plotting use case.
7+
# Compute Engine Integration
88

99
How the Compute Engine (CE) is used for compiling LaTeX expressions into
1010
executable functions for the plotting system.
@@ -600,6 +600,31 @@ converted after adding LaTeX string support to `VectorFunction2DInput`:
600600
- `VectorFunction2DInput` in `types.ts` — now accepts `string` alongside
601601
`(x, y) => [number, number]` and `{ kind: "js", fn }`
602602

603+
### 8. Recursive `_gpu_gamma` in `interval-glsl` preamble
604+
605+
**Problem:** The CE's `interval-glsl` compilation target emits a monolithic
606+
~29KB preamble containing the full interval arithmetic library. This preamble
607+
includes a `_gpu_gamma(float z)` function that uses the reflection formula
608+
`Gamma(z) = pi / (sin(pi*z) * Gamma(1-z))` — a recursive call. GLSL forbids
609+
recursion, so any shader that includes this preamble fails to compile. The
610+
preamble is always emitted in full regardless of whether the expression actually
611+
uses the gamma function, so even simple expressions like `x^2 + y^2 - 1` are
612+
affected.
613+
614+
**Discovery:** GPU interval arithmetic for implicit curves produced no visual
615+
output. Manual shader compilation in the browser console revealed the GLSL
616+
compiler error pointing to the recursive `_gpu_gamma` call.
617+
618+
**Workaround:** `sanitizeIntervalPreamble()` in `shader-templates.ts` detects
619+
the recursive `_gpu_gamma` function via regex and replaces it with a
620+
non-recursive Lanczos approximation (`NON_RECURSIVE_GPU_GAMMA`) that handles
621+
both the `z >= 0.5` and `z < 0.5` branches inline without recursion.
622+
623+
**Upstream fix:** The `interval-glsl` preamble should use non-recursive function
624+
implementations. Either replace the recursive gamma with a Lanczos/Stirling
625+
approximation, or emit the preamble selectively (only include functions that the
626+
compiled expression actually references).
627+
603628
## Conversion Patterns
604629

605630
50 of 51 functions in `grid_paper.html` were converted from JS to LaTeX/CE. The
@@ -847,23 +872,29 @@ plotting integration. Ordered by impact.
847872

848873
### Medium Priority
849874

850-
5. **Warn on `success: false` fallback**: When compilation fails but `run` is
875+
5. **Fix recursive `_gpu_gamma` in `interval-glsl` preamble (gap #8)**: The
876+
preamble uses `Gamma(z) = pi / (sin(pi*z) * Gamma(1-z))` — a recursive call
877+
that GLSL forbids. Replace with a non-recursive Lanczos/Stirling
878+
approximation. Ideally, also emit the preamble selectively (only include
879+
functions the expression actually uses) to reduce shader size.
880+
881+
6. **Warn on `success: false` fallback**: When compilation fails but `run` is
851882
still set (interpreter fallback), emit a console warning. The current silent
852883
behavior makes it very hard to detect compilation failures.
853884

854-
6. **Add `SphericalHarmonic(l, m, theta, phi)` and
885+
7. **Add `SphericalHarmonic(l, m, theta, phi)` and
855886
`AssociatedLegendreP(n, m, x)`**: Would allow general spherical harmonics
856887
without manual expansion for each (l, m) pair.
857888

858-
7. **Support `\prod` in interval-js**: Currently only `\sum` compiles to
889+
8. **Support `\prod` in interval-js**: Currently only `\sum` compiles to
859890
interval-js. Product accumulation with `_IA.mul` would be analogous.
860891

861892
### Low Priority (Nice to Have)
862893

863-
8. **GLSL compilation for Bessel, Airy, Zeta, LambertW**: These are JS-only
894+
9. **GLSL compilation for Bessel, Airy, Zeta, LambertW**: These are JS-only
864895
today. GLSL preamble-based implementations would enable GPU-accelerated
865896
rendering of plots using these functions.
866897

867-
9. **Subscripted variable names in blocks**: Allow `r_1 \coloneq expr` to define
868-
a variable named `r_1` rather than parsing as `Subscript(r, 1)`. This is
869-
common in mathematical notation for intermediate values.
898+
10. **Subscripted variable names in blocks**: Allow `r_1 \coloneq expr` to
899+
define a variable named `r_1` rather than parsing as `Subscript(r, 1)`. This
900+
is common in mathematical notation for intermediate values.

src/compute-engine/compilation/interval-glsl-target.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,16 @@ IntervalResult ia_gammaln(IntervalResult x) {
10751075
return ia_gammaln(x.value);
10761076
}
10771077
1078+
// Factorial via gamma: n! = gamma(n+1)
1079+
IntervalResult ia_factorial(vec2 x) {
1080+
return ia_gamma(ia_add(ia_ok(x), ia_point(1.0)));
1081+
}
1082+
1083+
IntervalResult ia_factorial(IntervalResult x) {
1084+
if (ia_is_error(x.status)) return x;
1085+
return ia_factorial(x.value);
1086+
}
1087+
10781088
// Boolean interval comparisons
10791089
// Returns 1.0 = true, 0.0 = false, 0.5 = maybe
10801090
const float IA_TRUE = 1.0;
@@ -1224,6 +1234,7 @@ const INTERVAL_GLSL_FUNCTIONS: CompiledFunctions<Expression> = {
12241234
// Special functions
12251235
Gamma: (args, compile) => `ia_gamma(${compile(args[0])})`,
12261236
GammaLn: (args, compile) => `ia_gammaln(${compile(args[0])})`,
1237+
Factorial: (args, compile) => `ia_factorial(${compile(args[0])})`,
12271238

12281239
// Elementary functions
12291240
Abs: (args, compile) => `ia_abs(${compile(args[0])})`,

src/compute-engine/compilation/interval-wgsl-target.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,101 @@ fn ia_asech(x: IntervalResult) -> IntervalResult {
953953
return ia_asech_v(x.value);
954954
}
955955
956+
// Gamma function using Lanczos approximation (g=7, n=9 coefficients)
957+
// Poles at non-positive integers; minimum at x ≈ 1.4616
958+
fn _gpu_gamma(z_in: f32) -> f32 {
959+
let PI = 3.14159265358979;
960+
var z = z_in;
961+
if (z < 0.5) {
962+
return PI / (sin(PI * z) * _gpu_gamma(1.0 - z));
963+
}
964+
z -= 1.0;
965+
var x = 0.99999999999980993;
966+
x += 676.5203681218851 / (z + 1.0);
967+
x += -1259.1392167224028 / (z + 2.0);
968+
x += 771.32342877765313 / (z + 3.0);
969+
x += -176.61502916214059 / (z + 4.0);
970+
x += 12.507343278686905 / (z + 5.0);
971+
x += -0.13857109526572012 / (z + 6.0);
972+
x += 9.9843695780195716e-6 / (z + 7.0);
973+
x += 1.5056327351493116e-7 / (z + 8.0);
974+
let t = z + 7.5;
975+
return sqrt(2.0 * PI) * pow(t, z + 0.5) * exp(-t) * x;
976+
}
977+
978+
// Interval gamma function
979+
// Handles poles at non-positive integers and the minimum at x ≈ 1.4616
980+
fn ia_gamma_v(x: vec2f) -> IntervalResult {
981+
let GAMMA_MIN_X = 1.4616321;
982+
let GAMMA_MIN_Y = 0.8856032;
983+
984+
// Check for poles: interval crosses or touches zero
985+
if (x.x <= 0.0 && x.y >= 0.0) {
986+
return ia_singular(0.0);
987+
}
988+
989+
// Entirely negative: check if interval spans a negative integer
990+
if (x.x < 0.0) {
991+
let ceilLo = ceil(x.x);
992+
let floorHi = floor(x.y);
993+
if (ceilLo <= floorHi) {
994+
return ia_singular(ceilLo);
995+
}
996+
// No pole — both endpoints between same consecutive negative integers
997+
let gLo = _gpu_gamma(x.x);
998+
let gHi = _gpu_gamma(x.y);
999+
return ia_ok(vec2f(min(gLo, gHi) - IA_EPS, max(gLo, gHi) + IA_EPS));
1000+
}
1001+
1002+
// Entirely positive
1003+
if (x.x >= GAMMA_MIN_X) {
1004+
// Monotonically increasing
1005+
return ia_ok(vec2f(_gpu_gamma(x.x) - IA_EPS, _gpu_gamma(x.y) + IA_EPS));
1006+
}
1007+
if (x.y <= GAMMA_MIN_X) {
1008+
// Monotonically decreasing
1009+
return ia_ok(vec2f(_gpu_gamma(x.y) - IA_EPS, _gpu_gamma(x.x) + IA_EPS));
1010+
}
1011+
// Crosses the minimum
1012+
let gMax = max(_gpu_gamma(x.x), _gpu_gamma(x.y));
1013+
return ia_ok(vec2f(GAMMA_MIN_Y - IA_EPS, gMax + IA_EPS));
1014+
}
1015+
1016+
fn ia_gamma(x: IntervalResult) -> IntervalResult {
1017+
if (ia_is_error(x.status)) { return x; }
1018+
return ia_gamma_v(x.value);
1019+
}
1020+
1021+
// Log-gamma using Stirling asymptotic expansion, z > 0
1022+
fn _gpu_gammaln(z: f32) -> f32 {
1023+
let z3 = z * z * z;
1024+
return z * log(z) - z - 0.5 * log(z)
1025+
+ 0.5 * log(2.0 * 3.14159265358979)
1026+
+ 1.0 / (12.0 * z)
1027+
- 1.0 / (360.0 * z3)
1028+
+ 1.0 / (1260.0 * z3 * z * z);
1029+
}
1030+
1031+
// Interval log-gamma — monotonically increasing for x > 0
1032+
fn ia_gammaln_v(x: vec2f) -> IntervalResult {
1033+
if (x.y <= 0.0) { return ia_empty(); }
1034+
if (x.x > 0.0) {
1035+
return ia_ok(vec2f(_gpu_gammaln(x.x) - IA_EPS, _gpu_gammaln(x.y) + IA_EPS));
1036+
}
1037+
// Partial: clipped at lo
1038+
return ia_partial(vec2f(0.0, _gpu_gammaln(x.y) + IA_EPS), IA_PARTIAL_LO);
1039+
}
1040+
1041+
fn ia_gammaln(x: IntervalResult) -> IntervalResult {
1042+
if (ia_is_error(x.status)) { return x; }
1043+
return ia_gammaln_v(x.value);
1044+
}
1045+
1046+
// Factorial via gamma: n! = gamma(n+1)
1047+
fn ia_factorial(x: IntervalResult) -> IntervalResult {
1048+
return ia_gamma(ia_add(x, ia_point(1.0)));
1049+
}
1050+
9561051
// Boolean interval comparisons
9571052
// Returns 1.0 = true, 0.0 = false, 0.5 = maybe
9581053
const IA_TRUE: f32 = 1.0;
@@ -1103,6 +1198,11 @@ const INTERVAL_WGSL_FUNCTIONS: CompiledFunctions<Expression> = {
11031198
},
11041199
Negate: (args, compile) => `ia_negate(${compile(args[0])})`,
11051200

1201+
// Special functions
1202+
Gamma: (args, compile) => `ia_gamma(${compile(args[0])})`,
1203+
GammaLn: (args, compile) => `ia_gammaln(${compile(args[0])})`,
1204+
Factorial: (args, compile) => `ia_factorial(${compile(args[0])})`,
1205+
11061206
// Elementary functions
11071207
Abs: (args, compile) => `ia_abs(${compile(args[0])})`,
11081208
Ceil: (args, compile) => `ia_ceil(${compile(args[0])})`,

src/compute-engine/compilation/javascript-target.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import {
6363
variance,
6464
} from '../numerics/statistics';
6565
import { monteCarloEstimate } from '../numerics/monte-carlo';
66-
import { normalizeIndexingSet } from '../library/utils';
6766

6867
import { BaseCompiler } from './base-compiler';
6968
import type {
@@ -1383,13 +1382,23 @@ function compileSumProduct(
13831382
* Compile integration function
13841383
*/
13851384
function compileIntegrate(args, _, target: CompileTarget<Expression>): string {
1386-
const { index, lower, upper } = normalizeIndexingSet(args[1]);
1385+
const { index, lowerExpr, upperExpr, lowerNum, upperNum } =
1386+
extractLimits(args[1]);
13871387
const f = BaseCompiler.compile(args[0], {
13881388
...target,
13891389
var: (id) => (id === index ? id : target.var(id)),
13901390
});
13911391

1392-
return `_SYS.integrate((${index}) => (${f}), ${lower}, ${upper})`;
1392+
const lo =
1393+
lowerNum !== undefined
1394+
? String(lowerNum)
1395+
: BaseCompiler.compile(lowerExpr, target);
1396+
const hi =
1397+
upperNum !== undefined
1398+
? String(upperNum)
1399+
: BaseCompiler.compile(upperExpr, target);
1400+
1401+
return `_SYS.integrate((${index}) => (${f}), ${lo}, ${hi})`;
13931402
}
13941403

13951404
/**

test/compute-engine/compile-sum-product.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,37 @@ describe('COMPILE Sum - symbolic bounds', () => {
178178
expect(val.lo).toBeCloseTo(120, 10);
179179
expect(val.hi).toBeCloseTo(120, 10);
180180
});
181+
182+
// Symbolic LOWER bound tests
183+
test('JS: sum_{k=m}^{10} k with m=3 => 3+4+...+10 = 52', () => {
184+
const expr = ce.parse('\\sum_{k=m}^{10} k');
185+
const result = compile(expr);
186+
expect(result.success).toBe(true);
187+
expect(result.run!({ m: 3 })).toBe(52);
188+
});
189+
190+
test('JS: sum_{k=m}^{n} k with m=1 n=4 => 10', () => {
191+
const expr = ce.parse('\\sum_{k=m}^{n} k');
192+
const result = compile(expr);
193+
expect(result.success).toBe(true);
194+
expect(result.run!({ m: 1, n: 4 })).toBe(10);
195+
});
196+
197+
test('interval-js: sum_{k=m}^{10} k with m=3 => 52', () => {
198+
const expr = ce.parse('\\sum_{k=m}^{10} k');
199+
const result = compile(expr, { to: 'interval-js' });
200+
expect(result.success).toBe(true);
201+
const val = unwrapInterval(result.run!({ m: 3 }));
202+
expect(val.lo).toBeCloseTo(52, 10);
203+
expect(val.hi).toBeCloseTo(52, 10);
204+
});
205+
206+
test('JS: prod_{k=m}^{5} k with m=3 => 3*4*5 = 60', () => {
207+
const expr = ce.parse('\\prod_{k=m}^{5} k');
208+
const result = compile(expr);
209+
expect(result.success).toBe(true);
210+
expect(result.run!({ m: 3 })).toBe(60);
211+
});
181212
});
182213

183214
describe('COMPILE Product - interval-js', () => {
@@ -208,3 +239,22 @@ describe('COMPILE Product - interval-js', () => {
208239
expect(val.hi).toBe(1);
209240
});
210241
});
242+
243+
describe('COMPILE Integrate - symbolic bounds', () => {
244+
test('JS: int_0^a x dx with a=2 => 2', () => {
245+
const expr = ce.parse('\\int_0^a x \\, dx');
246+
const result = compile(expr);
247+
expect(result.success).toBe(true);
248+
// int_0^2 x dx = x^2/2 |_0^2 = 2
249+
const val = result.run!({ a: 2 });
250+
expect(val).toBeCloseTo(2, 1);
251+
});
252+
253+
test('JS: int_a^b x dx with a=0 b=1 => 0.5', () => {
254+
const expr = ce.parse('\\int_a^b x \\, dx');
255+
const result = compile(expr);
256+
expect(result.success).toBe(true);
257+
const val = result.run!({ a: 0, b: 1 });
258+
expect(val).toBeCloseTo(0.5, 1);
259+
});
260+
});

test/compute-engine/compile-which.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,16 @@ describe('COMPILE Which', () => {
109109
expect(result.code).toContain('_IA.piecewise');
110110

111111
// Test execution with point intervals
112-
// piecewise returns either {lo, hi} (plain interval) or {kind, value}
112+
// piecewise returns IntervalResult: {kind: 'interval', value: {lo, hi}}
113113
const positiveResult = result.run!({ x: 5 }) as any;
114-
// When condition is definitely true, piecewise returns the true branch
115-
// which is _IA.point(1) = {lo: 1, hi: 1}
116-
expect(positiveResult.lo).toBe(1);
117-
expect(positiveResult.hi).toBe(1);
114+
const posVal = positiveResult.kind === 'interval' ? positiveResult.value : positiveResult;
115+
expect(posVal.lo).toBe(1);
116+
expect(posVal.hi).toBe(1);
118117

119118
const negativeResult = result.run!({ x: -3 }) as any;
120-
expect(negativeResult.lo).toBe(-1);
121-
expect(negativeResult.hi).toBe(-1);
119+
const negVal = negativeResult.kind === 'interval' ? negativeResult.value : negativeResult;
120+
expect(negVal.lo).toBe(-1);
121+
expect(negVal.hi).toBe(-1);
122122
});
123123

124124
it('should compile multi-branch Which to nested piecewise', () => {

0 commit comments

Comments
 (0)