From 399648601ca68cf3b2813a3c451a61d574bb1f54 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 5 Mar 2022 15:32:51 -0800 Subject: [PATCH 1/2] feat(simplifyCore): convert equivalent function calls into operators Resolves #2415. --- src/expression/operators.js | 52 ++- src/function/algebra/simplifyCore.js | 300 ++++++++++-------- test/unit-tests/expression/operators.test.js | 13 +- .../function/algebra/simplify.test.js | 13 + .../function/algebra/simplifyCore.test.js | 27 +- 5 files changed, 271 insertions(+), 134 deletions(-) diff --git a/src/expression/operators.js b/src/expression/operators.js index 4a3e380486..049715f3c1 100644 --- a/src/expression/operators.js +++ b/src/expression/operators.js @@ -34,6 +34,7 @@ export const properties = [ }, { // logical or 'OperatorNode:or': { + op: 'or', associativity: 'left', associativeWith: [] } @@ -41,56 +42,67 @@ export const properties = [ }, { // logical xor 'OperatorNode:xor': { + op: 'xor', associativity: 'left', associativeWith: [] } }, { // logical and 'OperatorNode:and': { + op: 'and', associativity: 'left', associativeWith: [] } }, { // bitwise or 'OperatorNode:bitOr': { + op: '|', associativity: 'left', associativeWith: [] } }, { // bitwise xor 'OperatorNode:bitXor': { + op: '^|', associativity: 'left', associativeWith: [] } }, { // bitwise and 'OperatorNode:bitAnd': { + op: '&', associativity: 'left', associativeWith: [] } }, { // relational operators 'OperatorNode:equal': { + op: '==', associativity: 'left', associativeWith: [] }, 'OperatorNode:unequal': { + op: '!=', associativity: 'left', associativeWith: [] }, 'OperatorNode:smaller': { + op: '<', associativity: 'left', associativeWith: [] }, 'OperatorNode:larger': { + op: '<', associativity: 'left', associativeWith: [] }, 'OperatorNode:smallerEq': { + op: '<=', associativity: 'left', associativeWith: [] }, 'OperatorNode:largerEq': { + op: '>=', associativity: 'left', associativeWith: [] }, @@ -101,20 +113,24 @@ export const properties = [ }, { // bitshift operators 'OperatorNode:leftShift': { + op: '<<', associativity: 'left', associativeWith: [] }, 'OperatorNode:rightArithShift': { + op: '>>', associativity: 'left', associativeWith: [] }, 'OperatorNode:rightLogShift': { + op: '>>>', associativity: 'left', associativeWith: [] } }, { // unit conversion 'OperatorNode:to': { + op: 'to', associativity: 'left', associativeWith: [] } @@ -124,16 +140,19 @@ export const properties = [ }, { // addition, subtraction 'OperatorNode:add': { + op: '+', associativity: 'left', associativeWith: ['OperatorNode:add', 'OperatorNode:subtract'] }, 'OperatorNode:subtract': { + op: '-', associativity: 'left', associativeWith: [] } }, { // multiply, divide, modulus 'OperatorNode:multiply': { + op: '*', associativity: 'left', associativeWith: [ 'OperatorNode:multiply', @@ -143,6 +162,7 @@ export const properties = [ ] }, 'OperatorNode:divide': { + op: '/', associativity: 'left', associativeWith: [], latexLeftParens: false, @@ -153,6 +173,7 @@ export const properties = [ // in LaTeX }, 'OperatorNode:dotMultiply': { + op: '.*', associativity: 'left', associativeWith: [ 'OperatorNode:multiply', @@ -162,30 +183,37 @@ export const properties = [ ] }, 'OperatorNode:dotDivide': { + op: './', associativity: 'left', associativeWith: [] }, 'OperatorNode:mod': { + op: 'mod', associativity: 'left', associativeWith: [] } }, { // unary prefix operators 'OperatorNode:unaryPlus': { + op: '+', associativity: 'right' }, 'OperatorNode:unaryMinus': { + op: '-', associativity: 'right' }, 'OperatorNode:bitNot': { + op: '~', associativity: 'right' }, 'OperatorNode:not': { + op: 'not', associativity: 'right' } }, { // exponentiation 'OperatorNode:pow': { + op: '^', associativity: 'right', associativeWith: [], latexRightParens: false @@ -194,17 +222,20 @@ export const properties = [ // (it's on top) }, 'OperatorNode:dotPow': { + op: '.^', associativity: 'right', associativeWith: [] } }, { // factorial 'OperatorNode:factorial': { + op: '!', associativity: 'left' } }, { // matrix transpose - 'OperatorNode:transpose': { + 'OperatorNode:ctranspose': { + op: "'", associativity: 'left' } } @@ -309,3 +340,22 @@ export function isAssociativeWith (nodeA, nodeB, parenthesis) { // associativeWith is not defined return null } + +/** + * Get the operator associated with a function name. + * Returns a string with the operator symbol, or null if the + * input is not the name of a function associated with an + * operator. + * + * @param {string} Function name + * @return {string | null} Associated operator symbol, if any + */ +export function getOperator (fn) { + const identifier = 'OperatorNode:' + fn + for (const group of properties) { + if (identifier in group) { + return group[identifier].op + } + } + return null +} diff --git a/src/function/algebra/simplifyCore.js b/src/function/algebra/simplifyCore.js index ffbdf3aa61..59e9534ae9 100644 --- a/src/function/algebra/simplifyCore.js +++ b/src/function/algebra/simplifyCore.js @@ -1,9 +1,12 @@ import { isAccessorNode, isArrayNode, isConstantNode, isFunctionNode, isIndexNode, isObjectNode, isOperatorNode } from '../../utils/is.js' +import { getOperator } from '../../expression/operators.js' import { createUtil } from './simplify/util.js' import { factory } from '../../utils/factory.js' const name = 'simplifyCore' const dependencies = [ + 'typed', + 'parse', 'equal', 'isZero', 'add', @@ -23,6 +26,8 @@ const dependencies = [ ] export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ + typed, + parse, equal, isZero, add, @@ -71,164 +76,199 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ * Simplification options, as per simplify() * @return {Node} Returns expression with basic simplifications applied */ - function simplifyCore (node, options) { - const context = options ? options.context : undefined - if (hasProperty(node, 'trivial', context)) { - // This node does nothing if it has only one argument, so if so, - // return that argument simplified - if (isFunctionNode(node) && node.args.length === 1) { - return simplifyCore(node.args[0], options) - } - // For other node types, we try the generic methods - let simpChild = false - let childCount = 0 - node.forEach(c => { - ++childCount + const simplifyCore = typed('simplifyCore', { + string: function (expr) { + return this(parse(expr), {}) + }, + + 'string, Object': function (expr, options) { + return this(parse(expr), options) + }, + + Node: function (node) { + return this(node, {}) + }, + + 'Node, Object': function (nodeToSimplify, options) { + const context = options ? options.context : undefined + if (hasProperty(nodeToSimplify, 'trivial', context)) { + // This node does nothing if it has only one argument, so if so, + // return that argument simplified + if (isFunctionNode(nodeToSimplify) && nodeToSimplify.args.length === 1) { + return simplifyCore(nodeToSimplify.args[0], options) + } + // For other node types, we try the generic methods + let simpChild = false + let childCount = 0 + nodeToSimplify.forEach(c => { + ++childCount + if (childCount === 1) { + simpChild = simplifyCore(c, options) + } + }) if (childCount === 1) { - simpChild = simplifyCore(c, options) + return simpChild } - }) - if (childCount === 1) { - return simpChild } - } - if (isOperatorNode(node) && node.isUnary()) { - const a0 = simplifyCore(node.args[0], options) - - if (node.op === '-') { // unary minus - if (isOperatorNode(a0)) { - if (a0.isUnary() && a0.op === '-') { - return a0.args[0] - } else if (a0.isBinary() && a0.fn === 'subtract') { - return new OperatorNode('-', 'subtract', [a0.args[1], a0.args[0]]) + let node = nodeToSimplify + if (isFunctionNode(node)) { + const op = getOperator(node.name) + if (op) { + // Replace FunctionNode with a new OperatorNode + if (node.args.length > 2 && hasProperty(node, 'associative', context)) { + // unflatten into binary operations since that's what simplifyCore handles + while (node.args.length > 2) { + const last = node.args.pop() + const seclast = node.args.pop() + node.args.push(new OperatorNode(op, node.name, [last, seclast])) + } } + node = new OperatorNode(op, node.name, node.args) + } else { + return new FunctionNode( + simplifyCore(node.fn), node.args.map(n => simplifyCore(n, options))) } - return new OperatorNode(node.op, node.fn, [a0]) } - } else if (isOperatorNode(node) && node.isBinary()) { - const a0 = simplifyCore(node.args[0], options) - const a1 = simplifyCore(node.args[1], options) + if (isOperatorNode(node) && node.isUnary()) { + const a0 = simplifyCore(node.args[0], options) - if (node.op === '+') { - if (isConstantNode(a0)) { - if (isZero(a0.value)) { - return a1 - } else if (isConstantNode(a1)) { - return new ConstantNode(add(a0.value, a1.value)) + if (node.op === '-') { // unary minus + if (isOperatorNode(a0)) { + if (a0.isUnary() && a0.op === '-') { + return a0.args[0] + } else if (a0.isBinary() && a0.fn === 'subtract') { + return new OperatorNode('-', 'subtract', [a0.args[1], a0.args[0]]) + } } + return new OperatorNode(node.op, node.fn, [a0]) } - if (isConstantNode(a1) && isZero(a1.value)) { - return a0 - } - if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { - return new OperatorNode('-', 'subtract', [a0, a1.args[0]]) - } - return new OperatorNode(node.op, node.fn, a1 ? [a0, a1] : [a0]) - } else if (node.op === '-') { - if (isConstantNode(a0) && a1) { - if (isConstantNode(a1)) { - return new ConstantNode(subtract(a0.value, a1.value)) - } else if (isZero(a0.value)) { - return new OperatorNode('-', 'unaryMinus', [a1]) + } else if (isOperatorNode(node) && node.isBinary()) { + const a0 = simplifyCore(node.args[0], options) + const a1 = simplifyCore(node.args[1], options) + + if (node.op === '+') { + if (isConstantNode(a0)) { + if (isZero(a0.value)) { + return a1 + } else if (isConstantNode(a1)) { + return new ConstantNode(add(a0.value, a1.value)) + } } - } - // if (node.fn === "subtract" && node.args.length === 2) { - if (node.fn === 'subtract') { if (isConstantNode(a1) && isZero(a1.value)) { return a0 } if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { - return simplifyCore( - new OperatorNode('+', 'add', [a0, a1.args[0]]), options) + return new OperatorNode('-', 'subtract', [a0, a1.args[0]]) } - return new OperatorNode(node.op, node.fn, [a0, a1]) - } - } else if (node.op === '*') { - if (isConstantNode(a0)) { - if (isZero(a0.value)) { - return node0 - } else if (equal(a0.value, 1)) { - return a1 - } else if (isConstantNode(a1)) { - return new ConstantNode(multiply(a0.value, a1.value)) + return new OperatorNode(node.op, node.fn, a1 ? [a0, a1] : [a0]) + } else if (node.op === '-') { + if (isConstantNode(a0) && a1) { + if (isConstantNode(a1)) { + return new ConstantNode(subtract(a0.value, a1.value)) + } else if (isZero(a0.value)) { + return new OperatorNode('-', 'unaryMinus', [a1]) + } } - } - if (isConstantNode(a1)) { - if (isZero(a1.value)) { - return node0 - } else if (equal(a1.value, 1)) { - return a0 - } else if (isOperatorNode(a0) && a0.isBinary() && - a0.op === node.op && isCommutative(node, context)) { - const a00 = a0.args[0] - if (isConstantNode(a00)) { - const a00a1 = new ConstantNode(multiply(a00.value, a1.value)) - return new OperatorNode(node.op, node.fn, [a00a1, a0.args[1]], node.implicit) // constants on left + // if (node.fn === "subtract" && node.args.length === 2) { + if (node.fn === 'subtract') { + if (isConstantNode(a1) && isZero(a1.value)) { + return a0 + } + if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { + return simplifyCore( + new OperatorNode('+', 'add', [a0, a1.args[0]]), options) } + return new OperatorNode(node.op, node.fn, [a0, a1]) } - if (isCommutative(node, context)) { - return new OperatorNode(node.op, node.fn, [a1, a0], node.implicit) // constants on left - } else { - return new OperatorNode(node.op, node.fn, [a0, a1], node.implicit) + } else if (node.op === '*') { + if (isConstantNode(a0)) { + if (isZero(a0.value)) { + return node0 + } else if (equal(a0.value, 1)) { + return a1 + } else if (isConstantNode(a1)) { + return new ConstantNode(multiply(a0.value, a1.value)) + } } - } - return new OperatorNode(node.op, node.fn, [a0, a1], node.implicit) - } else if (node.op === '/') { - if (isConstantNode(a0)) { - if (isZero(a0.value)) { - return node0 - } else if (isConstantNode(a1) && - (equal(a1.value, 1) || equal(a1.value, 2) || equal(a1.value, 4))) { - return new ConstantNode(divide(a0.value, a1.value)) + if (isConstantNode(a1)) { + if (isZero(a1.value)) { + return node0 + } else if (equal(a1.value, 1)) { + return a0 + } else if (isOperatorNode(a0) && a0.isBinary() && + a0.op === node.op && isCommutative(node, context)) { + const a00 = a0.args[0] + if (isConstantNode(a00)) { + const a00a1 = new ConstantNode(multiply(a00.value, a1.value)) + return new OperatorNode(node.op, node.fn, [a00a1, a0.args[1]], node.implicit) // constants on left + } + } + if (isCommutative(node, context)) { + return new OperatorNode(node.op, node.fn, [a1, a0], node.implicit) // constants on left + } else { + return new OperatorNode(node.op, node.fn, [a0, a1], node.implicit) + } } - } - return new OperatorNode(node.op, node.fn, [a0, a1]) - } else if (node.op === '^') { - if (isConstantNode(a1)) { - if (isZero(a1.value)) { - return node1 - } else if (equal(a1.value, 1)) { - return a0 - } else { - if (isConstantNode(a0)) { - // fold constant - return new ConstantNode(pow(a0.value, a1.value)) - } else if (isOperatorNode(a0) && a0.isBinary() && a0.op === '^') { - const a01 = a0.args[1] - if (isConstantNode(a01)) { - return new OperatorNode(node.op, node.fn, [ - a0.args[0], - new ConstantNode(multiply(a01.value, a1.value)) - ]) + return new OperatorNode(node.op, node.fn, [a0, a1], node.implicit) + } else if (node.op === '/') { + if (isConstantNode(a0)) { + if (isZero(a0.value)) { + return node0 + } else if (isConstantNode(a1) && + (equal(a1.value, 1) || equal(a1.value, 2) || equal(a1.value, 4))) { + return new ConstantNode(divide(a0.value, a1.value)) + } + } + return new OperatorNode(node.op, node.fn, [a0, a1]) + } else if (node.op === '^') { + if (isConstantNode(a1)) { + if (isZero(a1.value)) { + return node1 + } else if (equal(a1.value, 1)) { + return a0 + } else { + if (isConstantNode(a0)) { + // fold constant + return new ConstantNode(pow(a0.value, a1.value)) + } else if (isOperatorNode(a0) && a0.isBinary() && a0.op === '^') { + const a01 = a0.args[1] + if (isConstantNode(a01)) { + return new OperatorNode(node.op, node.fn, [ + a0.args[0], + new ConstantNode(multiply(a01.value, a1.value)) + ]) + } } } } } + return new OperatorNode(node.op, node.fn, [a0, a1]) + } else if (isOperatorNode(node)) { + return new OperatorNode(node.op, node.fn, + node.args.map(a => simplifyCore(a, options))) + } + if (isArrayNode(node)) { + return new ArrayNode(node.items.map(n => simplifyCore(n, options))) + } + if (isAccessorNode(node)) { + return new AccessorNode( + simplifyCore(node.object, options), simplifyCore(node.index, options)) } - return new OperatorNode(node.op, node.fn, [a0, a1]) - } else if (isFunctionNode(node)) { - return new FunctionNode( - simplifyCore(node.fn), node.args.map(n => simplifyCore(n, options))) - } else if (isArrayNode(node)) { - return new ArrayNode( - node.items.map(n => simplifyCore(n, options))) - } else if (isAccessorNode(node)) { - return new AccessorNode( - simplifyCore(node.object, options), simplifyCore(node.index, options)) - } else if (isIndexNode(node)) { - return new IndexNode( - node.dimensions.map(n => simplifyCore(n, options))) - } else if (isObjectNode(node)) { - const newProps = {} - for (const prop in node.properties) { - newProps[prop] = simplifyCore(node.properties[prop], options) + if (isIndexNode(node)) { + return new IndexNode( + node.dimensions.map(n => simplifyCore(n, options))) + } + if (isObjectNode(node)) { + const newProps = {} + for (const prop in node.properties) { + newProps[prop] = simplifyCore(node.properties[prop], options) + } + return new ObjectNode(newProps) } - return new ObjectNode(newProps) - } else { // cannot simplify + return node } - return node - } + }) return simplifyCore }) diff --git a/test/unit-tests/expression/operators.test.js b/test/unit-tests/expression/operators.test.js index 2ef9640caa..9127df0dff 100644 --- a/test/unit-tests/expression/operators.test.js +++ b/test/unit-tests/expression/operators.test.js @@ -1,6 +1,6 @@ import assert from 'assert' import math from '../../../src/defaultInstance.js' -import { getAssociativity, getPrecedence, isAssociativeWith } from '../../../src/expression/operators.js' +import { getAssociativity, getPrecedence, isAssociativeWith, getOperator } from '../../../src/expression/operators.js' const OperatorNode = math.OperatorNode const AssignmentNode = math.AssignmentNode const SymbolNode = math.SymbolNode @@ -15,9 +15,11 @@ describe('operators', function () { const n1 = new AssignmentNode(new SymbolNode('a'), a) const n2 = new OperatorNode('or', 'or', [a, b]) + const n3 = math.parse("M'") assert.strictEqual(getPrecedence(n1, 'keep'), 0) assert.strictEqual(getPrecedence(n2, 'keep'), 2) + assert.strictEqual(getPrecedence(n3, 'keep'), 17) }) it('should return null if precedence is not defined for a node', function () { @@ -45,11 +47,13 @@ describe('operators', function () { const n2 = new OperatorNode('^', 'pow', [a, a]) const n3 = new OperatorNode('-', 'unaryMinus', [a]) const n4 = new OperatorNode('!', 'factorial', [a]) + const n5 = math.parse("M'") assert.strictEqual(getAssociativity(n1, 'keep'), 'left') assert.strictEqual(getAssociativity(n2, 'keep'), 'right') assert.strictEqual(getAssociativity(n3, 'keep'), 'right') assert.strictEqual(getAssociativity(n4, 'keep'), 'left') + assert.strictEqual(getAssociativity(n5, 'keep'), 'left') }) it('should return the associativity of a ParenthesisNode', function () { @@ -110,4 +114,11 @@ describe('operators', function () { assert.strictEqual(isAssociativeWith(p, sub, 'auto'), true) assert.strictEqual(isAssociativeWith(p, sub, 'keep'), null) }) + + it('should get the operator of a function name', () => { + assert.strictEqual(getOperator('multiply'), '*') + assert.strictEqual(getOperator('ctranspose'), "'") + assert.strictEqual(getOperator('mod'), 'mod') + assert.strictEqual(getOperator('square'), null) + }) }) diff --git a/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index 607c8ee6b9..dc09c26a67 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -233,6 +233,19 @@ describe('simplify', function () { simplifyAndCompare('x^2*y^3*z - y*z*x^2*y', 'x^2*z*(y^3-y^2)') }) + it('can simplify with functions as well as operators', () => { + simplifyAndCompare('add(x,x)', '2*x') + simplifyAndCompare('multiply(x,2)+x', '3*x') + simplifyAndCompare('add(2*add(x,1), x+1)', '3*(x + 1)') + simplifyAndCompare('multiply(2, x+1) + add(x,1)', '3*(x + 1)') + simplifyAndCompare('add(y*pow(x,2), multiply(2,x^2))', 'x^2*(y+2)') + simplifyAndCompare('add(x*y, multiply(y,x))', '2*x*y') + simplifyAndCompare('subtract(multiply(x,y), multiply(y,x))', '0') + simplifyAndCompare('pow(x,2)*multiply(y^3, z) - multiply(y,z,y,x^2,y)', '0') + simplifyAndCompare('subtract(multiply(x^2, pow(y,3))*z, y*multiply(z,x^2)*y)', + 'x^2*z*(y^3-y^2)') + }) + it('should collect separated like terms', function () { simplifyAndCompare('x+1+x', '2*x+1') simplifyAndCompare('x^2+x+3+x^2', '2*x^2+x+3') diff --git a/test/unit-tests/function/algebra/simplifyCore.test.js b/test/unit-tests/function/algebra/simplifyCore.test.js index 362b1c1a36..2bd0666050 100644 --- a/test/unit-tests/function/algebra/simplifyCore.test.js +++ b/test/unit-tests/function/algebra/simplifyCore.test.js @@ -4,8 +4,10 @@ import assert from 'assert' import math from '../../../../src/defaultInstance.js' describe('simplifyCore', function () { - const testSimplifyCore = function (expr, expected, opts = {}) { - const actual = math.simplifyCore(math.parse(expr)).toString(opts) + const testSimplifyCore = function (expr, expected, opts = {}, simpOpts = {}) { + let actual = math.simplifyCore(math.parse(expr), simpOpts).toString(opts) + assert.strictEqual(actual, expected) + actual = math.simplifyCore(expr, simpOpts).toString(opts) assert.strictEqual(actual, expected) } @@ -33,6 +35,14 @@ describe('simplifyCore', function () { testSimplifyCore('{a:x*1, b:y-0}', '{"a": x, "b": y}') }) + it('should not alter order of multiplication when noncommutative', function () { + testSimplifyCore('5*x*3', '5 * x * 3', {}, { context: { multiply: { commutative: false } } }) + }) + + it('should remove any trivial function', function () { + testSimplifyCore('foo(y)', 'y', {}, { context: { foo: { trivial: true } } }) + }) + it('strips ParenthesisNodes (implicit in tree)', function () { testSimplifyCore('((x)*(y))', 'x * y') testSimplifyCore('((x)*(y))^1', 'x * y') @@ -62,4 +72,17 @@ describe('simplifyCore', function () { testSimplifyCore('x+0==5', 'x == 5') testSimplifyCore('(x*1) % (y^1)', 'x % y') }) + + it('converts functions to their corresponding infix operators', () => { + testSimplifyCore('add(x, y)', 'x + y') + testSimplifyCore('mod(x, 5)', 'x mod 5') + testSimplifyCore('to(5 cm, in)', '5 cm to in') + testSimplifyCore('ctranspose(M)', "M'") + }) + + it('continues to simplify after function -> operator conversion', () => { + testSimplifyCore('add(multiply(x, 0), y)', 'y') + testSimplifyCore('and(multiply(1, x), true)', 'x and true') + testSimplifyCore('add(x, 0 ,y)', 'x + y') + }) }) From fd88f32073b7dc34d76d8edcc1f0b72917c1d7de Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 9 Mar 2022 15:59:32 -0800 Subject: [PATCH 2/2] WIP: "Actual" symbolic evaluation This PR is not intended for merging as-is, but serves as an alternate demonstration (vis-a-vis issue #2437) that essentially all the ingredients already exist in mathjs for evaluation in which all undefined variables evaluate to symbols, therefore possibly returning an expression (Node) rather than a concrete value (while still evaluating all the way to concrete values when possible). Moreover, mathematical manipulation of symbolic expressions can be supported without circularity and without modifying numerous source files. This PR does however depend on a small addition to typed-function.js, see https://github.com/josdejong/typed-function/pull/125. See (or run) examples/symbolic_evaluation.mjs for further details on this. --- examples/symbolic_evaluation.mjs | 122 ++++++++++++++++++ src/core/function/typed.js | 14 +- src/expression/node/OperatorNode.js | 4 +- src/expression/node/SymbolNode.js | 4 +- src/function/algebra/simplify.js | 1 + .../algebra/simplify/simplifyConstant.js | 2 + src/type/unit/Unit.js | 8 +- src/type/unit/function/unit.js | 5 + .../function/algebra/derivative.test.js | 4 +- test/unit-tests/function/matrix/count.test.js | 2 +- test/unit-tests/function/matrix/diag.test.js | 2 +- test/unit-tests/function/unit/to.test.js | 2 +- .../type/matrix/function/matrix.test.js | 4 +- .../type/matrix/function/sparse.test.js | 2 +- 14 files changed, 151 insertions(+), 25 deletions(-) create mode 100644 examples/symbolic_evaluation.mjs diff --git a/examples/symbolic_evaluation.mjs b/examples/symbolic_evaluation.mjs new file mode 100644 index 0000000000..1ef1c037d1 --- /dev/null +++ b/examples/symbolic_evaluation.mjs @@ -0,0 +1,122 @@ +import math from '../src/defaultInstance.js' + +math.SymbolNode.onUndefinedSymbol = (name, node) => node + +math.typed.onMismatch = (name, args, signatures) => { + let nodeArg = false + for (const arg of args) { + if (math.isNode(arg)) { + nodeArg = true + break + } + } + if (nodeArg) { + const specialOps = { addScalar: 'add', multiplyScalar: 'multiply' } + if (name in specialOps) name = specialOps[name] + const maybeOp = math.OperatorNode.getOperator(name) + const newArgs = Array.from(args, arg => math.simplify.ensureNode(arg)) + if (maybeOp) return new math.OperatorNode(maybeOp, name, newArgs) + return new math.FunctionNode(new math.SymbolNode(name), newArgs) + } + + let argstr = args[0].toString() + for (let i = 1; i < args.length; ++i) { + argstr += `, ${args[i]}` + } + + throw TypeError(`Typed function type mismatch for ${name} called with '${argstr}'`) +} + +function mystringify (obj) { + let s = '{' + for (const key in obj) { + s += `${key}: ${obj[key]}, ` + } + return s.slice(0, -2) + '}' +} + +function logExample (expr, scope = {}) { + let header = `Evaluating: '${expr}'` + if (Object.keys(scope).length > 0) { + header += ` in scope ${mystringify(scope)}` + } + console.log(header) + let result + try { + result = math.evaluate(expr, scope) + if (math.isNode(result)) { + result = `Expression ${result.toString()}` + } + } catch (err) { + result = err.toString() + } + console.log(` --> ${result}`) +} + +let point = 1 +console.log(`${point++}. By just evaluating all unknown symbols to themselves, and +providing a typed-function handler that builds expression trees when there is +no matching signature, we implement full-fledged symbolic evaluation:`) +logExample('x*y + 3x - y + 2', { y: 7 }) +console.log(` +${point++}. If all of the free variables have values, this evaluates +all the way to the numeric value:`) +logExample('x*y + 3x - y + 2', { x: 1, y: 7 }) +console.log(` +${point++}. It works with matrices as well, for example.`) +logExample('[x^2 + 3x + x*y, y, 12]', { x: 2 }) +logExample('[x^2 + 3x + x*y, y, 12]', { x: 2, y: 7 }) +console.log(`(Note there are no fractions as in the simplifyConstant +version, since we are using ordinary 'math.evaluate()' in this approach.) + +${point++}. However, to break a chain of automatic conversions that disrupts +this style of evaluation, it's necessary to remove the former conversion +from 'number' to 'string':`) +logExample('count(57)') +console.log(`(In develop, this returns 2, the length of the string representation +of 57. However, it turns out that with only very slight tweaks to "Unit," +all tests pass without the automatic 'number' -> 'string' conversion, +suggesting it isn't really being used, or at least very little. + +${point++}. This lets you more easily perform operations like symbolic differentiation:`) +logExample('derivative(sin(x) + exp(x) + x^3, x)') +console.log("(Note no quotes in the argument to 'derivative' -- it is directly\n" + + 'operating on the expression, without any string values involved.)') + +console.log(` +${point++}. Doing it this way respects assignment, since ordinary evaluate does:`) +logExample('f = x^2+2x*y; derivative(f,x)') +console.log(` +${point++}. You can also build up expressions incrementally and use the scope:`) +logExample('h1 = x^2+5x; h3 = h1 + h2; derivative(h3,x)', { + h2: math.evaluate('3x+7') +}) +console.log(` +${point++}. Some kinks still remain at the moment. Scope values for the +variable of differentiation disrupt the results:`) +logExample('derivative(x^3 + x^2, x)') +logExample('derivative(x^3 + x^2, x)', { x: 1 }) +console.log(`${''}(We'd like the latter evaluation to return the result of the +first differentiation, evaluated at 1, or namely 5. However, there is not (yet) +a concept in math.evaluate that 'derivative' creates a variable-binding +environment, blocking off the 'x' from being substituted via the outside +scope within its first argument. Implementing this may be slightly trickier +in this approach since ordinary 'evaluate' (in the absence of 'rawArgs' +markings) is an essentially "bottom-up" operation whereas 'math.resolve' is +more naturally a "top-down" operation. The point is you need to know you're +inside a 'derivative' or other binding environment at the time that you do +substitution.) + +Also, unlike the simplifyConstant approach, derivative doesn't know to +'check' whether a contained variable actually depends on 'x', so the order +of assignments makes a big difference:`) +logExample('h3 = h1+h2; h1 = x^2+5x; derivative(h3,x)', { + h2: math.evaluate('3x+7') +}) +console.log(`${''}(Here, 'h1' in the first assignment evaluates to a +SymbolNode('h1'), which ends up being part of the argument to the eventual +derivative call, and there's never anything to fill in the later definition +of 'h1', and as it's a different symbol, its derivative with respect to 'x' +is assumed to be 0.) + +Nevertheless, such features could be implemented.`) diff --git a/src/core/function/typed.js b/src/core/function/typed.js index ad51b4a643..ca60622cb8 100644 --- a/src/core/function/typed.js +++ b/src/core/function/typed.js @@ -179,13 +179,13 @@ export const createTyped = /* #__PURE__ */ factory('typed', dependencies, functi return new Complex(x, 0) } - }, { - from: 'number', - to: 'string', - convert: function (x) { - return x + '' - } - }, { + }, // { + // from: 'number', + // to: 'string', + // convert: function (x) { + // return x + '' + // } + /* }, */ { from: 'BigNumber', to: 'Complex', convert: function (x) { diff --git a/src/expression/node/OperatorNode.js b/src/expression/node/OperatorNode.js index 5dc5c75df4..76b16441f2 100644 --- a/src/expression/node/OperatorNode.js +++ b/src/expression/node/OperatorNode.js @@ -2,7 +2,7 @@ import { isNode } from '../../utils/is.js' import { map } from '../../utils/array.js' import { escape } from '../../utils/string.js' import { getSafeProperty, isSafeMethod } from '../../utils/customs.js' -import { getAssociativity, getPrecedence, isAssociativeWith, properties } from '../operators.js' +import { getAssociativity, getPrecedence, getOperator, isAssociativeWith, properties } from '../operators.js' import { latexOperators } from '../../utils/latex.js' import { factory } from '../../utils/factory.js' @@ -613,5 +613,7 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({ return this.type + ':' + this.fn } + OperatorNode.getOperator = getOperator + return OperatorNode }, { isClass: true, isNode: true }) diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index 57b4c098f0..5f2c513781 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -74,13 +74,13 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m } } else { const isUnit = isValuelessUnit(name) - + const me = this return function (scope, args, context) { return scope.has(name) ? scope.get(name) : isUnit ? new Unit(null, name) - : SymbolNode.onUndefinedSymbol(name) + : SymbolNode.onUndefinedSymbol(name, me) } } } diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index ee18b2eeed..cf79f4181a 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -279,6 +279,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( simplify.defaultContext = defaultContext simplify.realContext = realContext simplify.positiveContext = positiveContext + simplify.ensureNode = simplifyConstant.ensureNode function removeParens (node) { return node.transform(function (node, path, parent) { diff --git a/src/function/algebra/simplify/simplifyConstant.js b/src/function/algebra/simplify/simplifyConstant.js index 577b0f8c32..86d54c8165 100644 --- a/src/function/algebra/simplify/simplifyConstant.js +++ b/src/function/algebra/simplify/simplifyConstant.js @@ -428,5 +428,7 @@ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies } } + simplifyConstant.ensureNode = _ensureNode + return simplifyConstant }) diff --git a/src/type/unit/Unit.js b/src/type/unit/Unit.js index 7c4ade2ead..1b6ef0f151 100644 --- a/src/type/unit/Unit.js +++ b/src/type/unit/Unit.js @@ -82,13 +82,7 @@ export const createUnitClass = /* #__PURE__ */ factory(name, dependencies, ({ this.units = u.units this.dimensions = u.dimensions } else { - this.units = [ - { - unit: UNIT_NONE, - prefix: PREFIXES.NONE, // link to a list with supported prefixes - power: 0 - } - ] + this.units = [] this.dimensions = [] for (let i = 0; i < BASE_DIMENSIONS.length; i++) { this.dimensions[i] = 0 diff --git a/src/type/unit/function/unit.js b/src/type/unit/function/unit.js index 108e643329..96c736e783 100644 --- a/src/type/unit/function/unit.js +++ b/src/type/unit/function/unit.js @@ -47,6 +47,11 @@ export const createUnitFunction = /* #__PURE__ */ factory(name, dependencies, ({ return new Unit(value, unit) }, + 'number | BigNumber | Fraction': function (value) { + // dimensionless + return new Unit(value) + }, + 'Array | Matrix': function (x) { return deepMap(x, this) } diff --git a/test/unit-tests/function/algebra/derivative.test.js b/test/unit-tests/function/algebra/derivative.test.js index d061f84717..c940f68430 100644 --- a/test/unit-tests/function/algebra/derivative.test.js +++ b/test/unit-tests/function/algebra/derivative.test.js @@ -254,7 +254,7 @@ describe('derivative', function () { it('should throw error for incorrect argument types', function () { assert.throws(function () { derivative('42', '42') - }, /TypeError: Unexpected type of argument in function derivative \(expected: string or SymbolNode or number or boolean, actual: ConstantNode, index: 1\)/) + }, /TypeError: Unexpected type of argument in function derivative \(expected: string or SymbolNode or boolean, actual: ConstantNode, index: 1\)/) assert.throws(function () { derivative('[1, 2; 3, 4]', 'x') @@ -268,7 +268,7 @@ describe('derivative', function () { it('should throw error if incorrect number of arguments', function () { assert.throws(function () { derivative('x + 2') - }, /TypeError: Too few arguments in function derivative \(expected: string or SymbolNode or number or boolean, index: 1\)/) + }, /TypeError: Too few arguments in function derivative \(expected: string or SymbolNode or boolean, index: 1\)/) assert.throws(function () { derivative('x + 2', 'x', {}, true, 42) diff --git a/test/unit-tests/function/matrix/count.test.js b/test/unit-tests/function/matrix/count.test.js index d4954b63ce..4ca323877b 100644 --- a/test/unit-tests/function/matrix/count.test.js +++ b/test/unit-tests/function/matrix/count.test.js @@ -31,7 +31,7 @@ describe('count', function () { it('should throw an error if called with an invalid number of arguments', function () { assert.throws(function () { count() }, /TypeError: Too few arguments/) - assert.throws(function () { count(1, 2) }, /TypeError: Too many arguments/) + assert.throws(function () { count('1', 2) }, /TypeError: Too many arguments/) }) it('should throw an error if called with invalid type of arguments', function () { diff --git a/test/unit-tests/function/matrix/diag.test.js b/test/unit-tests/function/matrix/diag.test.js index 68a514475b..2b1bfbc19a 100644 --- a/test/unit-tests/function/matrix/diag.test.js +++ b/test/unit-tests/function/matrix/diag.test.js @@ -108,7 +108,7 @@ describe('diag', function () { it('should throw an error in case of wrong number of arguments', function () { assert.throws(function () { math.diag() }, /TypeError: Too few arguments/) - assert.throws(function () { math.diag([], 2, 3, 4) }, /TypeError: Too many arguments/) + assert.throws(function () { math.diag([], 3, 'dense', 4) }, /TypeError: Too many arguments/) }) it('should throw an error in case of invalid type of arguments', function () { diff --git a/test/unit-tests/function/unit/to.test.js b/test/unit-tests/function/unit/to.test.js index 1da0cc4655..184950438c 100644 --- a/test/unit-tests/function/unit/to.test.js +++ b/test/unit-tests/function/unit/to.test.js @@ -70,7 +70,7 @@ describe('to', function () { it('should throw an error if called with a number', function () { assert.throws(function () { math.to(5, unit('m')) }, TypeError) - assert.throws(function () { math.to(unit('5cm'), 2) }, /SyntaxError: "2" contains no units/) + assert.throws(function () { math.to(unit('5cm'), 2) }, TypeError) }) it('should throw an error if called with a string', function () { diff --git a/test/unit-tests/type/matrix/function/matrix.test.js b/test/unit-tests/type/matrix/function/matrix.test.js index f84e50e720..285e908588 100644 --- a/test/unit-tests/type/matrix/function/matrix.test.js +++ b/test/unit-tests/type/matrix/function/matrix.test.js @@ -85,11 +85,11 @@ describe('matrix', function () { }) it('should throw an error if called with too many arguments', function () { - assert.throws(function () { matrix([], 3, 3, 7) }, /TypeError: Too many arguments/) + assert.throws(function () { matrix([], 'dense', 'number', 7) }, /TypeError: Too many arguments/) }) it('should throw an error when called with an invalid storage format', function () { - assert.throws(function () { math.matrix([], 1) }, /TypeError: Unknown matrix type "1"/) + assert.throws(function () { math.matrix([], '1') }, /TypeError: Unknown matrix type "1"/) }) it('should throw an error when called with an unknown storage format', function () { diff --git a/test/unit-tests/type/matrix/function/sparse.test.js b/test/unit-tests/type/matrix/function/sparse.test.js index 8c18b64570..88648cf3c6 100644 --- a/test/unit-tests/type/matrix/function/sparse.test.js +++ b/test/unit-tests/type/matrix/function/sparse.test.js @@ -40,7 +40,7 @@ describe('sparse', function () { }) it('should throw an error if called with too many arguments', function () { - assert.throws(function () { sparse([], 3, 3) }, /TypeError: Too many arguments/) + assert.throws(function () { sparse([], 'number', 3) }, /TypeError: Too many arguments/) }) it('should LaTeX matrix', function () {