Skip to content

Commit

Permalink
feat: export simplifyConstant
Browse files Browse the repository at this point in the history
  Now that simplifyCore does not do any constant folding, clients may
  wish to access that behavior via simplifyConstant. Moreover, exporting it
  makes it easier to use in custom rule lists for simplify().

  Also adds docs, embedded docs, and tests for simplifyConstant().

  Also fixes simplifyCore() on logical functions (they always return boolean,
  rather than "short-circuiting").

  Resolves #2459.
  • Loading branch information
gwhitney committed Mar 20, 2022
1 parent e8d4f79 commit 32dcbb0
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 56 deletions.
2 changes: 2 additions & 0 deletions src/expression/embeddedDocs/embeddedDocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { qrDocs } from './function/algebra/qr.js'
import { rationalizeDocs } from './function/algebra/rationalize.js'
import { resolveDocs } from './function/algebra/resolve.js'
import { simplifyDocs } from './function/algebra/simplify.js'
import { simplifyConstantDocs } from './function/algebra/simplifyConstant.js'
import { simplifyCoreDocs } from './function/algebra/simplifyCore.js'
import { sluDocs } from './function/algebra/slu.js'
import { symbolicEqualDocs } from './function/algebra/symbolicEqual.js'
Expand Down Expand Up @@ -334,6 +335,7 @@ export const embeddedDocs = {
leafCount: leafCountDocs,
resolve: resolveDocs,
simplify: simplifyDocs,
simplifyConstant: simplifyConstantDocs,
simplifyCore: simplifyCoreDocs,
symbolicEqual: symbolicEqualDocs,
rationalize: rationalizeDocs,
Expand Down
16 changes: 16 additions & 0 deletions src/expression/embeddedDocs/function/algebra/simplifyConstant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const simplifyConstantDocs = {
name: 'simplifyConstant',
category: 'Algebra',
syntax: [
'simplifyConstant(expr)',
'simplifyConstant(expr, options)'
],
description: 'Replace constant subexpressions of node with their values.',
examples: [
'simplifyConatant("(3-3)*x")',
'simplifyConstant(parse("z-cos(tau/8)"))'
],
seealso: [
'simplify', 'simplifyCore', 'evaluate'
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const simplifyCoreDocs = {
'simplifyCore(parse("(x+0)*2"))'
],
seealso: [
'simplify', 'evaluate'
'simplify', 'simplifyConstant', 'evaluate'
]
}
1 change: 1 addition & 0 deletions src/factoriesAny.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export { createCatalan } from './function/combinatorics/catalan.js'
export { createComposition } from './function/combinatorics/composition.js'
export { createLeafCount } from './function/algebra/leafCount.js'
export { createSimplify } from './function/algebra/simplify.js'
export { createSimplifyConstant } from './function/algebra/simplifyConstant.js'
export { createSimplifyCore } from './function/algebra/simplifyCore.js'
export { createResolve } from './function/algebra/resolve.js'
export { createSymbolicEqual } from './function/algebra/symbolicEqual.js'
Expand Down
1 change: 1 addition & 0 deletions src/factoriesNumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export { createChain } from './type/chain/function/chain.js'
// algebra
export { createResolve } from './function/algebra/resolve.js'
export { createSimplify } from './function/algebra/simplify.js'
export { createSimplifyConstant } from './function/algebra/simplifyConstant.js'
export { createSimplifyCore } from './function/algebra/simplifyCore.js'
export { createDerivative } from './function/algebra/derivative.js'
export { createRationalize } from './function/algebra/rationalize.js'
Expand Down
20 changes: 2 additions & 18 deletions src/function/algebra/rationalize.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { isInteger } from '../../utils/number.js'
import { factory } from '../../utils/factory.js'
import { createSimplifyConstant } from './simplify/simplifyConstant.js'

const name = 'rationalize'
const dependencies = [
Expand All @@ -14,6 +13,7 @@ const dependencies = [
'divide',
'pow',
'parse',
'simplifyConstant',
'simplifyCore',
'simplify',
'?bignumber',
Expand Down Expand Up @@ -42,6 +42,7 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({
divide,
pow,
parse,
simplifyConstant,
simplifyCore,
simplify,
fraction,
Expand All @@ -58,23 +59,6 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({
SymbolNode,
ParenthesisNode
}) => {
const simplifyConstant = createSimplifyConstant({
typed,
config,
mathWithTransform,
matrix,
fraction,
bignumber,
AccessorNode,
ArrayNode,
ConstantNode,
FunctionNode,
IndexNode,
ObjectNode,
OperatorNode,
SymbolNode
})

/**
* Transform a rationalizable expression in a rational fraction.
* If rational fraction is one variable polynomial then converts
Expand Down
20 changes: 2 additions & 18 deletions src/function/algebra/simplify.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { isConstantNode, isParenthesisNode } from '../../utils/is.js'
import { factory } from '../../utils/factory.js'
import { createUtil } from './simplify/util.js'
import { createSimplifyConstant } from './simplify/simplifyConstant.js'
import { hasOwnProperty } from '../../utils/object.js'
import { createEmptyMap, createMap } from '../../utils/map.js'

Expand All @@ -18,6 +17,7 @@ const dependencies = [
'isZero',
'equal',
'resolve',
'simplifyConstant',
'simplifyCore',
'?fraction',
'?bignumber',
Expand Down Expand Up @@ -47,6 +47,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
isZero,
equal,
resolve,
simplifyConstant,
simplifyCore,
fraction,
bignumber,
Expand All @@ -63,23 +64,6 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
SymbolNode
}
) => {
const simplifyConstant = createSimplifyConstant({
typed,
config,
mathWithTransform,
matrix,
fraction,
bignumber,
AccessorNode,
ArrayNode,
ConstantNode,
FunctionNode,
IndexNode,
ObjectNode,
OperatorNode,
SymbolNode
})

const { hasProperty, isCommutative, isAssociative, mergeContext, flatten, unflattenr, unflattenl, createMakeNodeFunction, defaultContext, realContext, positiveContext } =
createUtil({ FunctionNode, OperatorNode, SymbolNode })

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// TODO this could be improved by simplifying seperated constants under associative and commutative operators
import { isFraction, isMatrix, isNode, isArrayNode, isConstantNode, isIndexNode, isObjectNode, isOperatorNode } from '../../../utils/is.js'
import { factory } from '../../../utils/factory.js'
import { createUtil } from './util.js'
import { noBignumber, noFraction } from '../../../utils/noop.js'
import { isFraction, isMatrix, isNode, isArrayNode, isConstantNode, isIndexNode, isObjectNode, isOperatorNode } from '../../utils/is.js'
import { factory } from '../../utils/factory.js'
import { createUtil } from './simplify/util.js'
import { noBignumber, noFraction } from '../../utils/noop.js'

const name = 'simplifyConstant'
const dependencies = [
'typed',
'parse',
'config',
'mathWithTransform',
'matrix',
Expand All @@ -24,6 +24,7 @@ const dependencies = [

export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies, ({
typed,
parse,
config,
mathWithTransform,
matrix,
Expand All @@ -41,9 +42,50 @@ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies
const { isCommutative, isAssociative, allChildren, createMakeNodeFunction } =
createUtil({ FunctionNode, OperatorNode, SymbolNode })

function simplifyConstant (expr, options) {
return _ensureNode(foldFraction(expr, options))
}
/**
* simplifyConstant() takes a mathjs expression (either a Node representing
* a parse tree or a string which it parses to produce a node), and replaces
* any subexpression of it consisting entirely of constants with the computed
* value of that subexpression.
*
* Syntax:
*
* simplifyConstant(expr)
* simplifyConstant(expr, options)
*
* Examples:
*
* math.simplifyConstant('x + 4*3/6') // Node "x + 2"
* math.simplifyConstant('z cos(0)') // Node "z 1"
* math.simplifyConstant('(5.2 + 1.08)t', {exactFractions: false}) // Node "6.28 t"
*
* See also:
*
* simplify, simplifyCore, resolve, derivative
*
* @param {Node | string} node
* The expression to be simplified
* @param {Object} options
* Simplification options, as per simplify()
* @return {Node} Returns expression with constant subexpressions evaluated
*/
const simplifyConstant = typed('simplifyConstant', {
string: function (expr) {
return this(parse(expr), {})
},

'string, Object': function (expr, options) {
return this(parse(expr), options)
},

Node: function (node) {
return this(node, {})
},

'Node, Object': function (expr, options) {
return _ensureNode(foldFraction(expr, options))
}
})

function _removeFractions (thing) {
if (isFraction(thing)) {
Expand Down
40 changes: 33 additions & 7 deletions src/function/algebra/simplifyCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({
}) => {
const node0 = new ConstantNode(0)
const node1 = new ConstantNode(1)
const nodeT = new ConstantNode(true)
const nodeF = new ConstantNode(false)
// test if a node will always have a boolean value (true/false)
// not sure if this list is complete
function isAlwaysBoolean (node) {
return isOperatorNode(node) && ['and', 'not', 'or'].includes(node.op)
}

const { hasProperty, isCommutative } =
createUtil({ FunctionNode, OperatorNode, SymbolNode })
Expand Down Expand Up @@ -86,9 +93,9 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({
*
* See also:
*
* simplify, resolve, derivative
* simplify, simplifyConstant, resolve, derivative
*
* @param {Node} node
* @param {Node | string} node
* The expression to be simplified
* @param {Object} options
* Simplification options, as per simplify()
Expand Down Expand Up @@ -157,7 +164,12 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({
}
if (node.op === 'not') { // logical not
if (isOperatorNode(a0) && a0.isUnary() && a0.op === 'not') {
return a0.args[0]
// Has the effect of turning the argument into a boolean
// So can only eliminate the double negation if
// the inside is already boolean
if (isAlwaysBoolean(a0.args[0])) {
return a0.args[0]
}
}
}
let finish = true
Expand Down Expand Up @@ -244,18 +256,32 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({
if (node.op === 'and') {
if (isConstantNode(a0)) {
if (a0.value) {
return a1
if (isAlwaysBoolean(a1)) return a1
} else {
return a0
return nodeF
}
}
if (isConstantNode(a1)) {
if (a1.value) {
if (isAlwaysBoolean(a0)) return a0
} else {
return nodeF
}
}
}
if (node.op === 'or') {
if (isConstantNode(a0)) {
if (a0.value) {
return a0
return nodeT
} else {
return a1
if (isAlwaysBoolean(a1)) return a1
}
}
if (isConstantNode(a1)) {
if (a1.value) {
return nodeT
} else {
if (isAlwaysBoolean(a0)) return a0
}
}
}
Expand Down
38 changes: 38 additions & 0 deletions test/unit-tests/function/algebra/simplifyConstant.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// test simplifyConstant
import assert from 'assert'

import math from '../../../../src/defaultInstance.js'

describe('simplifyConstant', function () {
const testSimplifyConstant = function (expr, expected, opts = {}, simpOpts = {}) {
let actual = math.simplifyConstant(math.parse(expr), simpOpts).toString(opts)
assert.strictEqual(actual, expected)
actual = math.simplifyConstant(expr, simpOpts).toString(opts)
assert.strictEqual(actual, expected)
}

it('should evaluate constant subexpressions', () => {
testSimplifyConstant('2+2', '4')
testSimplifyConstant('x+3*5', 'x + 15')
testSimplifyConstant('f(sin(0))', 'f(0)')
testSimplifyConstant('[10/2, y, 8-4]', '[5, y, 4]')
})

it('should by default convert decimals into fractions', () => {
testSimplifyConstant('0.5 x', '1 / 2 x')
})

it('should coalesce constants in a multi-argument expression', () => {
testSimplifyConstant('3 + x + 7 + y', '10 + x + y')
testSimplifyConstant('3 * x * 7 * y', '21 * x * y')
})

it('should respect simplify options', () => {
testSimplifyConstant('0.5 x', '0.5 * x', { implicit: 'show' },
{ exactFractions: false })
testSimplifyConstant('0.001 x', '0.001 * x', { implicit: 'show' },
{ fractionsLimit: 999 })
testSimplifyConstant('3 * x * 7 * y', '3 * x * 7 * y', {},
{ context: { multiply: { commutative: false } } })
})
})
10 changes: 7 additions & 3 deletions test/unit-tests/function/algebra/simplifyCore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ describe('simplifyCore', function () {
testSimplifyCore('0/x', '0')
testSimplifyCore('~~(a | b)', 'a | b')
testSimplifyCore('not (not (p and q))', 'p and q')
testSimplifyCore('1 and done', 'done')
testSimplifyCore('1 and not done', 'not done')
testSimplifyCore('false and you(know, it)', 'false')
testSimplifyCore('false or bust', 'bust')
testSimplifyCore('6 or dozen/2', '6')
testSimplifyCore('(p or q) and "you"', 'p or q')
testSimplifyCore('something and ""', 'false')
testSimplifyCore('false or not(way)', 'not way')
testSimplifyCore('6 or dozen/2', 'true')
testSimplifyCore('(a and b) or 0', 'a and b')
testSimplifyCore('consequences or true', 'true')
testSimplifyCore('(1*x + y*0)*1+0', 'x')
testSimplifyCore('sin(x+0)*1', 'sin(x)')
testSimplifyCore('((x+0)*1)', 'x')
Expand Down
8 changes: 7 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,9 @@ declare namespace math {
*/
simplify: Simplify;

simplifyConstant(expr: MathNode | string, options?: SimplifyOptions);
simplifyCore(expr: MathNode | string, options?: SimplifyOptions);

/**
* Calculate the Sparse Matrix LU decomposition with full pivoting.
* Sparse Matrix A is decomposed in two matrices (L, U) and two
Expand Down Expand Up @@ -3288,6 +3291,8 @@ declare namespace math {
* Default value is 10000.
*/
fractionsLimit?: number;
consoleDebug?: boolean;
context?: object;
}

type SimplifyRule = { l: string; r: string } | string | ((node: MathNode) => MathNode);
Expand Down Expand Up @@ -3761,7 +3766,8 @@ declare namespace math {
*/
simplify(rules?: SimplifyRule[], scope?: object): MathJsChain;

simplifyCore(expr: MathNode): MathNode;
simplifyConstant(options?: SimplifyOptions): MathJsChain;
simplifyCore(options?: SimplifyOptions): MathJsChain;

/**
* Calculate the Sparse Matrix LU decomposition with full pivoting.
Expand Down
Loading

0 comments on commit 32dcbb0

Please sign in to comment.