UNPKG

mathjs

Version:

Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with dif

612 lines (551 loc) 20.2 kB
'use strict'; function factory (type, config, load, typed) { var parse = load(require('../../expression/parse')); var ConstantNode = load(require('../../expression/node/ConstantNode')); var FunctionNode = load(require('../../expression/node/FunctionNode')); var OperatorNode = load(require('../../expression/node/OperatorNode')); var ParenthesisNode = load(require('../../expression/node/ParenthesisNode')); var SymbolNode = load(require('../../expression/node/SymbolNode')); var Node = load(require('../../expression/node/Node')); var simplifyConstant = load(require('./simplify/simplifyConstant')); var util = load(require('./simplify/util')); var isCommutative = util.isCommutative; var isAssociative = util.isAssociative; var flatten = util.flatten; var unflattenr = util.unflattenr; var unflattenl = util.unflattenl; var createMakeNodeFunction = util.createMakeNodeFunction; /** * Simplify an expression tree. * * It's possible to pass a custom set of rules to the function as second * argument. A rule can be specified as an object, string, or function: * * var rules = [ * { l: 'n1*n3 + n2*n3', r: '(n1+n2)*n3' }, * 'n1*n3 + n2*n3 -> (n1+n2)*n3', * function (node) { * // ... return a new node or return the node unchanged * return node * } * ] * * * The default list with rules is exposed on the function as `simplify.rules` * and can be used as a basis to built a set of custom rules. * * For more details on the theory, see: * * - [Strategies for simplifying math expressions (Stackoverflow)](http://stackoverflow.com/questions/7540227/strategies-for-simplifying-math-expressions) * - [Symbolic computation - Simplification (Wikipedia)](https://en.wikipedia.org/wiki/Symbolic_computation#Simplification) * * Syntax: * * simplify(expr) * simplify(expr, rules) * * Examples: * * math.simplify('2 * 1 * x ^ (2 - 1)'); // Node {2 * x} * var f = math.parse('2 * 1 * x ^ (2 - 1)'); * math.simplify(f); // Node {2 * x} * * See also: * * derivative, parse, eval * * @param {Node | string} expr * The expression to be simplified * @param {Array<{l:string, r: string} | string | function>} [rules] * Optional list with custom rules * @return {Node} Returns the simplified form of `expr` */ var simplify = typed('simplify', { 'string': function (expr) { return simplify(parse(expr), simplify.rules); }, 'string, Array': function (expr, rules) { return simplify(parse(expr), rules); }, 'Node': function (expr) { return simplify(expr, simplify.rules); }, 'Node, Array': function (expr, rules) { rules = _buildRules(rules); var res = removeParens(expr); var after = res.toString({parenthesis: 'all'}); var before = null; while(before != after) { lastsym = 0; before = after; for (var i=0; i<rules.length; i++) { if (typeof rules[i] === 'function') { res = rules[i](res); } else { flatten(res); res = applyRule(res, rules[i]); } unflattenl(res); // using left-heavy binary tree here since custom rule functions may expect it } after = res.toString({parenthesis: 'all'}); } return res; } }); function removeParens(node) { return node.transform(function(node, path, parent) { if(node.isParenthesisNode) { return node.content; } else { return node; } }); } // Array of strings, used to build the ruleSet. // Each l (left side) and r (right side) are parsed by // the expression parser into a node tree. // Left hand sides are matched to subtrees within the // expression to be parsed and replaced with the right // hand side. // TODO: Add support for constraints on constants (either in the form of a '=' expression or a callback [callback allows things like comparing symbols alphabetically]) // To evaluate lhs constants for rhs constants, use: { l: 'c1+c2', r: 'c3', evaluate: 'c3 = c1 + c2' }. Multiple assignments are separated by ';' in block format. // It is possible to get into an infinite loop with conflicting rules simplify.rules = [ { l: 'n+0', r: 'n' }, { l: 'n^0', r: '1' }, { l: '0*n', r: '0' }, { l: 'n/n', r: '1'}, { l: 'n^1', r: 'n' }, { l: '+n1', r:'n1' }, { l: 'n--n1', r:'n+n1' }, { l: 'log(e)', r:'1' }, // temporary rules { l: 'n-n1', r:'n+-n1' }, // temporarily replace 'subtract' so we can further flatten the 'add' operator { l: '-(c*C)', r: '(-c) * C' }, // make non-constant terms positive { l: '-C', r: '(-1) * C' }, { l: 'n/n1^n2', r:'n*n1^-n2' }, // temporarily replace 'divide' so we can further flatten the 'multiply' operator { l: 'n/n1', r:'n*n1^-1' }, // collect like factors { l: 'n*n', r: 'n^2' }, { l: 'n * n^n1', r: 'n^(n1+1)' }, { l: 'n^n1 * n^n2', r: 'n^(n1+n2)' }, // collect like terms { l: 'n+n', r: '2*n' }, { l: 'n+-n', r: '0' }, { l: 'n1*n2 + n2', r: '(n1+1)*n2' }, { l: 'n1*n3 + n2*n3', r: '(n1+n2)*n3' }, simplifyConstant, { l: '(-n)*n1', r: '-(n*n1)' }, // make factors positive (and undo 'make non-constant terms positive') // ordering of constants { l: 'c+C', r: 'C+c', context: { 'add': { commutative:false } } }, { l: 'C*c', r: 'c*C', context: { 'multiply': { commutative:false } } }, // undo temporary rules { l: '(-1) * n', r: '-n' }, { l: 'n+-n1', r:'n-n1' }, // undo replace 'subtract' { l: 'n*(n1^-1)', r:'n/n1' }, // undo replace 'divide' { l: 'n*n1^-n2', r:'n/n1^n2' }, { l: 'n1^-1', r:'1/n1' }, { l: 'n*(n1/n2)', r:'(n*n1)/n2' }, // '*' before '/' { l: 'n-(n1+n2)', r:'n-n1-n2' }, // '-' before '+' // { l: '(n1/n2)/n3', r: 'n1/(n2*n3)' }, // { l: '(n*n1)/(n*n2)', r: 'n1/n2' }, { l: '1*n', r: 'n' } // this pattern can be produced by simplifyConstant ]; /** * Parse the string array of rules into nodes * * Example syntax for rules: * * Position constants to the left in a product: * { l: 'n1 * c1', r: 'c1 * n1' } * n1 is any Node, and c1 is a ConstantNode. * * Apply difference of squares formula: * { l: '(n1 - n2) * (n1 + n2)', r: 'n1^2 - n2^2' } * n1, n2 mean any Node. * * Short hand notation: * 'n1 * c1 -> c1 * n1' */ function _buildRules(rules) { // Array of rules to be used to simplify expressions var ruleSet = []; for(var i=0; i<rules.length; i++) { var rule = rules[i]; var newRule; var ruleType = typeof rule; switch (ruleType) { case 'string': var lr = rule.split('->'); if (lr.length !== 2) { throw SyntaxError('Could not parse rule: ' + rule); } rule = {l: lr[0], r: lr[1]}; /* falls through */ case 'object': newRule = { l: removeParens(parse(rule.l)), r: removeParens(parse(rule.r)), } if(rule.context) { newRule.evaluate = rule.context; } if(rule.evaluate) { newRule.evaluate = parse(rule.evaluate); } if (newRule.l.isOperatorNode && isAssociative(newRule.l)) { var makeNode = createMakeNodeFunction(newRule.l); var expandsym = _getExpandPlaceholderSymbol(); newRule.expanded = {}; newRule.expanded.l = makeNode([newRule.l.clone(), expandsym]); // Push the expandsym into the deepest possible branch. // This helps to match the newRule against nodes returned from getSplits() later on. flatten(newRule.expanded.l); unflattenr(newRule.expanded.l); newRule.expanded.r = makeNode([newRule.r, expandsym]); } break; case 'function': newRule = rule; break; default: throw TypeError('Unsupported type of rule: ' + ruleType); } // console.log('Adding rule: ' + rules[i]); // console.log(newRule); ruleSet.push(newRule); } return ruleSet; } var lastsym = 0; function _getExpandPlaceholderSymbol() { return new SymbolNode('_p'+lastsym++); } /** * Returns a simplfied form of node, or the original node if no simplification was possible. * * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} node * @return {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} The simplified form of `expr`, or the original node if no simplification was possible. */ var applyRule = typed('applyRule', { 'Node, Object': function (node, rule) { //console.log('Entering applyRule(' + node.toString() + ')'); // Do not clone node unless we find a match var res = node; // First replace our child nodes with their simplified versions // If a child could not be simplified, the assignments will have // no effect since the node is returned unchanged if (res instanceof OperatorNode || res instanceof FunctionNode) { if (res.args) { for(var i=0; i<res.args.length; i++) { res.args[i] = applyRule(res.args[i], rule); } } } else if(res instanceof ParenthesisNode) { if(res.content) { res.content = applyRule(res.content, rule); } } // Try to match a rule against this node var repl = rule.r; var matches = _ruleMatch(rule.l, res)[0]; // If the rule is associative operator, we can try matching it while allowing additional terms. // This allows us to match rules like 'n+n' to the expression '(1+x)+x' or even 'x+1+x' if the operator is commutative. if (!matches && rule.expanded) { repl = rule.expanded.r; matches = _ruleMatch(rule.expanded.l, res)[0]; } if (matches) { // var before = res.toString({parenthesis: 'all'}); // Create a new node by cloning the rhs of the matched rule res = repl.clone(); // Replace placeholders with their respective nodes //console.log('Traversing rule ' + res); res = res.transform(function(n, path, parent) { if(n.isSymbolNode) { if(matches.placeholders.hasOwnProperty(n.name)) { var replace = matches.placeholders[n.name].clone(); return replace; } } return n; }); // var after = res.toString({parenthesis: 'all'}); // console.log('Simplified ' + before + ' to ' + after); } return res; } }); /** * Get (binary) combinations of a flattened binary node * e.g. +(node1, node2, node3) -> [ * +(node1, +(node2, node3)), * +(node2, +(node1, node3)), * +(node3, +(node1, node2))] * */ function getSplits(node, context) { var res = []; var right, rightArgs; var makeNode = createMakeNodeFunction(node); if (isCommutative(node, context)) { for (var i=0; i<node.args.length; i++) { rightArgs = node.args.slice(0); rightArgs.splice(i, 1); right = (rightArgs.length === 1) ? rightArgs[0] : makeNode(rightArgs); res.push(makeNode([node.args[i], right])); } } else { rightArgs = node.args.slice(1); right = (rightArgs.length === 1) ? rightArgs[0] : makeNode(rightArgs); res.push(makeNode([node.args[0], right])); } return res; } /** * Returns the set union of two match-placeholders or null if there is a conflict. */ function mergeMatch(match1, match2) { var res = {placeholders:{}}; // Some matches may not have placeholders; this is OK if (!match1.placeholders && !match2.placeholders) { return res; } else if (!match1.placeholders) { return match2; } else if (!match2.placeholders) { return match1; } // Placeholders with the same key must match exactly for (var key in match1.placeholders) { res.placeholders[key] = match1.placeholders[key]; if (match2.placeholders.hasOwnProperty(key)) { if (!_exactMatch(match1.placeholders[key], match2.placeholders[key] )) { return null; } } } for (var key in match2.placeholders) { res.placeholders[key] = match2.placeholders[key]; } return res; } /** * Combine two lists of matches by applying mergeMatch to the cartesian product of two lists of matches. * Each list represents matches found in one child of a node. */ function combineChildMatches(list1, list2) { var res = []; if (list1.length === 0 || list2.length === 0) { return res; } var merged; for (var i1 = 0; i1 < list1.length; i1++) { for (var i2 = 0; i2 < list2.length; i2++) { merged = mergeMatch(list1[i1], list2[i2]); if (merged) { res.push(merged); } } } return res; } /** * Combine multiple lists of matches by applying mergeMatch to the cartesian product of two lists of matches. * Each list represents matches found in one child of a node. * Returns a list of unique matches. */ function mergeChildMatches(childMatches) { if (childMatches.length === 0) { return childMatches; } var sets = childMatches.reduce(combineChildMatches); var uniqueSets = []; var unique = {}; for(var i = 0; i < sets.length; i++) { var s = JSON.stringify(sets[i]); if (!unique[s]) { unique[s] = true; uniqueSets.push(sets[i]); } } return uniqueSets; } /** * Determines whether node matches rule. * * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} rule * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} node * @return {Object} Information about the match, if it exists. */ function _ruleMatch(rule, node, isSplit) { // console.log('Entering _ruleMatch(' + JSON.stringify(rule) + ', ' + JSON.stringify(node) + ')'); // console.log('rule = ' + rule); // console.log('node = ' + node); // console.log('Entering _ruleMatch(' + rule.toString() + ', ' + node.toString() + ')'); var res = [{placeholders:{}}]; if (rule instanceof OperatorNode && node instanceof OperatorNode || rule instanceof FunctionNode && node instanceof FunctionNode) { // If the rule is an OperatorNode or a FunctionNode, then node must match exactly if (rule instanceof OperatorNode) { if (rule.op !== node.op || rule.fn !== node.fn) { return []; } } else if (rule instanceof FunctionNode) { if (rule.name !== node.name) { return []; } } // rule and node match. Search the children of rule and node. if (node.args.length === 1 && rule.args.length === 1 || !isAssociative(node) || isSplit) { // Expect non-associative operators to match exactly var childMatches = []; for (var i = 0; i < rule.args.length; i++) { var childMatch = _ruleMatch(rule.args[i], node.args[i]); if (childMatch.length === 0) { // Child did not match, so stop searching immediately return []; } // The child matched, so add the information returned from the child to our result childMatches.push(childMatch); } res = mergeChildMatches(childMatches); } else if (node.args.length >= 2 && rule.args.length === 2) { // node is flattened, rule is not // Associative operators/functions can be split in different ways so we check if the rule matches each // them and return their union. var splits = getSplits(node, rule.context); var splitMatches = []; for(var i = 0; i < splits.length; i++) { var matchSet = _ruleMatch(rule, splits[i], true); // recursing at the same tree depth here splitMatches = splitMatches.concat(matchSet); } return splitMatches; } else if (rule.args.length > 2) { throw Error('Unexpected non-binary associative function: ' + rule.toString()); } else { // Incorrect number of arguments in rule and node, so no match return []; } } else if (rule instanceof SymbolNode) { // If the rule is a SymbolNode, then it carries a special meaning // according to the first character of the symbol node name. // c.* matches a ConstantNode // n.* matches any node if (rule.name.length === 0) { throw new Error('Symbol in rule has 0 length...!?'); } if (rule.name[0] == 'n' || rule.name.substring(0,2) == '_p') { // rule matches _anything_, so assign this node to the rule.name placeholder // Assign node to the rule.name placeholder. // Our parent will check for matches among placeholders. res[0].placeholders[rule.name] = node; } else if (rule.name[0] == 'v') { // rule matches any variable thing (not a ConstantNode) if(!node.isConstantNode) { res[0].placeholders[rule.name] = node; } else { // Mis-match: rule was expecting something other than a ConstantNode return []; } } else if (rule.name[0] == 'C') { // rule matches anything but a ConstantNode if(node instanceof ConstantNode) { // Mis-match: rule was expecting not a ConstantNode return []; } else { res[0].placeholders[rule.name] = node; } } else if (rule.name[0] == 'c') { // rule matches any ConstantNode if(node instanceof ConstantNode) { res[0].placeholders[rule.name] = node; } else { // Mis-match: rule was expecting a ConstantNode return []; } } else { throw new Error('Invalid symbol in rule: ' + rule.name); } } else if (rule instanceof ConstantNode) { // Literal constant in our rule, so much match node exactly if(rule.value === node.value) { // The constants match } else { return []; } } else { // Some other node was encountered which we aren't prepared for, so no match return []; } // It's a match! // console.log('_ruleMatch(' + rule.toString() + ', ' + node.toString() + ') found a match'); return res; } /** * Determines whether p and q (and all their children nodes) are identical. * * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} p * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} q * @return {Object} Information about the match, if it exists. */ function _exactMatch(p, q) { if(p instanceof ConstantNode && q instanceof ConstantNode) { if(p.value !== q.value) { return false; } } else if(p instanceof SymbolNode && q instanceof SymbolNode) { if(p.name !== q.name) { return false; } } else if(p instanceof OperatorNode && q instanceof OperatorNode || p instanceof FunctionNode && q instanceof FunctionNode) { if (p instanceof OperatorNode) { if (p.op !== q.op || p.fn !== q.fn) { return false; } } else if (p instanceof FunctionNode) { if (p.name !== q.name) { return false; } } if(p.args.length !== q.args.length) { return false; } for(var i=0; i<p.args.length; i++) { if(!_exactMatch(p.args[i], q.args[i])) { return false; } } } else { return false; } return true; } return simplify; } exports.name = 'simplify'; exports.factory = factory;