From 5b9cda9e010e29016314ce8829f02fcfb423aea5 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 6 Mar 2022 18:25:57 -0800 Subject: [PATCH] WIP: "symbolic" evaluation This PR is not intended for merging as-is, but serves to demonstrate that essentially all the ingredients already exist in mathjs for evaluation in which all undefined variables are treated as symbols, therefore possibly returning an expression (Node) rather than a concrete value (while still evaluating all the way to concrete values when possible). See (or run) `examples/symbolic_evaluation.js` for further details on this. --- examples/symbolic_evaluation.js | 77 +++++++++++++++++++ src/function/algebra/simplify.js | 6 +- .../algebra/simplify/simplifyConstant.js | 14 ++-- 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 examples/symbolic_evaluation.js 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' &&