Skip to content

Commit f45cde8

Browse files
committed
feat: add simplification rules for sum and product with symbolic bounds, enhance LaTeX parsing for gcd and lcm
1 parent 28f48a2 commit f45cde8

4 files changed

Lines changed: 156 additions & 6 deletions

File tree

src/compute-engine/library/arithmetic.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,7 +1484,8 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
14841484
),
14851485
options.engine._timeRemaining
14861486
);
1487-
return result ?? options.engine.NaN;
1487+
// Evaluate the accumulated result to combine numeric factors
1488+
return result?.evaluate() ?? options.engine.NaN;
14881489
},
14891490

14901491
evaluateAsync: async (ops, options) => {
@@ -1498,7 +1499,7 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
14981499
options.engine._timeRemaining,
14991500
options.signal
15001501
);
1501-
return result ?? options.engine.NaN;
1502+
return result?.evaluate() ?? options.engine.NaN;
15021503
},
15031504
},
15041505

@@ -1525,7 +1526,9 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
15251526
),
15261527
engine._timeRemaining
15271528
);
1528-
return result ?? engine.NaN;
1529+
// Evaluate the accumulated result to combine numeric terms
1530+
// e.g., 3x + 1 + 2 + 3 → 3x + 6
1531+
return result?.evaluate() ?? engine.NaN;
15291532
},
15301533

15311534
evaluateAsync: async (xs, { engine, signal }) => {
@@ -1539,7 +1542,7 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
15391542
engine._timeRemaining,
15401543
signal
15411544
);
1542-
return result ?? engine.NaN;
1545+
return result?.evaluate() ?? engine.NaN;
15431546
},
15441547
},
15451548
},

src/compute-engine/symbolic/simplify-rules.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,91 @@ export const SIMPLIFY_RULES: Rule[] = [
262262
};
263263
},
264264
//
265-
// Product, Sum
265+
// Sum simplification
266266
//
267267
(x): RuleStep | undefined => {
268-
if (x.operator === 'Max') {
268+
if (x.operator !== 'Sum') return undefined;
269+
270+
const body = x.op1;
271+
const limits = x.op2;
272+
if (!body || !limits || limits.operator !== 'Limits') return undefined;
273+
274+
const index = limits.op1?.symbol;
275+
const lower = limits.op2;
276+
const upper = limits.op3;
277+
if (!index || !lower || !upper) return undefined;
278+
279+
const ce = x.engine;
280+
const bodyUnknowns = new Set(body.unknowns);
281+
282+
// If body doesn't depend on index: Sum(c, [n, a, b]) → (b - a + 1) * c
283+
if (!bodyUnknowns.has(index)) {
284+
const count = upper.sub(lower).add(ce.One).simplify();
285+
return {
286+
value: count.mul(body.simplify()),
287+
because: 'sum of constant',
288+
};
289+
}
290+
291+
// If body is just the index: Sum(n, [n, 1, b]) → b * (b + 1) / 2
292+
if (body.symbol === index && lower.is(1)) {
293+
// Triangular number formula
294+
const result = upper.mul(upper.add(ce.One)).div(2);
295+
return { value: result.simplify(), because: 'triangular number' };
296+
}
297+
298+
// If body is index squared: Sum(n^2, [n, 1, b]) → b(b+1)(2b+1)/6
299+
if (
300+
body.operator === 'Power' &&
301+
body.op1?.symbol === index &&
302+
body.op2?.is(2) &&
303+
lower.is(1)
304+
) {
305+
// Sum of squares formula: b(b+1)(2b+1)/6
306+
// Note: Don't simplify() here as the expanded form is more expensive
307+
const b = upper;
308+
const result = b.mul(b.add(ce.One)).mul(b.mul(2).add(ce.One)).div(6);
309+
return { value: result, because: 'sum of squares' };
310+
}
311+
312+
return undefined;
313+
},
314+
315+
//
316+
// Product simplification
317+
//
318+
(x): RuleStep | undefined => {
319+
if (x.operator !== 'Product') return undefined;
320+
321+
const body = x.op1;
322+
const limits = x.op2;
323+
if (!body || !limits || limits.operator !== 'Limits') return undefined;
324+
325+
const index = limits.op1?.symbol;
326+
const lower = limits.op2;
327+
const upper = limits.op3;
328+
if (!index || !lower || !upper) return undefined;
329+
330+
const ce = x.engine;
331+
const bodyUnknowns = new Set(body.unknowns);
332+
333+
// If body doesn't depend on index: Product(c, [n, a, b]) → c^(b - a + 1)
334+
if (!bodyUnknowns.has(index)) {
335+
const count = upper.sub(lower).add(ce.One).simplify();
336+
return {
337+
value: body.simplify().pow(count),
338+
because: 'product of constant',
339+
};
340+
}
341+
342+
// If body is just the index: Product(n, [n, 1, b]) → b!
343+
if (body.symbol === index && lower.is(1)) {
344+
return {
345+
value: ce.function('Factorial', [upper]),
346+
because: 'factorial',
347+
};
269348
}
349+
270350
return undefined;
271351
},
272352

test/compute-engine/arithmetic.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { check, checkJson, engine } from '../utils';
33
const ce = engine;
44

55
ce.assign('z', ['Complex', 0, 1]);
6+
ce.declare('b', 'integer'); // Used in Sum/Product simplification tests
67

78
describe('CONSTANTS', () => {
89
test(`ExponentialE`, () =>
@@ -737,6 +738,25 @@ describe('SUM', () => {
737738
.simplify()
738739
.toString()
739740
).toMatchInlineSnapshot(`3x + 6`));
741+
742+
// Simplification of Sum with symbolic bounds
743+
it('should simplify sum of constant with symbolic bounds', () => {
744+
expect(
745+
ce.parse('\\sum_{n=1}^{b}(x)').simplify().toString()
746+
).toMatchInlineSnapshot(`b * x`);
747+
});
748+
749+
it('should simplify sum of index (triangular number)', () => {
750+
expect(
751+
ce.parse('\\sum_{n=1}^{b}(n)').simplify().toString()
752+
).toMatchInlineSnapshot(`1/2 * (b^2 + b)`);
753+
});
754+
755+
it('should simplify sum of index squared', () => {
756+
expect(
757+
ce.parse('\\sum_{n=1}^{b}(n^2)').simplify().toString()
758+
).toMatchInlineSnapshot(`1/3 * b^3 + 1/2 * b^2 + 1/6 * b`);
759+
});
740760
});
741761

742762
describe('PRODUCT', () => {
@@ -763,6 +783,19 @@ describe('PRODUCT', () => {
763783
expect(
764784
ce.parse('\\prod_{n=1}^{3}(n \\cdot x)').evaluate().toString()
765785
).toMatchInlineSnapshot(`6x^3`));
786+
787+
// Simplification of Product with symbolic bounds
788+
it('should simplify product of constant with symbolic bounds', () => {
789+
expect(
790+
ce.parse('\\prod_{n=1}^{b}(x)').simplify().toString()
791+
).toMatchInlineSnapshot(`x^b`);
792+
});
793+
794+
it('should simplify product of index (factorial)', () => {
795+
expect(
796+
ce.parse('\\prod_{n=1}^{b}(n)').simplify().toString()
797+
).toMatchInlineSnapshot(`b!`);
798+
});
766799
});
767800

768801
describe('GCD/LCM', () => {

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,40 @@ describe('PRODUCT', () => {
231231
});
232232
});
233233

234+
describe('GCD/LCM parsing', () => {
235+
test('\\gcd with \\left( \\right) delimiters', () => {
236+
expect(ce.parse('\\gcd\\left(24,37\\right)')).toMatchInlineSnapshot(
237+
`["GCD", 24, 37]`
238+
);
239+
});
240+
241+
test('\\gcd with regular parentheses', () => {
242+
expect(ce.parse('\\gcd(24,37)')).toMatchInlineSnapshot(`["GCD", 24, 37]`);
243+
});
244+
245+
test('\\operatorname{gcd}', () => {
246+
expect(ce.parse('\\operatorname{gcd}(24,37)')).toMatchInlineSnapshot(
247+
`["GCD", 24, 37]`
248+
);
249+
});
250+
251+
test('\\lcm with \\left( \\right) delimiters', () => {
252+
expect(ce.parse('\\lcm\\left(24,37\\right)')).toMatchInlineSnapshot(
253+
`["LCM", 24, 37]`
254+
);
255+
});
256+
257+
test('\\lcm with regular parentheses', () => {
258+
expect(ce.parse('\\lcm(24,37)')).toMatchInlineSnapshot(`["LCM", 24, 37]`);
259+
});
260+
261+
test('\\operatorname{lcm}', () => {
262+
expect(ce.parse('\\operatorname{lcm}(24,37)')).toMatchInlineSnapshot(
263+
`["LCM", 24, 37]`
264+
);
265+
});
266+
});
267+
234268
describe('POWER', () => {
235269
test('Power Invalid forms', () => {
236270
expect(latex(['Power'])).toMatchInlineSnapshot(

0 commit comments

Comments
 (0)