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: "Actual" symbolic evaluation demo #2475

Draft
wants to merge 2 commits 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
122 changes: 122 additions & 0 deletions 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.`)
14 changes: 7 additions & 7 deletions src/core/function/typed.js
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion src/expression/node/OperatorNode.js
Expand Up @@ -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'

Expand Down Expand Up @@ -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 })
4 changes: 2 additions & 2 deletions src/expression/node/SymbolNode.js
Expand Up @@ -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)
}
}
}
Expand Down
52 changes: 51 additions & 1 deletion src/expression/operators.js
Expand Up @@ -34,63 +34,75 @@ export const properties = [
},
{ // logical or
'OperatorNode:or': {
op: 'or',
associativity: 'left',
associativeWith: []
}

},
{ // 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: []
},
Expand All @@ -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: []
}
Expand All @@ -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',
Expand All @@ -143,6 +162,7 @@ export const properties = [
]
},
'OperatorNode:divide': {
op: '/',
associativity: 'left',
associativeWith: [],
latexLeftParens: false,
Expand All @@ -153,6 +173,7 @@ export const properties = [
// in LaTeX
},
'OperatorNode:dotMultiply': {
op: '.*',
associativity: 'left',
associativeWith: [
'OperatorNode:multiply',
Expand All @@ -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
Expand All @@ -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'
}
}
Expand Down Expand Up @@ -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
}