Skip to content

Commit b88487f

Browse files
committed
fix: interval arithmetic for the gamma function
1 parent 384e208 commit b88487f

4 files changed

Lines changed: 102 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## [Unreleased]
22

3+
### Bug Fixes
4+
5+
- **Interval-JS compilation for Gamma functions**
6+
7+
### Other Changes
8+
39
- Updated color palettes
410

511
## 0.51.0 _2026-02-14_

src/compute-engine/interval/elementary.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import type { Interval, IntervalResult } from './types';
88
import { ok, containsZero, isNegative, unwrapOrPropagate } from './util';
99
import { sub, mul, div } from './arithmetic';
10-
import { gamma as scalarGamma } from '../numerics/special-functions';
10+
import {
11+
gamma as scalarGamma,
12+
gammaln as scalarGammaln,
13+
} from '../numerics/special-functions';
1114

1215
/**
1316
* Square root of an interval (or IntervalResult).
@@ -578,3 +581,44 @@ function _gamma(x: Interval): IntervalResult {
578581
hi: Math.max(scalarGamma(x.lo), scalarGamma(x.hi)),
579582
});
580583
}
584+
585+
/**
586+
* Natural logarithm of the absolute value of the gamma function.
587+
*
588+
* gammaln(x) = ln(|gamma(x)|)
589+
*
590+
* Has the same poles as gamma (at non-positive integers), but approaches
591+
* -Infinity near poles instead of ±Infinity. For positive x, gammaln is
592+
* monotonically increasing.
593+
*/
594+
export function gammaln(x: Interval | IntervalResult): IntervalResult {
595+
const unwrapped = unwrapOrPropagate(x);
596+
if (!Array.isArray(unwrapped)) return unwrapped;
597+
const [xVal] = unwrapped;
598+
return _gammaln(xVal);
599+
}
600+
601+
function _gammaln(x: Interval): IntervalResult {
602+
// Check for poles: gammaln has poles at every non-positive integer.
603+
if (x.hi >= 0 && x.lo <= 0) {
604+
// Interval crosses or touches zero — pole at 0
605+
return { kind: 'singular', at: 0 };
606+
}
607+
if (x.lo < 0) {
608+
// Entirely negative: check if interval spans a negative integer
609+
const ceilLo = Math.ceil(x.lo);
610+
const floorHi = Math.floor(x.hi);
611+
// If any integer in [ceil(lo), floor(hi)] is <= 0, there's a pole
612+
if (ceilLo <= floorHi) {
613+
return { kind: 'singular', at: ceilLo };
614+
}
615+
// No pole in interval — gammaln is continuous between poles
616+
const gLo = scalarGammaln(x.lo);
617+
const gHi = scalarGammaln(x.hi);
618+
return ok({ lo: Math.min(gLo, gHi), hi: Math.max(gLo, gHi) });
619+
}
620+
621+
// x.lo > 0: entirely positive
622+
// gammaln is monotonically increasing for x > 0
623+
return ok({ lo: scalarGammaln(x.lo), hi: scalarGammaln(x.hi) });
624+
}

src/compute-engine/interval/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import {
5555
mod as _mod,
5656
remainder as _remainder,
5757
sign as _sign,
58+
gamma as _gamma,
59+
gammaln as _gammaln,
5860
} from './elementary';
5961
import {
6062
sin as _sin,
@@ -144,6 +146,8 @@ export {
144146
mod,
145147
remainder,
146148
sign,
149+
gamma,
150+
gammaln,
147151
} from './elementary';
148152

149153
// Trigonometric functions
@@ -242,6 +246,8 @@ export const IntervalArithmetic = {
242246
mod: _mod,
243247
remainder: _remainder,
244248
sign: _sign,
249+
gamma: _gamma,
250+
gammaln: _gammaln,
245251

246252
// Trigonometric
247253
sin: _sin,

test/compute-engine/compile-interval-js.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ describe('INTERVAL JS COMPILATION - FUNCTIONS', () => {
123123
const fn = compile(expr, { to: 'interval-js' });
124124
expect(fn.code).toContain('_IA.piecewise');
125125
});
126+
127+
test('compiles Gamma', () => {
128+
const expr = ce.parse('\\Gamma(x)');
129+
const fn = compile(expr, { to: 'interval-js' });
130+
expect(fn.success).toBe(true);
131+
expect(fn.code).toContain('_IA.gamma');
132+
});
133+
134+
test('compiles GammaLn', () => {
135+
const expr = ce.box(['GammaLn', 'x']);
136+
const fn = compile(expr, { to: 'interval-js' });
137+
expect(fn.success).toBe(true);
138+
expect(fn.code).toContain('_IA.gammaln');
139+
});
126140
});
127141

128142
describe('INTERVAL JS EXECUTION', () => {
@@ -421,4 +435,35 @@ describe('INTERVAL JS COMPLEX EXPRESSIONS', () => {
421435
expect(result.value.lo).toBeCloseTo(Math.exp(-1), 5);
422436
expect(result.value.hi).toBeCloseTo(1, 5);
423437
});
438+
439+
test('Gamma function positive values', () => {
440+
const expr = ce.parse('\\Gamma(x)');
441+
const fn = compile(expr, { to: 'interval-js' });
442+
443+
// Gamma(2.5) ≈ 1.329
444+
const result = fn.run!({ x: { lo: 2.5, hi: 2.5 } });
445+
expect(result.kind).toBe('interval');
446+
expect(result.value.lo).toBeCloseTo(1.329, 2);
447+
expect(result.value.hi).toBeCloseTo(1.329, 2);
448+
});
449+
450+
test('Gamma function detects singularity at zero', () => {
451+
const expr = ce.parse('\\Gamma(x)');
452+
const fn = compile(expr, { to: 'interval-js' });
453+
454+
// Interval crossing zero should detect the pole
455+
const result = fn.run!({ x: { lo: -0.5, hi: 0.5 } });
456+
expect(result.kind).toBe('singular');
457+
});
458+
459+
test('GammaLn function', () => {
460+
const expr = ce.box(['GammaLn', 'x']);
461+
const fn = compile(expr, { to: 'interval-js' });
462+
463+
// GammaLn(2.5) ≈ ln(1.329) ≈ 0.284
464+
const result = fn.run!({ x: { lo: 2.5, hi: 2.5 } });
465+
expect(result.kind).toBe('interval');
466+
expect(result.value.lo).toBeCloseTo(0.284, 2);
467+
expect(result.value.hi).toBeCloseTo(0.284, 2);
468+
});
424469
});

0 commit comments

Comments
 (0)