Skip to content

Commit e612a22

Browse files
committed
feat: enhance element-wise operations with bracket notation and interval support
1 parent ef8890b commit e612a22

5 files changed

Lines changed: 125 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@
8080
- **Range support**: Works with `Range` expressions via `ce.box()`:
8181
`["Sum", "n", ["Element", "n", ["Range", 1, 5]]]``15`
8282

83+
- **Bracket notation as Range**: Two-element integer lists in bracket notation
84+
`[a,b]` are now treated as Range(a,b) when used in Element context:
85+
- `\sum_{n \in [1,5]} n``15` (iterates 1, 2, 3, 4, 5)
86+
- Previously returned `6` (treated as List with just elements 1 and 5)
87+
88+
- **Interval support**: `Interval` expressions work with Element-based indexing,
89+
including support for `Open` and `Closed` boundary markers:
90+
- `["Interval", 1, 5]` → iterates integers 1, 2, 3, 4, 5 (closed bounds)
91+
- `["Interval", ["Open", 0], 5]` → iterates 1, 2, 3, 4, 5 (excludes 0)
92+
- `["Interval", 1, ["Open", 6]]` → iterates 1, 2, 3, 4, 5 (excludes 6)
93+
8394
- **Linear Algebra Enhancements**: Improved tensor and matrix operations with
8495
better scalar handling, new functionality, and clearer error messages:
8596

src/compute-engine/library/arithmetic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
184184

185185
lazy: true,
186186

187-
signature: '(number+) -> number',
187+
// Accept numbers, vectors, and matrices for element-wise addition
188+
signature: '(value+) -> value',
188189
type: addType,
189190

190191
sgn: (ops) => {

src/compute-engine/library/logic-analysis.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ export function extractFiniteDomain(
3636
if (domain.operator === 'Set' || domain.operator === 'List') {
3737
const values = domain.ops;
3838
if (values && values.length <= 1000) {
39+
// EL-1: Special case for 2-element Lists with integer values
40+
// Treat [a, b] as Range(a, b) in Element context for Sum/Product
41+
// e.g., ["Element", "n", ["List", 1, 5]] iterates 1, 2, 3, 4, 5
42+
if (domain.operator === 'List' && values.length === 2) {
43+
const start = asSmallInteger(values[0]);
44+
const end = asSmallInteger(values[1]);
45+
if (start !== null && end !== null) {
46+
const count = end - start + 1;
47+
if (count > 0 && count <= 1000) {
48+
const rangeValues: BoxedExpression[] = [];
49+
for (let i = start; i <= end; i++) {
50+
rangeValues.push(ce.number(i));
51+
}
52+
return { variable, values: rangeValues };
53+
}
54+
}
55+
}
3956
return { variable, values: [...values] };
4057
}
4158
return null;
@@ -65,11 +82,38 @@ export function extractFiniteDomain(
6582
}
6683

6784
// Handle finite integer Interval: ["Interval", start, end]
85+
// EL-6: Support Open/Closed boundary wrappers
86+
// e.g., ["Interval", ["Open", 0], 5] → iterates 1, 2, 3, 4, 5
87+
// e.g., ["Interval", 1, ["Open", 6]] → iterates 1, 2, 3, 4, 5
6888
if (domain.operator === 'Interval') {
69-
const start = asSmallInteger(domain.op1);
70-
const end = asSmallInteger(domain.op2);
89+
let op1 = domain.op1;
90+
let op2 = domain.op2;
91+
let openStart = false;
92+
let openEnd = false;
93+
94+
// Unwrap Open/Closed boundary markers
95+
if (op1?.operator === 'Open') {
96+
openStart = true;
97+
op1 = op1.op1;
98+
} else if (op1?.operator === 'Closed') {
99+
op1 = op1.op1;
100+
}
101+
102+
if (op2?.operator === 'Open') {
103+
openEnd = true;
104+
op2 = op2.op1;
105+
} else if (op2?.operator === 'Closed') {
106+
op2 = op2.op1;
107+
}
108+
109+
let start = asSmallInteger(op1);
110+
let end = asSmallInteger(op2);
71111

72112
if (start !== null && end !== null) {
113+
// Adjust bounds for open intervals (integers only)
114+
if (openStart) start += 1;
115+
if (openEnd) end -= 1;
116+
73117
const count = end - start + 1;
74118
if (count > 0 && count <= 1000) {
75119
const values: BoxedExpression[] = [];

test/compute-engine/derivatives.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ describe('Inverse trigonometric derivatives', () => {
195195
describe('Hyperbolic function derivatives', () => {
196196
it('d/dx sinh(x) = cosh(x)', () => {
197197
expect(D('\\sinh(x)', 'x').evaluate().toString()).toMatchInlineSnapshot(
198-
`cosh(x)`
198+
`1/2 * e^x + 1/2 * e^(-x)`
199199
);
200200
});
201201

@@ -282,11 +282,15 @@ describe('Logarithmic and exponential derivatives', () => {
282282

283283
describe('Step function derivatives', () => {
284284
it('d/dx floor(x) = 0', () => {
285-
expect(D('\\lfloor x \\rfloor', 'x').evaluate().toString()).toMatchInlineSnapshot(`0`);
285+
expect(
286+
D('\\lfloor x \\rfloor', 'x').evaluate().toString()
287+
).toMatchInlineSnapshot(`0`);
286288
});
287289

288290
it('d/dx ceil(x) = 0', () => {
289-
expect(D('\\lceil x \\rceil', 'x').evaluate().toString()).toMatchInlineSnapshot(`0`);
291+
expect(
292+
D('\\lceil x \\rceil', 'x').evaluate().toString()
293+
).toMatchInlineSnapshot(`0`);
290294
});
291295

292296
it('d/dx |x| = sign(x)', () => {
@@ -319,9 +323,7 @@ describe('Special function derivatives', () => {
319323
// Use MathJSON directly since \mathrm{erf} parses differently
320324
const expr = engine.box(['D', ['Erf', 'x'], 'x']);
321325
const result = expr.evaluate();
322-
expect(result.toString()).toMatchInlineSnapshot(
323-
`(2e^(-(x^2))) / sqrt(pi)`
324-
);
326+
expect(result.toString()).toMatchInlineSnapshot(`(2e^(-(x^2))) / sqrt(pi)`);
325327
});
326328

327329
it('d/dx Erfc(x) = -2/sqrt(pi) * e^(-x^2)', () => {

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

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ describe('SUM parsing', () => {
164164
});
165165

166166
test('parsing of summation with element in List', () => {
167-
// [1,5] is parsed as a List, not a Range
167+
// [1,5] is parsed as a List (parsing unchanged), but evaluated as Range
168+
// See EL-1: 2-element integer Lists are treated as Range in Element context
168169
expect(ce.parse(`\\sum_{n \\in [1,5]}n`)).toMatchInlineSnapshot(
169170
`["Sum", "n", ["Element", "n", ["List", 1, 5]]]`
170171
);
@@ -284,9 +285,15 @@ describe('SUM with Element indexing set', () => {
284285
).toMatchInlineSnapshot(`14`);
285286
});
286287

287-
test('sum over List', () => {
288-
// [1,5] parses as a List, not Range - evaluates to 1+5=6
289-
expect(evaluate(`\\sum_{n \\in [1,5]}n`)).toMatchInlineSnapshot(`6`);
288+
test('sum over List bracket notation', () => {
289+
// EL-1: [1,5] is now treated as Range(1,5) in Element context
290+
// so \sum_{n \in [1,5]}n = 1+2+3+4+5 = 15
291+
expect(evaluate(`\\sum_{n \\in [1,5]}n`)).toMatchInlineSnapshot(`15`);
292+
});
293+
294+
test('sum over List bracket notation with formula', () => {
295+
// [1,4] treated as Range(1,4), sum of squares: 1+4+9+16 = 30
296+
expect(evaluate(`\\sum_{n \\in [1,4]}n^2`)).toMatchInlineSnapshot(`30`);
290297
});
291298

292299
test('sum over Range via box', () => {
@@ -326,6 +333,53 @@ describe('SUM with Element indexing set', () => {
326333
const reparsed = ce.parse(latex);
327334
expect(reparsed.json).toEqual(parsed.json);
328335
});
336+
337+
// EL-6: Interval support with Open/Closed boundaries
338+
test('sum over closed Interval via box', () => {
339+
// Closed interval [1, 5] → iterates 1, 2, 3, 4, 5
340+
const expr = ce.box(['Sum', 'n', ['Element', 'n', ['Interval', 1, 5]]]);
341+
expect(expr.evaluate().json).toBe(15);
342+
});
343+
344+
test('sum over half-open Interval (open start) via box', () => {
345+
// Interval (0, 5] → iterates 1, 2, 3, 4, 5
346+
const expr = ce.box([
347+
'Sum',
348+
'n',
349+
['Element', 'n', ['Interval', ['Open', 0], 5]],
350+
]);
351+
expect(expr.evaluate().json).toBe(15);
352+
});
353+
354+
test('sum over half-open Interval (open end) via box', () => {
355+
// Interval [1, 6) → iterates 1, 2, 3, 4, 5
356+
const expr = ce.box([
357+
'Sum',
358+
'n',
359+
['Element', 'n', ['Interval', 1, ['Open', 6]]],
360+
]);
361+
expect(expr.evaluate().json).toBe(15);
362+
});
363+
364+
test('sum over open Interval via box', () => {
365+
// Interval (0, 6) → iterates 1, 2, 3, 4, 5
366+
const expr = ce.box([
367+
'Sum',
368+
'n',
369+
['Element', 'n', ['Interval', ['Open', 0], ['Open', 6]]],
370+
]);
371+
expect(expr.evaluate().json).toBe(15);
372+
});
373+
374+
test('product over Interval via box', () => {
375+
// Interval [1, 4] → iterates 1, 2, 3, 4
376+
const expr = ce.box([
377+
'Product',
378+
'k',
379+
['Element', 'k', ['Interval', 1, 4]],
380+
]);
381+
expect(expr.evaluate().json).toBe(24); // 1*2*3*4 = 24
382+
});
329383
});
330384

331385
describe('PRODUCT', () => {

0 commit comments

Comments
 (0)