From 77f94c4f95f9238593f8dd0bd3bf31b27248e7a9 Mon Sep 17 00:00:00 2001 From: thatcomputerguy0101 Date: Tue, 15 Nov 2022 04:36:08 -0500 Subject: [PATCH] More simplify wildcards (#1915) * Create wildcards.js and implement basic type detections * Remove isUnit from isConstantExpression wildcard isUnit would never be encountered because units are stored as a part of ConstantNodes. * Add matching for the new wildcard rules * Add tests for the new wildcard rules * Remove comment regarding Unit * Seperate wildcard import into individual imports * Update comments at top and change '*i' to '*d' * Seperate Unit Tests * Update simplify documentation comment * Add unit test for #1406 * Update imports for new build system * Update simplify test with new rules syntax * Fix small documentation errors * Update simplify rules to use new wildcards * Add tests for rules updated with new wildcards * Remove duplicated comment information Co-authored-by: Jos de Jong --- AUTHORS | 1 + src/function/algebra/simplify.js | 144 +++++++++++++----- src/function/algebra/simplify/wildcards.js | 19 +++ .../function/algebra/simplify.test.js | 114 ++++++++++++++ 4 files changed, 236 insertions(+), 42 deletions(-) create mode 100644 src/function/algebra/simplify/wildcards.js diff --git a/AUTHORS b/AUTHORS index 9d71f7518c..16fa2c7dd1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -133,6 +133,7 @@ Tom Hickson dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> Markel F Lazersmoke +Alan Everett SungJinWoo-SL <56172528+SungJinWoo-SL@users.noreply.github.com> Nick Ewing jos diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index dc6c6d6667..0b0e85cdcc 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -1,4 +1,5 @@ -import { isConstantNode, isParenthesisNode } from '../../utils/is.js' +import { isParenthesisNode } from '../../utils/is.js' +import { isConstantNode, isVariableNode, isNumericNode, isConstantExpression } from './simplify/wildcards.js' import { factory } from '../../utils/factory.js' import { createUtil } from './simplify/util.js' import { hasOwnProperty } from '../../utils/object.js' @@ -90,9 +91,15 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( * expression is that variables starting with the following characters are * interpreted as wildcards: * - * - 'n' - matches any Node - * - 'c' - matches any ConstantNode - * - 'v' - matches any Node that is not a ConstantNode + * - 'n' - Matches any node [Node] + * - 'c' - Matches a constant literal (5 or 3.2) [ConstantNode] + * - 'cl' - Matches a constant literal; same as c [ConstantNode] + * - 'cd' - Matches a decimal literal (5 or -3.2) [ConstantNode or unaryMinus wrapping a ConstantNode] + * - 'ce' - Matches a constant expression (-5 or √3) [Expressions consisting of only ConstantNodes, functions, and operators] + * - 'v' - Matches a variable; anything not matched by c (-5 or x) [Node that is not a ConstantNode] + * - 'vl' - Matches a variable literal (x or y) [SymbolNode] + * - 'vd' - Matches a non-decimal expression; anything not matched by cd (x or √3) [Node that is not a ConstantNode or unaryMinus that is wrapping a ConstantNode] + * - 've' - Matches a variable expression; anything not matched by ce (x or 2x) [Expressions that contain a SymbolNode or other non-constant term] * * The default list of rules is exposed on the function as `simplify.rules` * and can be used as a basis to built a set of custom rules. Note that since @@ -253,15 +260,15 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( assuming: { subtract: { total: false } } }, { - s: '-(c*v) -> v * (-c)', // make non-constant terms positive + s: '-(cl*v) -> v * (-cl)', // make non-constant terms positive assuming: { multiply: { commutative: true }, subtract: { total: true } } }, { - s: '-(c*v) -> (-c) * v', // non-commutative version, part 1 + s: '-(cl*v) -> (-cl) * v', // non-commutative version, part 1 assuming: { multiply: { commutative: false }, subtract: { total: true } } }, { - s: '-(v*c) -> v * (-c)', // non-commutative version, part 2 + s: '-(v*cl) -> v * (-cl)', // non-commutative version, part 2 assuming: { multiply: { commutative: false }, subtract: { total: true } } }, { l: '-(n1/n2)', r: '-n1/n2' }, @@ -285,17 +292,17 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( }, // collect like factors; into a sum, only do this for nonconstants - { l: ' v * ( v * n1 + n2)', r: 'v^2 * n1 + v * n2' }, + { l: ' vd * ( vd * n1 + n2)', r: 'vd^2 * n1 + vd * n2' }, { - s: ' v * (v^n4 * n1 + n2) -> v^(1+n4) * n1 + v * n2', + s: ' vd * (vd^n4 * n1 + n2) -> vd^(1+n4) * n1 + vd * n2', assuming: { divide: { total: true } } // v*1/v = v^(1+-1) needs 1/v }, { - s: 'v^n3 * ( v * n1 + n2) -> v^(n3+1) * n1 + v^n3 * n2', + s: 'vd^n3 * ( vd * n1 + n2) -> vd^(n3+1) * n1 + vd^n3 * n2', assuming: { divide: { total: true } } }, { - s: 'v^n3 * (v^n4 * n1 + n2) -> v^(n3+n4) * n1 + v^n3 * n2', + s: 'vd^n3 * (vd^n4 * n1 + n2) -> vd^(n3+n4) * n1 + vd^n3 * n2', assuming: { divide: { total: true } } }, { l: 'n*n', r: 'n^2' }, @@ -320,12 +327,12 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( assuming: { add: { total: true } } // 2 = 1 + 1 needs to exist }, { l: 'n+-n', r: '0' }, - { l: 'v*n + v', r: 'v*(n+1)' }, // NOTE: leftmost position is special: + { l: 'vd*n + vd', r: 'vd*(n+1)' }, // NOTE: leftmost position is special: { l: 'n3*n1 + n3*n2', r: 'n3*(n1+n2)' }, // All sub-monomials tried there. { l: 'n3^(-n4)*n1 + n3 * n2', r: 'n3^(-n4)*(n1 + n3^(n4+1) *n2)' }, { l: 'n3^(-n4)*n1 + n3^n5 * n2', r: 'n3^(-n4)*(n1 + n3^(n4+n5)*n2)' }, { - s: 'n*v + v -> (n+1)*v', // noncommutative additional cases + s: 'n*vd + vd -> (n+1)*vd', // noncommutative additional cases assuming: { multiply: { commutative: false } } }, { @@ -340,9 +347,9 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( s: 'n1*n3^(-n4) + n2 * n3^n5 -> (n1 + n2*n3^(n4 + n5))*n3^(-n4)', assuming: { multiply: { commutative: false } } }, - { l: 'n*c + c', r: '(n+1)*c' }, + { l: 'n*cd + cd', r: '(n+1)*cd' }, { - s: 'c*n + c -> c*(n+1)', + s: 'cd*n + cd -> cd*(n+1)', assuming: { multiply: { commutative: false } } }, @@ -360,12 +367,12 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( // final ordering of constants { - s: 'c+v -> v+c', + s: 'ce+ve -> ve+ce', assuming: { add: { commutative: true } }, imposeContext: { add: { commutative: false } } }, { - s: 'v*c -> c*v', + s: 'vd*cd -> cd*vd', assuming: { multiply: { commutative: true } }, imposeContext: { multiply: { commutative: false } } }, @@ -890,9 +897,8 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( } } 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 + // according to the first one or two characters of the symbol node name. + // These meanings are expalined in the documentation for simplify() if (rule.name.length === 0) { throw new Error('Symbol in rule has 0 length...!?') } @@ -901,29 +907,83 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( if (rule.name !== node.name) { return [] } - } else 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 (!isConstantNode(node)) { - 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 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) + // wildcards are composed of up to two alphabetic or underscore characters + switch (rule.name[1] >= 'a' && rule.name[1] <= 'z' ? rule.name.substring(0, 2) : rule.name[0]) { + case 'n': + case '_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 + break + case 'c': + case 'cl': + // rule matches a ConstantNode + if (isConstantNode(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + case 'v': + // rule matches anything other than a ConstantNode + if (!isConstantNode(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + case 'vl': + // rule matches VariableNode + if (isVariableNode(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + case 'cd': + // rule matches a ConstantNode or unaryMinus-wrapped ConstantNode + if (isNumericNode(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + case 'vd': + // rule matches anything other than a ConstantNode or unaryMinus-wrapped ConstantNode + if (!isNumericNode(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + case 'ce': + // rule matches expressions that have a constant value + if (isConstantExpression(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + case 've': + // rule matches expressions that do not have a constant value + if (!isConstantExpression(node)) { + res[0].placeholders[rule.name] = node + } else { + // mis-match: rule does not encompass current node + return [] + } + break + default: + throw new Error('Invalid symbol in rule: ' + rule.name) + } } } else if (rule instanceof ConstantNode) { // Literal constant must match exactly diff --git a/src/function/algebra/simplify/wildcards.js b/src/function/algebra/simplify/wildcards.js new file mode 100644 index 0000000000..73f32ef20e --- /dev/null +++ b/src/function/algebra/simplify/wildcards.js @@ -0,0 +1,19 @@ +import { isConstantNode, isFunctionNode, isOperatorNode, isParenthesisNode } from '../../../utils/is.js' +export { isConstantNode, isSymbolNode as isVariableNode } from '../../../utils/is.js' + +export function isNumericNode (x) { + return isConstantNode(x) || (isOperatorNode(x) && x.isUnary() && isConstantNode(x.args[0])) +} + +export function isConstantExpression (x) { + if (isConstantNode(x)) { // Basic Constant types + return true + } + if ((isFunctionNode(x) || isOperatorNode(x)) && x.args.every(isConstantExpression)) { // Can be constant depending on arguments + return true + } + if (isParenthesisNode(x) && isConstantExpression(x.content)) { // Parenthesis are transparent + return true + } + return false // Probably missing some edge cases +} diff --git a/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index 8c311c33dc..7a226cff5f 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -48,6 +48,80 @@ describe('simplify', function () { assert.strictEqual(math.simplify(left).evaluate(scope), math.parse(right).evaluate(scope)) } + describe('wildcard types', function () { + it('should match constants (\'c\' and \'cl\') correctly', function () { + // c, cl - ConstantNode + simplifyAndCompare('1', '5', [{ l: 'c', r: '5' }]) + simplifyAndCompare('-1', '-5', [{ l: 'c', r: '5' }]) + simplifyAndCompare('a', 'a', [{ l: 'c', r: '5' }]) + simplifyAndCompare('2 * a', '5 * a', [{ l: 'c', r: '5' }]) + + simplifyAndCompare('1', '5', [{ l: 'cl', r: '5' }]) + simplifyAndCompare('-1', '-5', [{ l: 'cl', r: '5' }]) + simplifyAndCompare('a', 'a', [{ l: 'cl', r: '5' }]) + simplifyAndCompare('2 * a', '5 * a', [{ l: 'cl', r: '5' }]) + }) + + it('should match variables (\'v\') correctly', function () { + // v - Non-ConstantNode + simplifyAndCompare('1', '1', [{ l: 'v', r: '5' }]) + simplifyAndCompare('-1', '5', [{ l: 'v', r: '5' }]) + simplifyAndCompare('a', '5', [{ l: 'v', r: '5' }]) + simplifyAndCompare('2 * a', '5', [{ l: 'v', r: '5' }]) + }) + + it('should match variable literals (\'vl\') correctly', function () { + // vl - Variable + simplifyAndCompare('1', '1', [{ l: 'vl', r: '5' }]) + simplifyAndCompare('-1', '-1', [{ l: 'vl', r: '5' }]) + simplifyAndCompare('a', '5', [{ l: 'vl', r: '5' }]) + simplifyAndCompare('2 * a', '2 * 5', [{ l: 'vl', r: '5' }]) + }) + + it('should match decimal literals (\'cd\') correctly', function () { + // cd - Number + simplifyAndCompare('1', '5', [{ l: 'cd', r: '5' }]) + simplifyAndCompare('-1', '5', [{ l: 'cd', r: '5' }]) + simplifyAndCompare('a', 'a', [{ l: 'cd', r: '5' }]) + simplifyAndCompare('2 * a', '5 * a', [{ l: 'cd', r: '5' }]) + }) + + it('should match non-decimals (\'vd\') correctly', function () { + // vd - Non-number + simplifyAndCompare('1', '1', [{ l: 'vd', r: '5' }]) + simplifyAndCompare('-1', '-1', [{ l: 'vd', r: '5' }]) + simplifyAndCompare('a', '5', [{ l: 'vd', r: '5' }]) + simplifyAndCompare('2 * a', '5', [{ l: 'vd', r: '5' }]) + }) + + it('should match constant expressions (\'ce\') correctly', function () { + // ce - Constant Expression + simplifyAndCompare('1', '5', [{ l: 'ce', r: '5' }]) + simplifyAndCompare('-1', '5', [{ l: 'ce', r: '5' }]) + simplifyAndCompare('a', 'a', [{ l: 'ce', r: '5' }]) + simplifyAndCompare('2 * a', '5 * a', [{ l: 'ce', r: '5' }]) + simplifyAndCompare('2 ^ 32 + 3', '5', [{ l: 'ce', r: '5' }]) + simplifyAndCompare('2 ^ 32 + x', '5 + x', [{ l: 'ce', r: '5' }]) + simplifyAndCompare('2 ^ 32 + pi', '5', [{ l: 'ce', r: '5' }], { pi: math.pi }) + }) + + it('should match variable expressions (\'ve\') correctly', function () { + // ve - Variable Expression + simplifyAndCompare('1', '1', [{ l: 've', r: '5' }]) + simplifyAndCompare('-1', '-1', [{ l: 've', r: '5' }]) + simplifyAndCompare('a', '5', [{ l: 've', r: '5' }]) + simplifyAndCompare('2 * a', '2 * 5', [{ l: 've', r: '5' }]) + simplifyAndCompare('2 ^ 32 + 3', '2 ^ 32 + 3', [{ l: 've', r: '5' }]) + simplifyAndCompare('2 ^ 32 + x', '2 ^ 32 + 5', [{ l: 've', r: '5' }]) + simplifyAndCompare('2 ^ 32 + pi', '2 ^ 32 + 3.141592653589793', [{ l: 've', r: '5' }], { pi: math.pi }) + }) + + it('should correctly separate constant and variable expressions', function () { + simplifyAndCompare('2 * a ^ 5 * 8', '5', [{ l: 'ce * ve', r: '5' }]) + simplifyAndCompare('2 * a ^ 5 * 8 + 3', '5 + 3', [{ l: 'ce * ve', r: '5' }]) + }) + }) + it('should not change the value of the function', function () { simplifyAndCompareEval('3+2/4+2*8', '39/2') simplifyAndCompareEval('x+1+x', '2x+1', { x: 7 }) @@ -55,6 +129,13 @@ describe('simplify', function () { simplifyAndCompareEval('x^2+x-3+x^2', '2x^2+x-3', { x: 7 }) }) + it('should place constants at the end of expressions unless subtraction takes priority', function () { + simplifyAndCompare('2 + x', 'x + 2') + simplifyAndCompare('-2 + x', 'x - 2') + simplifyAndCompare('-2 + -x', '-x - 2') + simplifyAndCompare('2 + -x', '2 - x') + }) + it('should simplify exponents', function () { // power rule simplifyAndCompare('(x^2)^3', 'x^6') @@ -278,6 +359,11 @@ describe('simplify', function () { simplifyAndCompare('x*2*x', '2*x^2') }) + it('should preserve seperated numerical factors', function () { + simplifyAndCompare('2 * (2 * x + y)', '2 * (2 * x + y)') + simplifyAndCompare('-2 * (-2 * x + y)', '-(2 * (y - 2 * x))') // Failed before introduction of vd in #1915 + }) + it('should handle nested exponentiation', function () { simplifyAndCompare('(x^2)^3', 'x^6') simplifyAndCompare('(x^y)^z', 'x^(y*z)') @@ -391,6 +477,34 @@ describe('simplify', function () { assert.strictEqual(simplified.toString({ implicit: 'hide' }), '2 x') }) + it('should offer differentiation for constants of either sign', function () { + // Mostly just an alternative formatting preference + // Allows for basic constant fractions to be kept separate from a variable expressions + // see https://github.com/josdejong/mathjs/issues/1406 + const rules = math.simplify.rules.slice() + const index = rules.findIndex(rule => (rule.s ? rule.s.split('->')[0].trim() : rule.l) === 'n*(n1/n2)') + rules.splice( + index, 1, + { + s: 'cd*(cd1/cd2) -> (cd*cd1)/cd2', + assuming: { multiply: { associative: true } } + }, + { + s: 'n*(n1/vd2) -> (n*n1)/vd2', + assuming: { multiply: { associative: true } } + }, + { + s: 'n*(vd1/n2) -> (n*vd1)/n2', + assuming: { multiply: { associative: true } } + } + ) + assert.strictEqual(math.simplify('(1 / 2) * a', rules).toString({ parenthesis: 'all' }), '(1 / 2) * a') + assert.strictEqual(math.simplify('-(1 / 2) * a', rules).toString({ parenthesis: 'all' }), '((-1) / 2) * a') + assert.strictEqual(math.simplify('(1 / 2) * 3', rules).toString({ parenthesis: 'all' }), '3 / 2') + assert.strictEqual(math.simplify('(1 / x) * a', rules).toString({ parenthesis: 'all' }), 'a / x') + assert.strictEqual(math.simplify('-(1 / x) * a', rules).toString({ parenthesis: 'all' }), '-(a / x)') + }) + describe('expression parser', function () { it('should evaluate simplify containing string value', function () { const res = math.evaluate('simplify("2x + 3x")')