Skip to content

Commit 3c54623

Browse files
authored
fix(simplify): Correct regression in simplify (#2394)
* fix(simplify): Correct regression in simplify Also adds a 'debugConsole' option to simplify() so that it is possible to see the effect of each rule. This was critical to identifying the problem, which was that recent changes ended up with `simplifyConstant` in the wrong position in the list of rules. Correcting that also removed the need for the two rules coalescing negations with constants. Resolves #2393. * fix(simplify): Correct another regression based on rule ordering Disccovered that `x - (y-y+x)` had also stopped simplifying due to recent changes, again because of re-ordering of the rules. So added it to the tests, and fixed the rule ordering (adding a more extensive comment about it). A big part of the reason that rule ordering is so sensitive is that the reduction engine only checks once in each pass for each rule whether it matches. So an alternate fix to changing the rule ordering back would have been to re-check each rule after it's applied (in case its application created new instances of the rule) but since the re-ordering worked, that seemed simpler as a fix for now.
1 parent bfa42ec commit 3c54623

File tree

2 files changed

+34
-12
lines changed

2 files changed

+34
-12
lines changed

src/function/algebra/simplify.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,14 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
140140
* - [Symbolic computation - Simplification (Wikipedia)](https://en.wikipedia.org/wiki/Symbolic_computation#Simplification)
141141
*
142142
* An optional `options` argument can be passed as last argument of `simplify`.
143-
* There is currently one option available:
144-
* - `exactFractions`: a boolean which is `true` by default.
145-
* - `fractionsLimit`: when `exactFractions` is true, a fraction will be returned
146-
* only when both numerator and denominator are smaller than `fractionsLimit`.
147-
* Default value is 10000.
143+
* Currently available options (defaults in parentheses):
144+
* - `consoleDebug` (false): whether to write the expression being simplified
145+
and any changes to it, along with the rule responsible, to console
146+
* - `exactFractions` (true): whether to try to convert all constants to
147+
exact rational numbers.
148+
* - `fractionsLimit` (10000): when `exactFractions` is true, constants will
149+
be expressed as fractions only when both numerator and denominator
150+
are smaller than `fractionsLimit`.
148151
*
149152
* Syntax:
150153
*
@@ -225,6 +228,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
225228
},
226229

227230
'Node, Array, Map, Object': function (expr, rules, scope, options) {
231+
const debug = options.consoleDebug
228232
rules = _buildRules(rules)
229233
let res = resolve(expr, scope)
230234
res = removeParens(res)
@@ -233,12 +237,26 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
233237
while (!visited[str]) {
234238
visited[str] = true
235239
_lastsym = 0 // counter for placeholder symbols
240+
let laststr = str
241+
if (debug) console.log('Working on: ', str)
236242
for (let i = 0; i < rules.length; i++) {
243+
let rulestr = ''
237244
if (typeof rules[i] === 'function') {
238245
res = rules[i](res, options)
246+
if (debug) rulestr = rules[i].name
239247
} else {
240248
flatten(res)
241249
res = applyRule(res, rules[i])
250+
if (debug) {
251+
rulestr = `${rules[i].l.toString()} -> ${rules[i].r.toString()}`
252+
}
253+
}
254+
if (debug) {
255+
const newstr = res.toString({ parenthesis: 'all' })
256+
if (newstr !== laststr) {
257+
console.log('Applying', rulestr, 'produced', newstr)
258+
laststr = newstr
259+
}
242260
}
243261
unflattenl(res) // using left-heavy binary tree here since custom rule functions may expect it
244262
}
@@ -309,11 +327,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
309327
{ l: 'n/n1^n2', r: 'n*n1^-n2' }, // temporarily replace 'divide' so we can further flatten the 'multiply' operator
310328
{ l: 'n/n1', r: 'n*n1^-1' },
311329

312-
// remove parenthesis in the case of negating a quantity
313-
{ l: 'n1 + (n2 + n3)*(-1)', r: 'n1 + n2*(-1) + n3*(-1)' },
314-
// subsume resulting -1 into constants where possible
315-
{ l: '(-1) * c', r: '-c' },
316-
{ l: '(-1) * (-c)', r: 'c' },
330+
simplifyConstant,
317331

318332
// expand nested exponentiation
319333
{ l: '(n ^ n1) ^ n2', r: 'n ^ (n1 * n2)' },
@@ -330,9 +344,15 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
330344
{ l: 'n3*n1 + n3*n2', r: 'n3*(n1+n2)' }, // All sub-monomials tried there.
331345
{ l: 'n*c + c', r: '(n+1)*c' },
332346

333-
simplifyConstant,
347+
// remove parenthesis in the case of negating a quantity
348+
// (It might seem this rule should precede collecting like terms,
349+
// but putting it after gives another chance of noticing like terms,
350+
// and any new like terms produced by this will be collected
351+
// on the next pass through all the rules.)
352+
{ l: 'n1 + (n2 + n3)*(-1)', r: 'n1 + n2*(-1) + n3*(-1)' },
334353

335-
{ l: '(-n)*n1', r: '-(n*n1)' }, // make factors positive (and undo 'make non-constant terms positive')
354+
// make factors positive (and undo 'make non-constant terms positive')
355+
{ l: '(-n)*n1', r: '-(n*n1)' },
336356

337357
// final ordering of constants
338358
{ l: 'c+v', r: 'v+c', context: { add: { commutative: false } } },

test/unit-tests/function/algebra/simplify.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,14 @@ describe('simplify', function () {
270270
simplifyAndCompare('x^2+x+3+x^2', '2*x^2+x+3')
271271
simplifyAndCompare('x+1+2x', '3*x+1')
272272
simplifyAndCompare('x-1+x', '2*x-1')
273+
simplifyAndCompare('2-(x+1)', '1-x') // #2393
273274
simplifyAndCompare('x-1-2x+2', '1-x')
274275
})
275276

276277
it('should collect like terms that are embedded in other terms', function () {
277278
simplifyAndCompare('10 - (x - 2)', '12 - x')
278279
simplifyAndCompare('x - (y + x)', '-y')
280+
simplifyAndCompare('x - (y - y + x)', '0')
279281
simplifyAndCompare('x - (y - (y - x))', '0')
280282
simplifyAndCompare('5 + (5 * x) - (3 * x) + 2', '2*x+7')
281283
})

0 commit comments

Comments
 (0)