Skip to content

Apply the two power rewrites only where they are true (#752) - #758

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/power-of-power-branch
Aug 6, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
fix/power-of-power-branch

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Closes #752.

What was wrong

"sqrt(x ^ 2)".Simplify()       // x            — at x = -0.63 that is -0.63 where it is 0.63
"(x ^ 2) ^ (3/2)".Simplify()   // x ^ 3        — at x = -2 that is -8 where it is 8
"sqrt(-x)".Simplify()          // i * sqrt(x)  — at x = -0.63 that is -0.7937, not 0.7937

Wrong by a whole sign, not a rounding. Two rules, each applied without the condition it needs:

  • (a^b)^c = a^(b*c) holds for a positive a, and for any a when c is whole — (a^b)^3 is a^b multiplied by itself three times however a is signed.
  • (a*x)^c = a^c * x^c holds for a positive a, and again for a whole c.

Both are now restricted to exactly that, and unchanged where they hold: (x^(1/2))^2 is still x, (-x)^2 is still x^2, (2^2)^(1/2) is still 2, (x^2)^2 is still x^4, sqrt(4*x) and (3*x)^2 are untouched.

sqrt(x^2) is abs(x), and the library does not write that for you — it leaves the expression alone rather than answering something false. Writing abs requires knowing the expression is real, which #719's codomain (merged in #755) now makes sayable and the simplifier does not yet read.

Measured

work/simpsweep goes from 30 disagreements to 0 over 10463 generated expressions. Those 30 were the whole of what it had left after #751.

  • Suite 5147 → 5169 passed / 0 failed, F# 130/130.
  • Corpus 112/117, 0 wrong / 0 error / 0 timeout, every verdict and answer byte-identical.
  • work/rootcheck 596/596 clean, work/propcheck 0 failures.

Two test expectations move, and they are different cases

One got better. PowerQuotientGatheringTest pinned (sqrt(x) + 1)^x / sqrt(x)^x as deliberately not gathered, because gathering it would have squared the numerator. It gathers now, and without that trade: sqrt(x)^x no longer flattens to x^(x/2), so both exponents stay x and the ordinary pairing rule reaches it. The gap that file is about was a rewrite running ahead of the pairing, and one fewer rewrite is one fewer way to run ahead of it.

One got worse, and it is a fudge coming out rather than a regression to hide. SolveInequality.Test pinned (x - a)(x + a) <= 0 as [a; -a]. That is right for a < 0 and empty for a > 0, and it read that way only because sqrt(4a^2) was simplifying to 2a. It now answers { a, -a } \/ (sqrt(a^2); -sqrt(a^2)), whose interval is empty for either sign.

Neither form is right for both signs, because the solver has no case split on the sign of a symbolic coefficient. Filed as #757, and the expectation is pinned as it now stands with a comment pointing there rather than deleted — it is still evidence, just of something other than correctness. Concrete coefficients are unaffected: (x - 2)(x + 2) <= 0 is still [-2; 2].

That is the one judgement call in this PR and it is worth your eye: it trades an answer that was right half the time for one that is right neither, in exchange for removing 30 measured wrong answers from Simplify, which is the more used API. AGENTS.md puts correctness ahead of compatibility, which is how I read it, but the inequality output is a real loss and I would rather you saw it stated than buried.

🤖 Generated with Claude Code

    sqrt(x^2)      came back as x,      which at -0.63 is -0.63 where it is 0.63
    (x^2)^(3/2)    came back as x^3,    which at -2 is -8 where it is 8
    sqrt(-x)       came back as i*sqrt(x), which at -0.63 is -0.7937, not 0.7937

Two rules, each applied without the condition it needs. `(a^b)^c = a^(b*c)` holds
for a positive `a`, and for any `a` when `c` is whole -- `(a^b)^3` is `a^b`
multiplied by itself three times however `a` is signed. `(a*x)^c = a^c * x^c`
holds for a positive `a`, and again for a whole `c`. Both are restricted to
exactly that.

Where they hold they are unchanged: `(x^(1/2))^2` is still `x`, `(-x)^2` is
still `x^2`, `(2^2)^(1/2)` is still `2`, `(x^2)^2` is still `x^4`.

`sqrt(x^2)` is `abs(x)` and the library does not write that for you. It leaves
the expression alone rather than answering something false; writing `abs`
requires knowing the expression is real, which the codomain added in #755 now
makes sayable and the simplifier does not yet read.

Two test expectations move, and they are different cases:

- `PowerQuotientGatheringTest` pinned `(sqrt(x) + 1)^x / sqrt(x)^x` as
  deliberately *not* gathered, because gathering it would have squared the
  numerator. **It gathers now, and better**: `sqrt(x)^x` no longer flattens to
  `x^(x/2)`, so both exponents stay `x` and the ordinary pairing rule reaches
  it. The gap that file is about was a rewrite running ahead of the pairing, and
  one less rewrite is one less way to run ahead of it.

- `SolveInequality.Test` pinned `(x - a)(x + a) <= 0` as `[a; -a]`. **That got
  worse and it is a fudge coming out**, not a regression to hide: `[a; -a]` is
  right for `a < 0` and empty for `a > 0`, and it read that way only because
  `sqrt(4a^2)` was simplifying to `2a`. It now answers
  `{ a, -a } \/ (sqrt(a^2); -sqrt(a^2))`, whose interval is empty for either
  sign. Neither is right for both, because the solver has no case split on the
  sign of a symbolic coefficient. Filed as #757 and pinned as it now stands with
  a comment pointing there, rather than deleted -- the expectation is still
  evidence, of something other than correctness.

Found by `work/simpsweep`, which generates the expressions it checks rather than
listing them. **Its count of disagreements over 10463 expressions goes 30 to 0**,
and those 30 were the whole of what it had left.

Suite 5147 -> 5169 passed / 0 failed, F# 130/130, corpus 112/117 with every
verdict and answer byte-identical, rootcheck 596/596, propcheck 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit 03be4eb into master Aug 6, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/power-of-power-branch branch August 6, 2026 04:04
Rafael-SOWNet added a commit that referenced this pull request Aug 7, 2026
PR #758 (#779) pinned four integrands as a *separate* known gap, declined rather
than hung, so that they would not be read as the defect that file is about. PR
#786 (#781) fixed that gap, and the two were cut independently from master, so
neither could carry the other's half of this.

The assertion is inverted rather than deleted, because these four are the
evidence that the two changes compose: distributing `sin(x)^4 * (5 - 6 sin(x)^2)`
over its sum is what *produces* `sin(x)^4 * (-6) * sin(x)^2`, and merging the two
powers is what makes that answerable. Neither PR could show that on its own, and
a deleted test would have left it unshown.

5442 pass, 0 fail on merged master.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simplify collapses (x^2)^(1/2) to x and sqrt(-x) to i*sqrt(x), both wrong for negative x

1 participant