Skip to content

Commit

Permalink
More simplify wildcards (#1915)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
thatcomputerguy0101 and josdejong authored Nov 15, 2022
1 parent c921121 commit 77f94c4
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 42 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Tom Hickson <[email protected]>
dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Markel F <[email protected]>
Lazersmoke <[email protected]>
Alan Everett <[email protected]>
SungJinWoo-SL <[email protected]>
Nick Ewing <[email protected]>
jos <[email protected]>
Expand Down
144 changes: 102 additions & 42 deletions src/function/algebra/simplify.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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' },
Expand All @@ -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' },
Expand All @@ -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 } }
},
{
Expand All @@ -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 } }
},

Expand All @@ -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 } }
},
Expand Down Expand Up @@ -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...!?')
}
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/function/algebra/simplify/wildcards.js
Original file line number Diff line number Diff line change
@@ -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
}
114 changes: 114 additions & 0 deletions test/unit-tests/function/algebra/simplify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,94 @@ 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 })
simplifyAndCompareEval('x+1+2x', '3x+1', { x: 7 })
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')
Expand Down Expand Up @@ -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)')
Expand Down Expand Up @@ -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")')
Expand Down

0 comments on commit 77f94c4

Please sign in to comment.