Skip to content

sqrt(x) * sqrt(y) is simplified to sqrt(x * y), which is wrong at x = y = -1 #801

Description

@Rafael-SOWNet

Simplify rewrites sqrt(x) * sqrt(y) to sqrt(x * y), which is false across the branch cuts.

"sqrt(x) * sqrt(y)".ToEntity().Simplify()          // sqrt(x * y)

// at x = y = -1
"sqrt(x) * sqrt(y)".ToEntity().Substitute("x", -1).Substitute("y", -1).Evaled   // -1
"sqrt(x * y)".ToEntity().Substitute("x", -1).Substitute("y", -1).Evaled         //  1

sqrt(-1) * sqrt(-1) is i * i = -1. sqrt((-1)(-1)) is sqrt(1) = 1. The rewrite turns one into the other.

Where

Patterns.PowerRules:

// {1} ^ n * {2} ^ n = ({1} * {2}) ^ n
Mulf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 * any2, any3),
Divf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 / any2, any3),

Both are unconditional. (ab)^n = a^n b^n holds for a whole n whatever the signs, and for positive real bases whatever the exponent, but not for a fractional n over the complex plane in general — the two sides can differ by a full turn of the argument.

The same shape as #752

#752 was the same mistake in two neighbouring rules — sqrt(x^2) -> x and sqrt(-x) -> i*sqrt(x) — and was fixed by asking the condition each already required. The ({}^{})^{} rule immediately above these two now carries exactly the guard that is missing here:

Powf(Powf(var any1, var any2), var any3)
    when any3 is Integer || any1.Evaled is Real { IsPositive: true }
    => new Powf(any1, any2 * any3),

How it was found

Not by a harness. Writing a perfect-square collapse for #176, the rule needed to decide whether a cross term is twice the product of two roots, and asked Simplify — which answered using this rewrite and made the collapse fire on x + 2*sqrt(x*y) + y, whose collapsed form is -4 at x = y = -1 where the sum is 0. That fix now gates the symbolic match on a numeric check rather than trusting the answer, so the wrong collapse no longer ships; this issue is the cause underneath it.

simpsweep does not catch it because it generates single-variable expressions, and this needs two variables both taken negative at once.

Suggested fix

The same guard the rule above carries: fire when the exponent is an integer, or when both bases are decidably positive reals. sqrt(2) * sqrt(3) -> sqrt(6) keeps working; sqrt(x) * sqrt(y) stops.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions