From a5e30234ec1613faffe8c2e166934a22883697b3 Mon Sep 17 00:00:00 2001 From: Emily Traynor Date: Sat, 16 Dec 2023 18:17:41 -0500 Subject: [PATCH 1/2] extended summation functionality to parse a sequence in the sub and super script --- src/compute-engine/library/arithmetic-add.ts | 32 ++++++-- .../library/arithmetic-multiply.ts | 30 +++++-- src/compute-engine/library/utils.ts | 82 +++++++++++++++++-- .../latex-syntax/arithmetic.test.ts | 58 +++++++++++++ 4 files changed, 186 insertions(+), 16 deletions(-) diff --git a/src/compute-engine/library/arithmetic-add.ts b/src/compute-engine/library/arithmetic-add.ts index d764d1e5..e421e0ce 100644 --- a/src/compute-engine/library/arithmetic-add.ts +++ b/src/compute-engine/library/arithmetic-add.ts @@ -7,7 +7,12 @@ import { Sum } from '../symbolic/sum'; import { asBignum, asFloat, MAX_SYMBOLIC_TERMS } from '../numerics/numeric'; import { widen } from '../boxed-expression/boxed-domain'; import { sortAdd } from '../boxed-expression/order'; -import { canonicalIndexingSet, normalizeIndexingSet } from './utils'; +// import { canonicalIndexingSet, normalizeIndexingSet } from './utils'; +import { + MultiIndexingSet, + SingleIndexingSet, + normalizeIndexingSet, +} from './utils'; import { each, isIndexableCollection } from '../collection-utils'; /** The canonical form of `Add`: @@ -121,11 +126,26 @@ export function canonicalSummation( ce.pushScope(); body ??= ce.error('missing'); - - indexingSet = canonicalIndexingSet(indexingSet); - const result = indexingSet - ? ce._fn('Sum', [body.canonical, indexingSet]) - : ce._fn('Sum', [body.canonical]); + var result: BoxedExpression | undefined = undefined; + + if ( + indexingSet && + indexingSet.ops && + indexingSet.ops[0]?.head === 'Delimiter' + ) { + var multiIndex = MultiIndexingSet(indexingSet); + if (!multiIndex) return undefined; + var bodyAndIndex = [body.canonical]; + multiIndex.forEach((element) => { + bodyAndIndex.push(element); + }); + result = ce._fn('Sum', bodyAndIndex); + } else { + var singleIndex = SingleIndexingSet(indexingSet); + result = singleIndex + ? ce._fn('Sum', [body.canonical, singleIndex]) + : ce._fn('Sum', [body.canonical]); + } ce.popScope(); return result; diff --git a/src/compute-engine/library/arithmetic-multiply.ts b/src/compute-engine/library/arithmetic-multiply.ts index a0445d1c..f35525ba 100644 --- a/src/compute-engine/library/arithmetic-multiply.ts +++ b/src/compute-engine/library/arithmetic-multiply.ts @@ -14,7 +14,12 @@ import { neg, } from '../numerics/rationals'; import { apply2N } from '../symbolic/utils'; -import { canonicalIndexingSet, normalizeIndexingSet } from './utils'; +import { + MultiIndexingSet, + SingleIndexingSet, + normalizeIndexingSet, +} from './utils'; +// import { canonicalIndexingSet, normalizeIndexingSet } from './utils'; import { each } from '../collection-utils'; /** The canonical form of `Multiply`: @@ -221,11 +226,26 @@ export function canonicalProduct( ce.pushScope(); body ??= ce.error('missing'); + var result: BoxedExpression | undefined = undefined; - indexingSet = canonicalIndexingSet(indexingSet); - const result = indexingSet - ? ce._fn('Product', [body.canonical, indexingSet]) - : ce._fn('Product', [body.canonical]); + if ( + indexingSet && + indexingSet.ops && + indexingSet.ops[0]?.head === 'Delimiter' + ) { + var multiIndex = MultiIndexingSet(indexingSet); + if (!multiIndex) return undefined; + var bodyAndIndex = [body.canonical]; + multiIndex.forEach((element) => { + bodyAndIndex.push(element); + }); + result = ce._fn('Product', bodyAndIndex); + } else { + var singleIndex = SingleIndexingSet(indexingSet); + result = singleIndex + ? ce._fn('Product', [body.canonical, singleIndex]) + : ce._fn('Product', [body.canonical]); + } ce.popScope(); return result; diff --git a/src/compute-engine/library/utils.ts b/src/compute-engine/library/utils.ts index 16326c3c..06839a79 100755 --- a/src/compute-engine/library/utils.ts +++ b/src/compute-engine/library/utils.ts @@ -1,5 +1,6 @@ import { checkDomain } from '../boxed-expression/validate'; import { MAX_ITERATION, asSmallInteger } from '../numerics/numeric'; +import { _BoxedExpression } from '../private.js'; import { BoxedExpression } from '../public'; /** @@ -7,15 +8,87 @@ import { BoxedExpression } from '../public'; * variable will be declared in that scope. * * @param indexingSet + + * IndexingSet is an expression describing an index variable + * and a range of values for that variable. + * + * The MultiIndexingSet function takes an expression of the form + * \sum_{i=1,j=1}^{10,10} x and returns an array of expressions + * ["Sum","x",["Triple","i",1,10],["Triple","j",1,10] + */ + +export function MultiIndexingSet( + indexingSet: BoxedExpression | undefined +): BoxedExpression[] | undefined { + if (!indexingSet) return undefined; + const ce = indexingSet.engine; + let indexes: BoxedExpression[] = []; + let hasSuperSequence = true ? indexingSet.ops?.length == 3 : false; + + let subSequence = indexingSet.ops![0].ops![0].ops; + let sequenceLength = subSequence?.length ?? 0; + let superSequence: BoxedExpression[] | null = null; + if (hasSuperSequence) { + superSequence = indexingSet.ops![2].ops![0].ops; + // check that the sequence lengths are the same in the sub and super scripts + if (subSequence?.length != superSequence?.length) { + return undefined; + } + } + // iterate through seuqences and call subscriptAgnosticIndexingSet + for (let i = 0; i < sequenceLength; i++) { + // this for loop separates any sequences of element in the sub and super script + // and put them into a proper indexing set + // e.g. \sum_{i=1,j=1}^{10,10} x -> ["Sum","x",["Triple","i",1,10],["Triple","j",1,10] + let canonicalizedIndex: BoxedExpression | undefined = undefined; + let index: BoxedExpression; + let lower: BoxedExpression | null = null; + let upper: BoxedExpression | null = null; + + index = subSequence![i].canonical; + if (subSequence) { + if (subSequence[i].head === 'Equal') { + index = subSequence[i].op1.canonical; + lower = subSequence[i].op2.canonical; + } + } + if (superSequence) { + upper = superSequence[i].canonical; + } + + if (upper && lower) + canonicalizedIndex = SingleIndexingSet(ce.tuple([index, lower, upper])); + else if (upper) + canonicalizedIndex = SingleIndexingSet(ce.tuple([index, ce.One, upper])); + else if (lower) + canonicalizedIndex = SingleIndexingSet(ce.tuple([index, lower])); + else canonicalizedIndex = SingleIndexingSet(index); + + if (canonicalizedIndex) indexes.push(canonicalizedIndex); + } + + return indexes; +} + +/** + * Assume the caller has setup a scope. The index + * variable will be declared in that scope. * + * @param indexingSet + + * IndexingSet is an expression describing an index variable + * and a range of values for that variable. + * + * The SingleIndexingSet function takes an expression of the form + * \sum_{i=1}^{10} x and returns an array of expressions + * ["Sum","x",["Triple","i",1,10] */ -export function canonicalIndexingSet( + +export function SingleIndexingSet( indexingSet: BoxedExpression | undefined ): BoxedExpression | undefined { if (!indexingSet) return undefined; - const ce = indexingSet.engine; - let index: BoxedExpression | null = null; let lower: BoxedExpression | null = null; let upper: BoxedExpression | null = null; @@ -39,8 +112,7 @@ export function canonicalIndexingSet( if (index.symbol) { ce.declare(index.symbol, { domain: 'Integers' }); index.bind(); - index = ce.hold(index); - } else index = ce.domainError('Symbols', index.domain, index); + } // The range bounds, if present, should be integers numbers if (lower && lower.isFinite) lower = checkDomain(ce, lower, 'Integers'); diff --git a/test/compute-engine/latex-syntax/arithmetic.test.ts b/test/compute-engine/latex-syntax/arithmetic.test.ts index 31db457f..3a13c43c 100644 --- a/test/compute-engine/latex-syntax/arithmetic.test.ts +++ b/test/compute-engine/latex-syntax/arithmetic.test.ts @@ -43,4 +43,62 @@ describe('PRODUCT', () => { evaluate(`\\prod \\lbrack 1, 2, 3, 4, 5\\rbrack`) ).toMatchInlineSnapshot(`120`); }); + + test('parsing many indices with non symbol index', () => { + expect(engine.parse(`\\sum_{n,m} k_{n,m}`)).toMatchInlineSnapshot(` + [ + "Sum", + ["Subscript", "k", ["Delimiter", ["Sequence", "n", "m"], "','"]], + "n", + "m" + ] + `); + }); + + test('sum but not actually of multiple indices', () => { + expect(engine.parse(`\\sum_{n=0,m=4}^{4,8}{n+m}`)).toMatchInlineSnapshot(` + [ + "Sum", + ["Add", "m", "n"], + ["Triple", "n", 0, 4], + ["Triple", "m", 4, 8] + ] + `); + }); + + test('parsing indices with element', () => { + expect(engine.parse(`\\sum_{n \\in N}K_n`)).toMatchInlineSnapshot( + `["Sum", "K_n", ["Element", "n", "N"]]` + ); + }); + + test('parsing indices with element', () => { + expect(engine.parse(`\\sum_{n \\in N; d \\in D} K`)).toMatchInlineSnapshot( + `["Sum", "K", ["Element", "n", "N"], ["Element", "d", "D"]]` + ); + }); + + test('parsing indices with element', () => { + expect(engine.parse(`\\sum_{n = 6; d \\in D} K`)).toMatchInlineSnapshot( + `["Sum", "K", ["Pair", "n", 6], ["Element", "d", "D"]]` + ); + }); + + test('parsing indices with element', () => { + expect(engine.parse(`\\sum_{d \\in D, d != V} K`)).toMatchInlineSnapshot( + `["Sum", "K", ["Element", "d", "D"], ["Unequal", "d", "V"]]` + ); + }); + + test('parsing indices with element', () => { + expect(engine.parse(`\\sum_{d_1} K`)).toMatchInlineSnapshot( + `["Sum", "K", ["Subscript", "d", 1]]` + ); + }); + + test('parsing indices with element', () => { + expect(engine.parse(`\\sum_{d_{1} = 2} K`)).toMatchInlineSnapshot( + `["Sum", "K", ["Pair", "d_1", 2]]` + ); + }); }); From 80f878a861d94c4a12fdbae572e8b820e424d17d Mon Sep 17 00:00:00 2001 From: Emily Traynor Date: Sun, 17 Dec 2023 13:29:45 -0500 Subject: [PATCH 2/2] extended evalSummation and evalProduct --- src/compute-engine/library/arithmetic-add.ts | 297 +++++++++++------- .../library/arithmetic-multiply.ts | 265 ++++++++++------ src/compute-engine/library/arithmetic.ts | 14 +- src/compute-engine/library/utils.ts | 12 +- .../latex-syntax/arithmetic.test.ts | 42 ++- test/compute-engine/smoke.test.ts | 135 +++----- 6 files changed, 443 insertions(+), 322 deletions(-) diff --git a/src/compute-engine/library/arithmetic-add.ts b/src/compute-engine/library/arithmetic-add.ts index e421e0ce..97d6ec31 100644 --- a/src/compute-engine/library/arithmetic-add.ts +++ b/src/compute-engine/library/arithmetic-add.ts @@ -7,11 +7,12 @@ import { Sum } from '../symbolic/sum'; import { asBignum, asFloat, MAX_SYMBOLIC_TERMS } from '../numerics/numeric'; import { widen } from '../boxed-expression/boxed-domain'; import { sortAdd } from '../boxed-expression/order'; -// import { canonicalIndexingSet, normalizeIndexingSet } from './utils'; import { MultiIndexingSet, SingleIndexingSet, normalizeIndexingSet, + cartesianProduct, + range, } from './utils'; import { each, isIndexableCollection } from '../collection-utils'; @@ -121,7 +122,7 @@ export function canonicalSummation( ce: IComputeEngine, body: BoxedExpression, indexingSet: BoxedExpression | undefined -) { +): BoxedExpression | null { // Sum is a scoped function (to declare the index) ce.pushScope(); @@ -134,7 +135,7 @@ export function canonicalSummation( indexingSet.ops[0]?.head === 'Delimiter' ) { var multiIndex = MultiIndexingSet(indexingSet); - if (!multiIndex) return undefined; + if (!multiIndex) return null; var bodyAndIndex = [body.canonical]; multiIndex.forEach((element) => { bodyAndIndex.push(element); @@ -153,13 +154,20 @@ export function canonicalSummation( export function evalSummation( ce: IComputeEngine, - expr: BoxedExpression, - indexingSet: BoxedExpression | undefined, + summationEquation: BoxedExpression[], mode: 'simplify' | 'N' | 'evaluate' ): BoxedExpression | undefined { + let expr = summationEquation[0]; + let indexingSet: BoxedExpression[] = []; + if (summationEquation) { + indexingSet = []; + for (let i = 1; i < summationEquation.length; i++) { + indexingSet.push(summationEquation[i]); + } + } let result: BoxedExpression | undefined | null = null; - if (!indexingSet) { + if (indexingSet?.length === 0) { // The body is a collection, e.g. Sum({1, 2, 3}) const body = mode === 'simplify' @@ -200,141 +208,194 @@ export function evalSummation( return result ?? undefined; } - const [index, lower, upper, isFinite] = normalizeIndexingSet( - indexingSet.evaluate() - ); - - if (!index) return undefined; + var indexArray: string[] = []; + let lowerArray: number[] = []; + let upperArray: number[] = []; + let isFiniteArray: boolean[] = []; + indexingSet.forEach((indexingSetElement) => { + const [index, lower, upper, isFinite] = normalizeIndexingSet( + indexingSetElement.evaluate() + ); + if (!index) return undefined; + indexArray.push(index); + lowerArray.push(lower); + upperArray.push(upper); + isFiniteArray.push(isFinite); + }); const fn = expr; + const savedContext = ce.swapScope(fn.scope); + ce.pushScope(); + fn.bind(); - if (lower >= upper) return undefined; + for (let i = 0; i < indexArray.length; i++) { + const index = indexArray[i]; + const lower = lowerArray[i]; + const upper = upperArray[i]; + const isFinite = isFiniteArray[i]; + if (lower >= upper) return undefined; - if (mode === 'simplify' && upper - lower >= MAX_SYMBOLIC_TERMS) - return undefined; + if (mode === 'simplify' && upper - lower >= MAX_SYMBOLIC_TERMS) + return undefined; - if (mode === 'evaluate' && upper - lower >= MAX_SYMBOLIC_TERMS) mode = 'N'; + if (mode === 'evaluate' && upper - lower >= MAX_SYMBOLIC_TERMS) mode = 'N'; - const savedContext = ce.swapScope(fn.scope); - ce.pushScope(); - fn.bind(); + if (mode === 'simplify') { + const terms: BoxedExpression[] = []; + for (let i = lower; i <= upper; i++) { + ce.assign(index, i); + terms.push(fn.simplify()); + } + result = ce.add(terms).simplify(); + } + } - if (mode === 'simplify') { - const terms: BoxedExpression[] = []; - for (let i = lower; i <= upper; i++) { - ce.assign(index, i); - terms.push(fn.simplify()); + // create cartesian product of ranges + let cartesianArray: number[][] = []; + if (indexArray.length > 1) { + for (let i = 0; i < indexArray.length - 1; i++) { + if (cartesianArray.length === 0) { + cartesianArray = cartesianProduct( + range(lowerArray[i], upperArray[i]), + range(lowerArray[i + 1], upperArray[i + 1]) + ); + } else { + cartesianArray = cartesianProduct( + cartesianArray.map((x) => x[0]), + range(lowerArray[i + 1], upperArray[i + 1]) + ); + } } - result = ce.add(terms).simplify(); + } else { + cartesianArray = range(lowerArray[0], upperArray[0]).map((x) => [x]); } if (mode === 'evaluate') { const terms: BoxedExpression[] = []; - for (let i = lower; i <= upper; i++) { - ce.assign(index, i); + for (const element of cartesianArray) { + const index = indexArray.map((x, i) => { + ce.assign(x, element[i]); + return x; + }); terms.push(fn.evaluate()); } result = ce.add(terms).evaluate(); } + for (let i = 0; i < indexArray.length; i++) { + // unassign indexes once done because if left assigned to an integer value, + // in double summations the .evaluate will assume the inner index value = upper + // for example in the following code latex: \\sum_{n=0}^{4}\\sum_{m=4}^{8}{n+m}` + // if the indexes aren't unassigned, once the first pass is done, every following pass + // will assume m is 8 for the m=4->8 iterations + ce.assign(indexArray[i], undefined); + } + if (mode === 'N') { - // if (result === null && !fn.scope) { - // // - // // The term is not a function of the index - // // - - // const n = fn.N(); - // if (!isFinite) { - // if (n.isZero) result = ce._ZERO; - // else if (n.isPositive) result = ce._POSITIVE_INFINITY; - // else result = ce._NEGATIVE_INFINITY; - // } - // if (result === null && fn.isPure) - // result = ce.mul([ce.number(upper - lower + 1), n]); - - // // If the term is not a function of the index, but it is not pure, - // // fall through to the general case - // } - - // - // Finite series. Evaluate each term and add them up - // - if (result === null && isFinite) { - if (bignumPreferred(ce)) { - let sum = ce.bignum(0); - for (let i = lower; i <= upper; i++) { - ce.assign(index, i); - const term = asBignum(fn.N()); - if (term === null) { - result = undefined; - break; - } - if (!term.isFinite()) { - sum = term; - break; - } - sum = sum.add(term); - } - if (result === null) result = ce.number(sum); - } else { - // Machine precision - const numericMode = ce.numericMode; - const precision = ce.precision; - ce.numericMode = 'machine'; - let sum = 0; - for (let i = lower; i <= upper; i++) { - ce.assign(index, i); - const term = asFloat(fn.N()); - if (term === null) { - result = undefined; - break; + for (let i = 0; i < indexArray.length; i++) { + const index = indexArray[i]; + const lower = lowerArray[i]; + const upper = upperArray[i]; + const isFinite = isFiniteArray[i]; + // if (result === null && !fn.scope) { + // // + // // The term is not a function of the index + // // + + // const n = fn.N(); + // if (!isFinite) { + // if (n.isZero) result = ce._ZERO; + // else if (n.isPositive) result = ce._POSITIVE_INFINITY; + // else result = ce._NEGATIVE_INFINITY; + // } + // if (result === null && fn.isPure) + // result = ce.mul([ce.number(upper - lower + 1), n]); + + // // If the term is not a function of the index, but it is not pure, + // // fall through to the general case + // } + + // + // Finite series. Evaluate each term and add them up + // + if (result === null && isFinite) { + if (bignumPreferred(ce)) { + let sum = ce.bignum(0); + for (let i = lower; i <= upper; i++) { + ce.assign(index, i); + const term = asBignum(fn.N()); + if (term === null) { + result = undefined; + break; + } + if (!term.isFinite()) { + sum = term; + break; + } + sum = sum.add(term); } - if (!Number.isFinite(term)) { - sum = term; - break; + if (result === null) result = ce.number(sum); + } else { + // Machine precision + const numericMode = ce.numericMode; + const precision = ce.precision; + ce.numericMode = 'machine'; + let sum = 0; + for (let i = lower; i <= upper; i++) { + ce.assign(index, i); + const term = asFloat(fn.N()); + if (term === null) { + result = undefined; + break; + } + if (!Number.isFinite(term)) { + sum = term; + break; + } + sum += term; } - sum += term; + ce.numericMode = numericMode; + ce.precision = precision; + if (result === null) result = ce.number(sum); } - ce.numericMode = numericMode; - ce.precision = precision; - if (result === null) result = ce.number(sum); - } - } else if (result === null) { - // - // Infinite series. - // - - // First, check for divergence - ce.assign(index, 1000); - const nMax = fn.N(); - ce.assign(index, 999); - const nMaxMinusOne = fn.N(); - - const ratio = asFloat(ce.div(nMax, nMaxMinusOne).N()); - if (ratio !== null && Number.isFinite(ratio) && Math.abs(ratio) > 1) { - result = ce.PositiveInfinity; - } else { - // Potentially converging series. - // Evaluate as a machine number (it's an approximation to infinity, so - // no point in calculating with high precision), and check for convergence - let sum = 0; - const numericMode = ce.numericMode; - const precision = ce.precision; - ce.numericMode = 'machine'; - for (let i = lower; i <= upper; i++) { - ce.assign(index, i); - const term = asFloat(fn.N()); - if (term === null) { - result = undefined; - break; + } else if (result === null) { + // + // Infinite series. + // + + // First, check for divergence + ce.assign(index, 1000); + const nMax = fn.N(); + ce.assign(index, 999); + const nMaxMinusOne = fn.N(); + + const ratio = asFloat(ce.div(nMax, nMaxMinusOne).N()); + if (ratio !== null && Number.isFinite(ratio) && Math.abs(ratio) > 1) { + result = ce.PositiveInfinity; + } else { + // Potentially converging series. + // Evaluate as a machine number (it's an approximation to infinity, so + // no point in calculating with high precision), and check for convergence + let sum = 0; + const numericMode = ce.numericMode; + const precision = ce.precision; + ce.numericMode = 'machine'; + for (let i = lower; i <= upper; i++) { + ce.assign(index, i); + const term = asFloat(fn.N()); + if (term === null) { + result = undefined; + break; + } + // Converged (or diverged), early exit + if (Math.abs(term) < Number.EPSILON || !Number.isFinite(term)) + break; + sum += term; } - // Converged (or diverged), early exit - if (Math.abs(term) < Number.EPSILON || !Number.isFinite(term)) break; - sum += term; + ce.numericMode = numericMode; + ce.precision = precision; + if (result === null) result = ce.number(sum); } - ce.numericMode = numericMode; - ce.precision = precision; - if (result === null) result = ce.number(sum); } } } diff --git a/src/compute-engine/library/arithmetic-multiply.ts b/src/compute-engine/library/arithmetic-multiply.ts index f35525ba..f5a37987 100644 --- a/src/compute-engine/library/arithmetic-multiply.ts +++ b/src/compute-engine/library/arithmetic-multiply.ts @@ -18,8 +18,9 @@ import { MultiIndexingSet, SingleIndexingSet, normalizeIndexingSet, + cartesianProduct, + range, } from './utils'; -// import { canonicalIndexingSet, normalizeIndexingSet } from './utils'; import { each } from '../collection-utils'; /** The canonical form of `Multiply`: @@ -221,7 +222,7 @@ export function canonicalProduct( ce: IComputeEngine, body: BoxedExpression | undefined, indexingSet: BoxedExpression | undefined -) { +): BoxedExpression | null { // Product is a scoped function (to declare the index) ce.pushScope(); @@ -234,7 +235,7 @@ export function canonicalProduct( indexingSet.ops[0]?.head === 'Delimiter' ) { var multiIndex = MultiIndexingSet(indexingSet); - if (!multiIndex) return undefined; + if (!multiIndex) return null; var bodyAndIndex = [body.canonical]; multiIndex.forEach((element) => { bodyAndIndex.push(element); @@ -253,18 +254,25 @@ export function canonicalProduct( export function evalMultiplication( ce: IComputeEngine, - expr: BoxedExpression, - indexingSet: BoxedExpression | undefined, - mode: 'simplify' | 'evaluate' | 'N' + summationEquation: BoxedExpression[], + mode: 'simplify' | 'N' | 'evaluate' ): BoxedExpression | undefined { + let expr = summationEquation[0]; + let indexingSet: BoxedExpression[] = []; + if (summationEquation) { + indexingSet = []; + for (let i = 1; i < summationEquation.length; i++) { + indexingSet.push(summationEquation[i]); + } + } let result: BoxedExpression | undefined | null = null; - const body = - mode === 'simplify' - ? expr.simplify() - : expr.evaluate({ numericMode: mode === 'N' }); + if (indexingSet?.length === 0) { + const body = + mode === 'simplify' + ? expr.simplify() + : expr.evaluate({ numericMode: mode === 'N' }); - if (!indexingSet) { // The body is a collection, e.g. Product({1, 2, 3}) if (bignumPreferred(ce)) { let product = ce.bignum(1); @@ -300,111 +308,122 @@ export function evalMultiplication( return result ?? undefined; } - const [index, lower, upper, isFinite] = normalizeIndexingSet(indexingSet); - - if (!index) return undefined; + var indexArray: string[] = []; + let lowerArray: number[] = []; + let upperArray: number[] = []; + let isFiniteArray: boolean[] = []; + indexingSet.forEach((indexingSetElement) => { + const [index, lower, upper, isFinite] = normalizeIndexingSet( + indexingSetElement.evaluate() + ); + if (!index) return undefined; + indexArray.push(index); + lowerArray.push(lower); + upperArray.push(upper); + isFiniteArray.push(isFinite); + }); const fn = expr; - if (mode !== 'N' && (lower >= upper || upper - lower >= MAX_SYMBOLIC_TERMS)) - return undefined; - const savedContext = ce.swapScope(fn.scope); ce.pushScope(); fn.bind(); - if (mode === 'simplify') { - const terms: BoxedExpression[] = []; - for (let i = lower; i <= upper; i++) { - ce.assign({ [index]: i }); - terms.push(fn.simplify()); + for (let i = 0; i < indexArray.length; i++) { + const index = indexArray[i]; + const lower = lowerArray[i]; + const upper = upperArray[i]; + const isFinite = isFiniteArray[i]; + if (lower >= upper) return undefined; + + if (mode !== 'N' && (lower >= upper || upper - lower >= MAX_SYMBOLIC_TERMS)) + return undefined; + + if (mode === 'simplify') { + const terms: BoxedExpression[] = []; + for (let i = lower; i <= upper; i++) { + ce.assign({ [index]: i }); + terms.push(fn.simplify()); + } + result = ce.mul(terms).simplify(); } - result = ce.mul(terms).simplify(); + } + + // create cartesian product of ranges + let cartesianArray: number[][] = []; + if (indexArray.length > 1) { + for (let i = 0; i < indexArray.length - 1; i++) { + if (cartesianArray.length === 0) { + cartesianArray = cartesianProduct( + range(lowerArray[i], upperArray[i]), + range(lowerArray[i + 1], upperArray[i + 1]) + ); + } else { + cartesianArray = cartesianProduct( + cartesianArray.map((x) => x[0]), + range(lowerArray[i + 1], upperArray[i + 1]) + ); + } + } + } else { + cartesianArray = range(lowerArray[0], upperArray[0]).map((x) => [x]); } if (mode === 'evaluate') { const terms: BoxedExpression[] = []; - for (let i = lower; i <= upper; i++) { - ce.assign({ [index]: i }); + for (const element of cartesianArray) { + const index = indexArray.map((x, i) => { + ce.assign(x, element[i]); + return x; + }); + //ce.assign({ [index]: i }); terms.push(fn.evaluate()); } result = ce.mul(terms).evaluate(); } if (mode === 'N') { - // if (result === null && !fn.scope) { - // // - // // The term is not a function of the index - // // - - // const n = fn.N(); - // if (!isFinite) { - // if (n.isZero) result = ce._ZERO; - // else if (n.isPositive) result = ce._POSITIVE_INFINITY; - // else result = ce._NEGATIVE_INFINITY; - // } - // if (result === null && fn.isPure) - // result = ce.pow(n, ce.number(upper - lower + 1)); - - // // If the term is not a function of the index, but it is not pure, - // // fall through to the general case - // } - - // - // Finite series. Evaluate each term and multiply them - // - if (result === null && isFinite) { - if (bignumPreferred(ce)) { - let product = ce.bignum(1); - for (let i = lower; i <= upper; i++) { - ce.assign({ [index]: i }); - const term = asBignum(fn.N()); - if (term === null || !term.isFinite()) { - result = term !== null ? ce.number(term) : undefined; - break; - } - product = product.mul(term); - } - if (result === null) result = ce.number(product); - } + for (let i = 0; i < indexArray.length; i++) { + const index = indexArray[i]; + const lower = lowerArray[i]; + const upper = upperArray[i]; + const isFinite = isFiniteArray[i]; + // if (result === null && !fn.scope) { + // // + // // The term is not a function of the index + // // + + // const n = fn.N(); + // if (!isFinite) { + // if (n.isZero) result = ce._ZERO; + // else if (n.isPositive) result = ce._POSITIVE_INFINITY; + // else result = ce._NEGATIVE_INFINITY; + // } + // if (result === null && fn.isPure) + // result = ce.pow(n, ce.number(upper - lower + 1)); + + // // If the term is not a function of the index, but it is not pure, + // // fall through to the general case + // } - // Machine precision - let product = 1; - const numericMode = ce.numericMode; - const precision = ce.precision; - ce.numericMode = 'machine'; - for (let i = lower; i <= upper; i++) { - ce.assign({ [index]: i }); - const term = asFloat(fn.N()); - if (term === null || !Number.isFinite(term)) { - result = term !== null ? ce.number(term) : undefined; - break; - } - product *= term; - } - ce.numericMode = numericMode; - ce.precision = precision; - - if (result === null) result = ce.number(product); - } - - if (result === null) { // - // Infinite series. + // Finite series. Evaluate each term and multiply them // + if (result === null && isFinite) { + if (bignumPreferred(ce)) { + let product = ce.bignum(1); + for (let i = lower; i <= upper; i++) { + ce.assign({ [index]: i }); + const term = asBignum(fn.N()); + if (term === null || !term.isFinite()) { + result = term !== null ? ce.number(term) : undefined; + break; + } + product = product.mul(term); + } + if (result === null) result = ce.number(product); + } - // First, check for divergence - ce.assign({ [index]: 1000 }); - const nMax = fn.N(); - ce.assign({ [index]: 999 }); - const nMaxMinusOne = fn.N(); - - const ratio = asFloat(ce.div(nMax, nMaxMinusOne).N()); - if (ratio !== null && Number.isFinite(ratio) && Math.abs(ratio) > 1) { - result = ce.PositiveInfinity; - } else { - // Potentially converging series. - // Evaluate as a machine number (it's an approximation to infinity, so - // no point in calculating with high precision), and check for convergence + // Machine precision let product = 1; const numericMode = ce.numericMode; const precision = ce.precision; @@ -412,22 +431,66 @@ export function evalMultiplication( for (let i = lower; i <= upper; i++) { ce.assign({ [index]: i }); const term = asFloat(fn.N()); - if (term === null) { - result = undefined; + if (term === null || !Number.isFinite(term)) { + result = term !== null ? ce.number(term) : undefined; break; } - // Converged (or diverged), early exit - if (Math.abs(1 - term) < Number.EPSILON || !Number.isFinite(term)) - break; product *= term; } - if (result === null) result = ce.number(product); ce.numericMode = numericMode; ce.precision = precision; + + if (result === null) result = ce.number(product); + } + + if (result === null) { + // + // Infinite series. + // + + // First, check for divergence + ce.assign({ [index]: 1000 }); + const nMax = fn.N(); + ce.assign({ [index]: 999 }); + const nMaxMinusOne = fn.N(); + + const ratio = asFloat(ce.div(nMax, nMaxMinusOne).N()); + if (ratio !== null && Number.isFinite(ratio) && Math.abs(ratio) > 1) { + result = ce.PositiveInfinity; + } else { + // Potentially converging series. + // Evaluate as a machine number (it's an approximation to infinity, so + // no point in calculating with high precision), and check for convergence + let product = 1; + const numericMode = ce.numericMode; + const precision = ce.precision; + ce.numericMode = 'machine'; + for (let i = lower; i <= upper; i++) { + ce.assign({ [index]: i }); + const term = asFloat(fn.N()); + if (term === null) { + result = undefined; + break; + } + // Converged (or diverged), early exit + if (Math.abs(1 - term) < Number.EPSILON || !Number.isFinite(term)) + break; + product *= term; + } + if (result === null) result = ce.number(product); + ce.numericMode = numericMode; + ce.precision = precision; + } } } } + for (let i = 0; i < indexArray.length; i++) { + // unassign indexes once done because if left assigned to an integer value, + // the .evaluate will assume the inner index value = upper in the following pass + ce.assign(indexArray[i], undefined); + } + ce.popScope(); ce.swapScope(savedContext); diff --git a/src/compute-engine/library/arithmetic.ts b/src/compute-engine/library/arithmetic.ts index b309acef..1a2f0a05 100755 --- a/src/compute-engine/library/arithmetic.ts +++ b/src/compute-engine/library/arithmetic.ts @@ -1160,11 +1160,9 @@ export const ARITHMETIC_LIBRARY: IdentifierDefinitions[] = [ // codomain: (ce, args) => domainAdd(ce, args), // The 'body' and 'range' need to be interpreted by canonicalMultiplication(). Don't canonicalize them yet. canonical: (ce, ops) => canonicalProduct(ce, ops[0], ops[1]), - simplify: (ce, ops) => - evalMultiplication(ce, ops[0], ops[1], 'simplify'), - evaluate: (ce, ops) => - evalMultiplication(ce, ops[0], ops[1], 'evaluate'), - N: (ce, ops) => evalMultiplication(ce, ops[0], ops[1], 'N'), + simplify: (ce, ops) => evalMultiplication(ce, ops, 'simplify'), + evaluate: (ce, ops) => evalMultiplication(ce, ops, 'evaluate'), + N: (ce, ops) => evalMultiplication(ce, ops, 'N'), }, }, @@ -1183,9 +1181,9 @@ export const ARITHMETIC_LIBRARY: IdentifierDefinitions[] = [ 'Numbers', ], canonical: (ce, ops) => canonicalSummation(ce, ops[0], ops[1]), - simplify: (ce, ops) => evalSummation(ce, ops[0], ops[1], 'simplify'), - evaluate: (ce, ops) => evalSummation(ce, ops[0], ops[1], 'evaluate'), - N: (ce, ops) => evalSummation(ce, ops[0], ops[1], 'N'), + simplify: (ce, ops) => evalSummation(ce, ops, 'simplify'), + evaluate: (ce, ops) => evalSummation(ce, ops, 'evaluate'), + N: (ce, ops) => evalSummation(ce, ops, 'N'), }, }, }, diff --git a/src/compute-engine/library/utils.ts b/src/compute-engine/library/utils.ts index 06839a79..a93d2d06 100755 --- a/src/compute-engine/library/utils.ts +++ b/src/compute-engine/library/utils.ts @@ -1,6 +1,5 @@ import { checkDomain } from '../boxed-expression/validate'; import { MAX_ITERATION, asSmallInteger } from '../numerics/numeric'; -import { _BoxedExpression } from '../private.js'; import { BoxedExpression } from '../public'; /** @@ -186,3 +185,14 @@ export function normalizeIndexingSet( } return [index, lower, upper, isFinite]; } + +export function cartesianProduct( + array1: number[], + array2: number[] +): number[][] { + return array1.flatMap((item1) => array2.map((item2) => [item1, item2])); +} + +export function range(start: number, end: number): number[] { + return Array.from({ length: end - start + 1 }, (_, index) => start + index); +} diff --git a/test/compute-engine/latex-syntax/arithmetic.test.ts b/test/compute-engine/latex-syntax/arithmetic.test.ts index 3a13c43c..4fc241b7 100644 --- a/test/compute-engine/latex-syntax/arithmetic.test.ts +++ b/test/compute-engine/latex-syntax/arithmetic.test.ts @@ -44,7 +44,7 @@ describe('PRODUCT', () => { ).toMatchInlineSnapshot(`120`); }); - test('parsing many indices with non symbol index', () => { + test('testing parsing of double indexed summation', () => { expect(engine.parse(`\\sum_{n,m} k_{n,m}`)).toMatchInlineSnapshot(` [ "Sum", @@ -55,7 +55,7 @@ describe('PRODUCT', () => { `); }); - test('sum but not actually of multiple indices', () => { + test('testing parsing of double indexed summation with upper and lower bounds', () => { expect(engine.parse(`\\sum_{n=0,m=4}^{4,8}{n+m}`)).toMatchInlineSnapshot(` [ "Sum", @@ -66,39 +66,65 @@ describe('PRODUCT', () => { `); }); - test('parsing indices with element', () => { + test('testing parsing of summation with element boxed expression', () => { expect(engine.parse(`\\sum_{n \\in N}K_n`)).toMatchInlineSnapshot( `["Sum", "K_n", ["Element", "n", "N"]]` ); }); - test('parsing indices with element', () => { + test('testing parsing of multi indexed summation with different index variables', () => { expect(engine.parse(`\\sum_{n \\in N; d \\in D} K`)).toMatchInlineSnapshot( `["Sum", "K", ["Element", "n", "N"], ["Element", "d", "D"]]` ); }); - test('parsing indices with element', () => { + test('testing parsing of multi indexed summation with and equal and non-equal boxed expression', () => { expect(engine.parse(`\\sum_{n = 6; d \\in D} K`)).toMatchInlineSnapshot( `["Sum", "K", ["Pair", "n", 6], ["Element", "d", "D"]]` ); }); - test('parsing indices with element', () => { + test('testing parsing of multi indexed summation with non-equal boxed expressions', () => { expect(engine.parse(`\\sum_{d \\in D, d != V} K`)).toMatchInlineSnapshot( `["Sum", "K", ["Element", "d", "D"], ["Unequal", "d", "V"]]` ); }); - test('parsing indices with element', () => { + test('testing parsing of summation with a subscripted subscript index', () => { expect(engine.parse(`\\sum_{d_1} K`)).toMatchInlineSnapshot( `["Sum", "K", ["Subscript", "d", 1]]` ); }); - test('parsing indices with element', () => { + test('testing parsing of summation with a subscripted subscript index and value', () => { expect(engine.parse(`\\sum_{d_{1} = 2} K`)).toMatchInlineSnapshot( `["Sum", "K", ["Pair", "d_1", 2]]` ); }); + + test('testing evaluating layers of summaitons', () => { + expect(evaluate(`\\sum_{n=0,m=4}^{4,8}{n+m}`)).toMatchInlineSnapshot(`200`); + }); + + test('testing two levels of summations', () => { + expect( + evaluate(`\\sum_{n=0}^{4}\\sum_{m=4}^{8}{n+m}`) + ).toMatchInlineSnapshot(`200`); + }); + + test('testing more than two levels of summations', () => { + expect( + evaluate(`\\sum_{n=0}^{4}(\\sum_{m=4}^{8}(\\sum_{l=0}^{2}{n+m})+n)`) + ).toMatchInlineSnapshot(`610`); + }); + + test('testing the evaluation of products (pis)', () => { + expect(evaluate(`\\prod_{n=1}^{4}n`)).toMatchInlineSnapshot(`24`); + }); + + test('testing the evaluation of more than two levels of products (pis)', () => { + expect( + evaluate(`\\prod_{n=1}^{2}\\prod_{m=1}^{3}nm`) + ).toMatchInlineSnapshot(`288`); + }); }); diff --git a/test/compute-engine/smoke.test.ts b/test/compute-engine/smoke.test.ts index aef5a4dc..2b08865a 100644 --- a/test/compute-engine/smoke.test.ts +++ b/test/compute-engine/smoke.test.ts @@ -372,128 +372,91 @@ describe('PARSING numbers', () => { [ "Add", [ - "Delimiter", + "Sum", [ - "Sequence", + "Delimiter", [ - "Sum", + "Sequence", [ - "Delimiter", + "Floor", [ - "Sequence", + "Divide", + 1, [ - "Floor", + "Add", [ - "Divide", - 1, + "Power", + 0, [ - "Add", + "Subtract", + "n", [ - "Power", - 0, + "Sum", [ - "Subtract", - "n", + "Delimiter", [ - "Delimiter", + "Sequence", [ - "Sequence", + "Delimiter", [ - "Sum", + "Sequence", [ - "Delimiter", + "Product", [ - "Sequence", + "Delimiter", [ - "Delimiter", + "Sequence", [ - "Sequence", + "Subtract", + 1, [ - "Product", + "Power", + 0, [ - "Delimiter", + "Abs", [ - "Sequence", + "Add", [ - "Subtract", - 1, - [ - "Power", - 0, - [ - "Abs", - [ - ..., - ..., - ... - ] - ] - ] + "Divide", + ["Negate", "v_2"], + "v_3" + ], + [ + "Floor", + ["Divide", "v_2", "v_3"] ] ] - ], - [ - "Triple", - [ - "Error", - [ - "ErrorCode", - "'incompatible-domain'", - "Symbols", - "Undefined" - ], - ["Subscript", "v", 3] - ], - 2, - ["Floor", ["Sqrt", "v_2"]] ] ] ] ] - ] - ], - [ - "Triple", - [ - "Error", - [ - "ErrorCode", - "'incompatible-domain'", - "Symbols", - "Undefined" - ], - ["Subscript", "v", 2] ], - 2, - "v_1" + [ + "Triple", + "v_3", + 2, + ["Floor", ["Sqrt", "v_2"]] + ] ] ] ] ] - ] - ], - 1 + ], + ["Triple", "v_2", 2, "v_1"] + ] ] - ] + ], + 1 ] ] - ], - [ - "Triple", - [ - "Error", - [ - "ErrorCode", - "'incompatible-domain'", - "Symbols", - "Undefined" - ], - ["Subscript", "v", 1] - ], - 2, - ["Floor", ["Multiply", 1.5, "n", ["Ln", "n"]]] ] ] + ], + [ + "Triple", + "v_1", + 2, + ["Floor", ["Multiply", 1.5, "n", ["Ln", "n"]]] ] ], 2