Skip to content

Commit 4674de8

Browse files
committed
feat: implement declarative sequence definitions with recurrence relations
1 parent d191320 commit 4674de8

6 files changed

Lines changed: 861 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,41 @@
109109
This enables important mathematical simplifications that depend on knowing
110110
whether a variable is positive, negative, or zero.
111111

112+
- **Declarative Sequence Definitions**: Define mathematical sequences using
113+
recurrence relations with the new `declareSequence()` method:
114+
115+
```javascript
116+
// Fibonacci sequence
117+
ce.declareSequence('F', {
118+
base: { 0: 0, 1: 1 },
119+
recurrence: 'F_{n-1} + F_{n-2}',
120+
});
121+
ce.parse('F_{10}').evaluate(); // → 55
122+
ce.parse('F_{20}').evaluate(); // → 6765
123+
124+
// Arithmetic sequence: a_n = a_{n-1} + 2, a_0 = 1
125+
ce.declareSequence('A', {
126+
base: { 0: 1 },
127+
recurrence: 'A_{n-1} + 2',
128+
});
129+
ce.parse('A_{5}').evaluate(); // → 11
130+
131+
// Factorial via recurrence
132+
ce.declareSequence('H', {
133+
base: { 0: 1 },
134+
recurrence: 'n \\cdot H_{n-1}',
135+
});
136+
ce.parse('H_{5}').evaluate(); // → 120
137+
```
138+
139+
Features:
140+
- Base cases as index → value mapping
141+
- Recurrence relation as LaTeX string or BoxedExpression
142+
- Automatic memoization for efficient evaluation (configurable)
143+
- Custom index variable name (default: `n`)
144+
- Domain constraints (min/max valid indices)
145+
- Symbolic subscripts stay symbolic (e.g., `F_k` remains unevaluated)
146+
112147
- **Multi-Argument Function Derivatives**: Added derivative support for:
113148

114149
- **Log(x, base)** - Logarithm with custom base:

requirements/SUBSCRIPT.md

Lines changed: 325 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,23 @@ ce.declare('fib', {
259259
- `src/compute-engine/latex-syntax/parse-symbol.ts` - check symbol type at parse time
260260
- `src/compute-engine/library/core.ts` - canonical handler for complex subscripts
261261

262-
3. **Phase 3**: Add evaluation support for subscripted symbols
263-
- Allow defining subscript evaluation functions
264-
- Support sequence definitions
262+
3. **Phase 3**: Add evaluation support for subscripted symbols ✅ IMPLEMENTED
263+
- Added `subscriptEvaluate` handler to `ValueDefinition`
264+
- Supports both simple (`F_5`) and complex (`F_{5}`) subscript syntax
265+
- Handler receives evaluated subscript and returns result (or `undefined` for symbolic)
266+
- Subscripted expressions with `subscriptEvaluate` have type `number` for arithmetic
267+
- Changes in:
268+
- `src/compute-engine/global-types.ts` - added `subscriptEvaluate` to ValueDefinition
269+
- `src/compute-engine/boxed-expression/boxed-value-definition.ts` - store handler
270+
- `src/compute-engine/library/core.ts` - evaluate and type handlers for Subscript
271+
- `src/compute-engine/latex-syntax/parse-symbol.ts` - prevent compound symbol creation
272+
- `src/compute-engine/latex-syntax/types.ts` and `parse.ts` - parser callback
273+
- `src/compute-engine/index.ts` - hasSubscriptEvaluate option
274+
275+
4. **Phase 4**: Declarative sequence definitions (SUB-4) 🔲 PLANNED
276+
- Allow defining sequences using LaTeX recurrence relations
277+
- Support base cases and recurrence rules
278+
- See [Declarative Sequence Definitions](#declarative-sequence-definitions-sub-4) below
265279

266280
### Open Questions
267281

@@ -327,3 +341,311 @@ disambiguate:
327341
The immediate fix is to change the return type of `Subscript` from `'symbol'` to
328342
`'unknown'` for complex subscripts, allowing them to be used in arithmetic
329343
contexts. The longer-term solution is full type-aware subscript interpretation.
344+
345+
---
346+
347+
## Declarative Sequence Definitions (SUB-4)
348+
349+
### Motivation
350+
351+
While `subscriptEvaluate` allows defining sequences programmatically with
352+
JavaScript, mathematicians often define sequences using recurrence relations:
353+
354+
```latex
355+
a_n = a_{n-1} + 1, \quad a_0 = 1
356+
F_n = F_{n-1} + F_{n-2}, \quad F_0 = 0, F_1 = 1
357+
```
358+
359+
A declarative API would make it easier to define sequences using familiar
360+
mathematical notation.
361+
362+
### Proposed API
363+
364+
#### Option 1: Object-Based Declaration
365+
366+
```typescript
367+
ce.declareSequence('a', {
368+
base: { 0: 1 }, // a_0 = 1
369+
recurrence: 'a_{n-1} + 1', // a_n = a_{n-1} + 1
370+
});
371+
372+
ce.declareSequence('F', {
373+
base: { 0: 0, 1: 1 }, // F_0 = 0, F_1 = 1
374+
recurrence: 'F_{n-1} + F_{n-2}', // F_n = F_{n-1} + F_{n-2}
375+
});
376+
377+
// Usage
378+
ce.parse('a_{10}').evaluate(); // → 11
379+
ce.parse('F_{10}').evaluate(); // → 55
380+
```
381+
382+
#### Option 2: LaTeX-Based Declaration
383+
384+
```typescript
385+
// Using assignment notation
386+
ce.parse('a_0 := 1').evaluate();
387+
ce.parse('a_n := a_{n-1} + 1').evaluate();
388+
389+
// Or using a sequence definition function
390+
ce.parse('\\operatorname{DefineSequence}(F, n, F_{n-1} + F_{n-2}, \\{0: 0, 1: 1\\})').evaluate();
391+
```
392+
393+
#### Option 3: Combined Approach (Recommended)
394+
395+
```typescript
396+
ce.declareSequence('a', {
397+
variable: 'n', // Index variable (default: 'n')
398+
base: { 0: 1 }, // Base cases as object
399+
recurrence: ce.parse('a_{n-1} + 1'), // Recurrence as expression
400+
// OR
401+
recurrence: 'a_{n-1} + 1', // Recurrence as LaTeX string
402+
});
403+
```
404+
405+
### Implementation Plan
406+
407+
#### 1. Add `declareSequence` Method to ComputeEngine
408+
409+
**File:** `src/compute-engine/index.ts`
410+
411+
```typescript
412+
declareSequence(
413+
name: string,
414+
options: {
415+
variable?: string; // Index variable name, default 'n'
416+
base: Record<number, number | BoxedExpression>;
417+
recurrence: string | BoxedExpression;
418+
memoize?: boolean; // Default true
419+
domain?: { min?: number; max?: number }; // Valid index range
420+
}
421+
): void
422+
```
423+
424+
#### 2. Parse Recurrence Expression
425+
426+
The recurrence expression needs to:
427+
- Identify self-references (e.g., `a_{n-1}`, `a_{n-2}`)
428+
- Extract the offset from each self-reference
429+
- Handle multiple self-references (Fibonacci-style)
430+
431+
```typescript
432+
function parseRecurrence(
433+
ce: ComputeEngine,
434+
name: string,
435+
variable: string,
436+
expr: BoxedExpression
437+
): {
438+
offsets: number[]; // e.g., [-1, -2] for Fibonacci
439+
evaluate: (n: number, memo: Map<number, number>) => number;
440+
}
441+
```
442+
443+
#### 3. Generate subscriptEvaluate Handler
444+
445+
Convert the parsed recurrence into a `subscriptEvaluate` handler:
446+
447+
```typescript
448+
function createSequenceHandler(
449+
ce: ComputeEngine,
450+
base: Record<number, number>,
451+
recurrence: BoxedExpression,
452+
variable: string,
453+
memoize: boolean
454+
): SubscriptEvaluateHandler {
455+
const memo = memoize ? new Map<number, BoxedExpression>() : null;
456+
457+
return (subscript, { engine }) => {
458+
const n = subscript.re;
459+
if (!Number.isInteger(n)) return undefined;
460+
461+
// Check base cases
462+
if (n in base) return engine.number(base[n]);
463+
464+
// Check memo
465+
if (memo?.has(n)) return memo.get(n);
466+
467+
// Evaluate recurrence by substituting n
468+
const result = evaluateRecurrence(engine, recurrence, variable, n, memo);
469+
470+
if (memo && result) memo.set(n, result);
471+
return result;
472+
};
473+
}
474+
```
475+
476+
#### 4. Handle Self-References in Evaluation
477+
478+
When evaluating the recurrence, self-references like `a_{n-1}` need to
479+
recursively call the sequence:
480+
481+
```typescript
482+
function evaluateRecurrence(
483+
ce: ComputeEngine,
484+
expr: BoxedExpression,
485+
variable: string,
486+
n: number,
487+
memo: Map<number, BoxedExpression> | null
488+
): BoxedExpression | undefined {
489+
// Substitute the variable with the current index
490+
const substituted = expr.subs({ [variable]: ce.number(n) });
491+
492+
// The substituted expression may contain Subscript(name, n-1) etc.
493+
// These will evaluate via the subscriptEvaluate handler (recursive)
494+
return substituted.evaluate();
495+
}
496+
```
497+
498+
### Edge Cases and Validation
499+
500+
#### 1. Detect Invalid Recurrences
501+
502+
- **Missing base cases**: If recurrence references `a_{n-1}` but no base case
503+
for `a_0` is provided
504+
- **Circular references**: `a_n = a_n + 1` (infinite loop)
505+
- **Non-convergent**: Ensure recurrence terminates
506+
507+
```typescript
508+
function validateRecurrence(
509+
offsets: number[],
510+
base: Record<number, number>
511+
): { valid: boolean; error?: string } {
512+
// Check that base cases cover all needed starting points
513+
const minOffset = Math.min(...offsets);
514+
for (let i = 0; i > minOffset; i--) {
515+
if (!(i in base)) {
516+
return { valid: false, error: `Missing base case for index ${i}` };
517+
}
518+
}
519+
return { valid: true };
520+
}
521+
```
522+
523+
#### 2. Handle Non-Integer Subscripts
524+
525+
Return `undefined` for non-integer subscripts to keep expression symbolic.
526+
527+
#### 3. Handle Negative Indices
528+
529+
Option to support negative indices for bi-directional sequences:
530+
531+
```typescript
532+
ce.declareSequence('a', {
533+
base: { 0: 1 },
534+
recurrence: 'a_{n-1} + 1',
535+
domain: { min: 0 }, // Only valid for n >= 0
536+
});
537+
```
538+
539+
### Multi-Index Sequences (Matrices, Tensors)
540+
541+
For sequences with multiple indices:
542+
543+
```typescript
544+
ce.declareSequence('P', {
545+
variables: ['n', 'k'], // Pascal's triangle
546+
base: {
547+
'0,0': 1,
548+
'n,0': 1, // P_{n,0} = 1 for all n
549+
'n,n': 1, // P_{n,n} = 1 for all n
550+
},
551+
recurrence: 'P_{n-1,k-1} + P_{n-1,k}',
552+
});
553+
554+
ce.parse('P_{5,2}').evaluate(); // → 10
555+
```
556+
557+
This is more complex and may be Phase 5.
558+
559+
### Closed-Form Detection (Future Enhancement)
560+
561+
For simple recurrences, detect and use closed-form solutions:
562+
563+
| Recurrence | Closed Form |
564+
|------------|-------------|
565+
| `a_n = a_{n-1} + d` | `a_n = a_0 + n*d` (arithmetic) |
566+
| `a_n = r * a_{n-1}` | `a_n = a_0 * r^n` (geometric) |
567+
| `a_n = a_{n-1} + a_{n-2}` | Binet's formula (Fibonacci) |
568+
569+
This optimization would avoid recursion for large indices.
570+
571+
### Test Cases
572+
573+
```typescript
574+
describe('Declarative Sequence Definitions', () => {
575+
test('Arithmetic sequence', () => {
576+
const ce = new ComputeEngine();
577+
ce.declareSequence('a', {
578+
base: { 0: 1 },
579+
recurrence: 'a_{n-1} + 2',
580+
});
581+
expect(ce.parse('a_{5}').evaluate().re).toBe(11); // 1, 3, 5, 7, 9, 11
582+
});
583+
584+
test('Fibonacci sequence', () => {
585+
const ce = new ComputeEngine();
586+
ce.declareSequence('F', {
587+
base: { 0: 0, 1: 1 },
588+
recurrence: 'F_{n-1} + F_{n-2}',
589+
});
590+
expect(ce.parse('F_{10}').evaluate().re).toBe(55);
591+
});
592+
593+
test('Factorial via recurrence', () => {
594+
const ce = new ComputeEngine();
595+
ce.declareSequence('fact', {
596+
base: { 0: 1 },
597+
recurrence: 'n * fact_{n-1}', // Note: uses n directly
598+
});
599+
expect(ce.parse('fact_{5}').evaluate().re).toBe(120);
600+
});
601+
602+
test('Symbolic subscript stays symbolic', () => {
603+
const ce = new ComputeEngine();
604+
ce.declareSequence('a', {
605+
base: { 0: 1 },
606+
recurrence: 'a_{n-1} + 1',
607+
});
608+
const result = ce.parse('a_k').evaluate();
609+
expect(result.operator).toBe('Subscript');
610+
});
611+
612+
test('Missing base case returns undefined', () => {
613+
const ce = new ComputeEngine();
614+
ce.declareSequence('a', {
615+
base: { 1: 1 }, // Missing a_0
616+
recurrence: 'a_{n-1} + 1',
617+
});
618+
expect(ce.parse('a_{0}').evaluate().re).toBe(NaN);
619+
});
620+
621+
test('Arithmetic with sequence values', () => {
622+
const ce = new ComputeEngine();
623+
ce.declareSequence('S', {
624+
base: { 0: 0 },
625+
recurrence: 'S_{n-1} + n', // Triangular numbers
626+
});
627+
expect(ce.parse('S_{5} + S_{3}').evaluate().re).toBe(21); // 15 + 6
628+
});
629+
});
630+
```
631+
632+
### Files to Modify
633+
634+
| File | Change |
635+
|------|--------|
636+
| `src/compute-engine/global-types.ts` | Add `SequenceDefinition` type |
637+
| `src/compute-engine/index.ts` | Add `declareSequence()` method |
638+
| `src/compute-engine/sequence.ts` | New file for sequence parsing/evaluation |
639+
| `test/compute-engine/sequences.test.ts` | New test file |
640+
| `doc/06-guide-augmenting.md` | Document `declareSequence()` |
641+
642+
### Summary
643+
644+
Phase 4 adds a declarative way to define sequences using recurrence relations.
645+
The implementation:
646+
647+
1. Parses the recurrence expression to identify self-references
648+
2. Validates that base cases cover all required starting points
649+
3. Generates a `subscriptEvaluate` handler with memoization
650+
4. Supports symbolic subscripts (returns undefined → stays symbolic)
651+
5. Integrates with arithmetic (type is `number`)

0 commit comments

Comments
 (0)