Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: "symbolic" evaluation #2470

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 77 additions & 0 deletions 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.`)
6 changes: 5 additions & 1 deletion 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'
Expand Down Expand Up @@ -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:
*
Expand Down Expand Up @@ -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
*/
Expand All @@ -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) {
Expand Down
14 changes: 7 additions & 7 deletions src/function/algebra/simplify/simplifyConstant.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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' &&
Expand Down