diff --git a/examples/symbolic_evaluation.js b/examples/symbolic_evaluation.js new file mode 100644 index 0000000000..24c7a48f4e --- /dev/null +++ b/examples/symbolic_evaluation.js @@ -0,0 +1,77 @@ +const math = require('..') +function symval (expr, scope = {}) { + return math.simplify( + expr, [ + 'n1-n2 -> n1+(-n2)', + 'n1/n2 -> n1*n2^(-1)', + math.simplify.simplifyConstant, + 'n1*n2^(-1) -> n1/n2', + 'n1+(-n2) -> n1-n2' + ], + scope, { + unwrapConstants: true + } + ) +} + +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 = symval(expr, scope) + if (math.isNode(result)) { + result = `Expression ${result.toString()}` + } + console.log(` --> ${result}`) +} + +let point = 1 +console.log(`${point++}. By just simplifying constants as fully as possible, using +the scope as necessary, we create a sort of "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 all the fractions because currently simplifyConstant prefers +them. That preference could be tweaked for this purpose.) + +${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++}. You can also build up expressions incrementally:`) +logExample('derivative(h3,x)', { + h3: symval('h1+h2'), + h1: symval('x^2+3x'), + h2: symval('3x+7') +}) +console.log(` +${point++}. Some kinks still remain at the moment. The scope is not affected +by assignment expressions, and 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 mathjs (specifically in 'resolve') that 'derivative' creates a +variable-binding environment, blocking off the 'x' from being substituted via +the outside scope within its first argument.) + +But such features can be implemented.`) diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index ee18b2eeed..0280c31d2e 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -1,4 +1,4 @@ -import { isConstantNode, isParenthesisNode } from '../../utils/is.js' +import { isNode, isConstantNode, isParenthesisNode } from '../../utils/is.js' import { factory } from '../../utils/factory.js' import { createUtil } from './simplify/util.js' import { createSimplifyConstant } from './simplify/simplifyConstant.js' @@ -156,6 +156,8 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( * - `fractionsLimit` (10000): when `exactFractions` is true, constants will * be expressed as fractions only when both numerator and denominator * are smaller than `fractionsLimit`. + * - `unwrapConstants` (false): if the entire expression simplifies down to + * a constant, return the value directly (as opposed to wrapped in a Node). * * Syntax: * @@ -266,6 +268,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( laststr = newstr } } + if (!isNode(res)) return res // short-circuit if we got to a concrete value /* Use left-heavy binary tree internally, * since custom rule functions may expect it */ @@ -279,6 +282,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( simplify.defaultContext = defaultContext simplify.realContext = realContext simplify.positiveContext = positiveContext + simplify.simplifyConstant = simplifyConstant 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..2435e9e95f 100644 --- a/src/function/algebra/simplify/simplifyConstant.js +++ b/src/function/algebra/simplify/simplifyConstant.js @@ -42,7 +42,9 @@ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies createUtil({ FunctionNode, OperatorNode, SymbolNode }) function simplifyConstant (expr, options) { - return _ensureNode(foldFraction(expr, options)) + const folded = foldFraction(expr, options) + if (options.unwrapConstants) return folded + return _ensureNode(folded) } function _removeFractions (thing) { @@ -310,12 +312,10 @@ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies if (operatorFunctions.indexOf(node.name) === -1) { const args = node.args.map(arg => foldFraction(arg, options)) - // If all args are numbers - if (!args.some(isNode)) { - try { - return _eval(node.name, args, options) - } catch (ignoreandcontinue) { } - } + // If the function can handle the arguments, call it + try { + return _eval(node.name, args, options) + } catch (ignoreandcontinue) { } // Size of a matrix does not depend on entries if (node.name === 'size' &&