Skip to content

Simplify turns ln(e^x) into x, which is wrong off the strip Im x in (-pi, pi] #902

Description

@Rafael-SOWNet

Simplify turns ln(e^x) into x, which is wrong wherever Im x leaves (-pi, pi]. The rule is
Patterns.Power.cs:158:

Logf(var any1, Powf(var any2, var any3)) => any3 * MathS.Log(any1, any2),

log_b(a^c) = c * log_b(a) needs a to be a positive real, or c to be real together with a branch
choice. Neither is required here.

Measured on master (5397c6f9), .NET 10:

"ln(e^x)".Simplify()        =>  x
"log(2, 2^x)".Simplify()    =>  x

eval(ln(e^(3*pi*i)))        =>  3.14159...i      (pi*i, since e^(3*pi*i) is -1)
eval(3*pi*i)                =>  9.42477...i

So the two sides differ by 2*pi*i at that point. MathS.Settings.Codomain defaults to
Domain.Complex, which makes this unsound on the library's own default setting.

It is also unsound for a negative real base: log(2, 64) is 6, while 2 * log(2, -8) is
6 + 9.0647...i.

This is the half of #884 that was never fixed

#884 reported five rules of one shape — an
identity that holds on a strip or an interval, written as though it held everywhere. The four
inverse-trigonometric ones were fixed in
#885 and #884 was closed with them; this
logarithm rule was analysed in the same issue and left alone, so it now has no open issue of its own.
Filing it so it is not lost a second time.

The analysis there still holds, including the part that makes this hard to catch: at the shape
log(2, x^2) the rewritten form currently loses the complexity contest and the expression is left
alone. The wrong candidate is still generated, so a neighbouring expression where it happens to be
shorter gets the wrong answer while this one does not — the PolynomialLongDivision failure mode from
#800.

What the guard has to be

ln(a^c) = c * ln(a) exactly when Im(c * ln a) lands in (-pi, pi]. Sufficient and decidable:

  • a a positive real and c real — then c * ln a is real and the identity is unconditional;
  • the reading is real analysis (MathS.Settings.Codomain.Value is Domain.Real), which is how
    Patterns.EqualityInequality.cs already asks this question, paired with IsKnownReal.

A symbolic exponent under the default complex reading is not decidable, so the honest answer there
is to leave the expression as written — the same treatment as the four rules of #884 and as
arctan(x) + arccotan(x) in #887: decide,
or decline.

The coverage cost needs measuring rather than guessing, and it is the reason this is filed rather
than fixed in the same breath.
ln(e^x) -> x is common in solver output, so declining it by default
may move casbench, rootcheck and the suite. Worth knowing before choosing between the guard above
and a narrower one. The opposite direction, e^(ln z) = z, is unconditional for z != 0 and is not in
question here.

Related, and deliberately not bundled

Patterns.Power.cs:159, Logf(x, x) => 1 provided x > 0, is the same file disagreeing with itself
about whether these rules need guarding — but its condition is a different question. log_x(x) is
ln x / ln x, which is 1 for every x outside {0, 1} including negative ones, while the attached
x > 0 matches Logf's declared DomainCondition. Whether the declared domain or the evaluation
is authoritative is #721, recorded as
needing a decision under #890. It should be
settled there, not by a guard written here.

How it was found

Written up first in #884 by reading the rules rather than sampling them. work/boundcheck now confirms
it from the other side: composing every unary node with every other and comparing at points where an
assumption fails, ln(e^x) and log(2, 2^x) are two of its four remaining disagreements, both at
x = 3*pi*i. All four sampling harnesses stay green — simpsweep samples real points, where this
rule is correct.

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