mathjs/lib/function/algebra/simplify.js

586 lines
19 KiB
JavaScript

'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;
/**
* Returns a simplified expression tree.
*
* For more details on the theory:
* http://stackoverflow.com/questions/7540227/strategies-for-simplifying-math-expressions
* https://en.wikipedia.org/wiki/Symbolic_computation#Simplification
*
* Syntax:
*
* simplify(expr)
*
* Usage:
*
* math.simplify('2 * 1 * x ^ (2 - 1)');
* var f = math.parse('2 * 1 * x ^ (2 - 1)');
* math.simplify(f);
*
* @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} expr
* @return {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} The simplified form of `expr`
*/
var simplify = typed('simplify', {
'string': function (expr) {
return parse(expr).simplify(default_rules);
},
'string, Array': function (expr, rules) {
return parse(expr).simplify(rules);
},
'Node': function (expr) {
return simplify(expr, default_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
var default_rules = [
{ 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;