Skip to content

Commit ad73ae7

Browse files
committed
feat: implement conversion of known infinite integer sets to Limits form and add corresponding tests
1 parent b3faa76 commit ad73ae7

3 files changed

Lines changed: 113 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,19 @@
9191
- `["Interval", ["Open", 0], 5]` → iterates 1, 2, 3, 4, 5 (excludes 0)
9292
- `["Interval", 1, ["Open", 6]]` → iterates 1, 2, 3, 4, 5 (excludes 6)
9393

94+
- **Infinite series with Element notation**: Known infinite integer sets are
95+
converted to their equivalent Limits form and iterated (capped at 1,000,000):
96+
- `NonNegativeIntegers` (ℕ₀) → iterates from 0, like `\sum_{n=0}^{\infty}`
97+
- `PositiveIntegers` (ℤ⁺) → iterates from 1, like `\sum_{n=1}^{\infty}`
98+
- Convergent series produce numeric approximations:
99+
`\sum_{n \in \Z^+} \frac{1}{n^2}``≈1.6449` (close to π²/6)
100+
94101
- **Non-enumerable domains stay symbolic**: When the domain cannot be enumerated
95-
(unknown symbol, infinite set, or symbolic bounds), the expression now stays
96-
symbolic instead of returning NaN:
102+
(unknown symbol, non-iterable infinite set, or symbolic bounds), the expression
103+
stays symbolic instead of returning NaN:
97104
- `\sum_{n \in S} n` with unknown `S` → stays as `["Sum", "n", ["Element", "n", "S"]]`
98-
- `\sum_{n \in \N} n` with infinite set → stays symbolic
105+
- `\sum_{n \in \Z} n` → stays symbolic (bidirectional, can't forward iterate)
106+
- `\sum_{x \in \R} f(x)` → stays symbolic (non-countable)
99107
- `\sum_{n \in [1,a]} n` with symbolic bound → stays symbolic
100108
- Previously these would all return `NaN` with no explanation
101109

src/compute-engine/library/utils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@ import { MAX_ITERATION } from '../numerics/numeric';
44
import { fromRange, reduceCollection } from './collections';
55
import { extractFiniteDomainWithReason, ExtractDomainResult } from './logic-analysis';
66

7+
/**
8+
* EL-4: Convert known infinite integer sets to their equivalent Limits bounds.
9+
* Returns undefined if the set cannot be converted to a Limits form.
10+
*
11+
* Mappings:
12+
* - NonNegativeIntegers (ℕ₀) → [0, ∞)
13+
* - PositiveIntegers (ℤ⁺) → [1, ∞)
14+
* - NegativeIntegers (ℤ⁻) → Not supported (would need negative direction)
15+
* - Integers (ℤ) → Not supported (bidirectional)
16+
* - Other sets (Reals, Complexes, etc.) → Not supported (non-integer)
17+
*/
18+
export function convertInfiniteSetToLimits(
19+
domainSymbol: string
20+
): { lower: number; upper: number; isFinite: false } | undefined {
21+
switch (domainSymbol) {
22+
case 'NonNegativeIntegers':
23+
// ℕ₀ = {0, 1, 2, 3, ...}
24+
return { lower: 0, upper: MAX_ITERATION, isFinite: false };
25+
case 'PositiveIntegers':
26+
// ℤ⁺ = {1, 2, 3, ...}
27+
return { lower: 1, upper: 1 + MAX_ITERATION, isFinite: false };
28+
default:
29+
// NegativeIntegers, Integers, Reals, Complexes, etc. cannot be
30+
// converted to a simple forward iteration
31+
return undefined;
32+
}
33+
}
34+
735
export type IndexingSet = {
836
index: string | undefined;
937
lower: number;
@@ -476,6 +504,23 @@ function* reduceElementIndexingSets<T>(
476504
}
477505

478506
if (domainResult.status === 'non-enumerable') {
507+
// EL-4: Check if this is a known infinite integer set that can be
508+
// converted to Limits form for iteration
509+
if (
510+
domainResult.reason === 'infinite-domain' &&
511+
domainResult.domain?.symbol
512+
) {
513+
const limits = convertInfiniteSetToLimits(domainResult.domain.symbol);
514+
if (limits) {
515+
// Convert to Limits and continue with iteration
516+
limitsSets.push({
517+
index: domainResult.variable,
518+
...limits,
519+
});
520+
continue; // Process next index, don't return early
521+
}
522+
}
523+
479524
// Domain exists but cannot be enumerated - keep expression symbolic
480525
if (returnReason) {
481526
return {

test/compute-engine/latex-syntax/arithmetic.test.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -402,35 +402,31 @@ describe('EL-5: Non-enumerable domains stay symbolic', () => {
402402
expect(result.isNaN).not.toBe(true);
403403
});
404404

405-
test('sum over NonNegativeIntegers (infinite set) stays symbolic', () => {
406-
const expr = ce.box(['Sum', 'n', ['Element', 'n', 'NonNegativeIntegers']]);
407-
const result = expr.evaluate();
408-
expect(result.operator).toBe('Sum');
409-
expect(result.isNaN).not.toBe(true);
410-
});
405+
// EL-4: NonNegativeIntegers and PositiveIntegers can be converted to Limits
406+
// and will iterate (capped at MAX_ITERATION), so they evaluate to numbers
407+
// Other infinite sets (Integers, Reals) that can't be iterated stay symbolic
411408

412409
test('sum over Integers (infinite set) stays symbolic', () => {
410+
// Integers is bidirectional, cannot be converted to forward iteration
413411
const expr = ce.box(['Sum', 'n', ['Element', 'n', 'Integers']]);
414412
const result = expr.evaluate();
415413
expect(result.operator).toBe('Sum');
416414
expect(result.isNaN).not.toBe(true);
417415
});
418416

419417
test('sum over Reals (infinite set) stays symbolic', () => {
418+
// Reals is non-countable, cannot be iterated
420419
const expr = ce.box(['Sum', 'x', ['Element', 'x', 'Reals']]);
421420
const result = expr.evaluate();
422421
expect(result.operator).toBe('Sum');
423422
expect(result.isNaN).not.toBe(true);
424423
});
425424

426-
test('product over PositiveIntegers (infinite set) stays symbolic', () => {
427-
const expr = ce.box([
428-
'Product',
429-
'k',
430-
['Element', 'k', 'PositiveIntegers'],
431-
]);
425+
test('sum over NegativeIntegers (infinite set) stays symbolic', () => {
426+
// NegativeIntegers goes in the negative direction, can't be forward iterated
427+
const expr = ce.box(['Sum', 'n', ['Element', 'n', 'NegativeIntegers']]);
432428
const result = expr.evaluate();
433-
expect(result.operator).toBe('Product');
429+
expect(result.operator).toBe('Sum');
434430
expect(result.isNaN).not.toBe(true);
435431
});
436432

@@ -451,6 +447,54 @@ describe('EL-5: Non-enumerable domains stay symbolic', () => {
451447
});
452448
});
453449

450+
describe('EL-4: Infinite series with Element notation', () => {
451+
// EL-4: NonNegativeIntegers and PositiveIntegers are converted to Limits form
452+
// and iterated (capped at MAX_ITERATION), behaving like traditional bounds notation
453+
454+
test('sum over NonNegativeIntegers evaluates (converges to partial sum)', () => {
455+
// Sum n from 0 to MAX_ITERATION - should give a numeric result (triangular number approximation)
456+
// Note: This test verifies the behavior change, not the exact value
457+
const expr = ce.box(['Sum', 'n', ['Element', 'n', 'NonNegativeIntegers']]);
458+
const result = expr.evaluate();
459+
// Should be a Number, not remain as Sum
460+
expect(result.isNumber).toBe(true);
461+
expect(result.operator).not.toBe('Sum');
462+
expect(result.isNaN).not.toBe(true);
463+
}, 15000); // Allow 15 seconds for iteration
464+
465+
test('product over PositiveIntegers evaluates (converges to partial product)', () => {
466+
// Product k from 1 to MAX_ITERATION - should give a numeric result
467+
const expr = ce.box([
468+
'Product',
469+
'k',
470+
['Element', 'k', 'PositiveIntegers'],
471+
]);
472+
const result = expr.evaluate();
473+
// Should be a Number, not remain as Product
474+
expect(result.isNumber).toBe(true);
475+
expect(result.operator).not.toBe('Product');
476+
expect(result.isNaN).not.toBe(true);
477+
}, 15000); // Allow 15 seconds for iteration
478+
479+
test('convergent series over NonNegativeIntegers gives reasonable approximation', () => {
480+
// Sum 1/n^2 from 1 to infinity approaches π²/6 ≈ 1.6449
481+
// Using PositiveIntegers to avoid division by zero
482+
const expr = ce.box([
483+
'Sum',
484+
['Power', 'n', -2],
485+
['Element', 'n', 'PositiveIntegers'],
486+
]);
487+
const result = expr.evaluate();
488+
expect(result.isNumber).toBe(true);
489+
// Should be close to π²/6 ≈ 1.6449
490+
// .re gives us the numeric value as a number
491+
const value = result.re;
492+
expect(typeof value).toBe('number');
493+
expect(value).toBeGreaterThan(1.6);
494+
expect(value).toBeLessThan(1.7);
495+
}, 30000); // Allow 30 seconds for this computation
496+
});
497+
454498
describe('PRODUCT', () => {
455499
test('k is an Integer (as the index) and used a a Number (in the fraction)', () => {
456500
expect(evaluate(`\\prod_{k=1}^{10}\\frac{k}{2}`)).toMatchInlineSnapshot(

0 commit comments

Comments
 (0)