Skip to content

Make a logarithm behave like the division it is defined as (#890) - #893

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
fix/log-base-one
Aug 11, 2026
Merged

Make a logarithm behave like the division it is defined as (#890)#893
Rafael-SOWNet merged 2 commits into
masterfrom
fix/log-base-one

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

log_b(z) is ln(z) / ln(b). Three answers did not follow from that, and one of them was 0 where
every division by zero in this library is NaN.

Closes the second half of #890 — the wrong answers. The first half, the disagreement between
DomainCondition and evaluation, needs a decision and is left open.

What changed

was is
log(1, 1) 0 NaN — it is 0/0
log(1, 2) +oo NaN — dividing by ln 1 = 0 has no signed answer
log(b, 1) 0 for any base, including 1 0 provided not b = 1
log(1/2, 0), any base below 1 -oo +oo
log(2, 0), any base above 1 -oo, unchanged
log(x, 0) symbolic -oo left as written

0/0, 2/0 and -2/0 are all NaN here, so the library already has a settled convention for
dividing by zero and the logarithm simply was not following it.

Causes, which are two different ones

Number.Log had a shortcut that disagreed with its own fallback. For a positive real base it uses
EDecimal.LogN, and LogN answers 0 for log_1(1) and +oo for log_1(2) — where falling through
to Ln(x)/Ln(base), three lines below, gives NaN for both. A base of 1 is now excluded from the
shortcut, so the two paths agree. That is the whole fix for the first two rows: no new arithmetic,
just stop taking a shortcut that is wrong at one point.

Two arms of Logf.InnerSimplify stated no assumption. log(b, 1) -> 0 is 0/ln(b), which is 0
for every base but 1; log(b, 0) -> -oo is -oo/ln(b), whose sign follows the sign of ln(b),
so it is +oo for a base between 0 and 1.

A condition is right for log(b, 1), and this is worth being explicit about because the two
previous fixes in this family went the other way. In #884 and #887 attaching a condition would have
been wrong, because arcsin(sin(3)) is defined and merely has a different value. Here, at b = 1, the
expression is genuinely undefined — so narrowing the domain is what the mathematics says, and
0 provided not b = 1 is the honest answer. The contract's §3 is the distinction being applied.

For log(b, 0) with a base whose side of 1 cannot be read, there is no signed answer to give, so the
node is left as written.

boundcheck gains the points that would have caught this

The harness had 366 shapes passing at 23 points chosen for branch cuts and principal intervals
and none of them was x = 1, so log(x, 1) -> 0 looked sound at every point tried. That is a real gap
in the harness, not just in the rule: "boundary" has to include the points where an identity's own
arithmetic degenerates, not only where a branch is crossed.

Added x = 1 and x = 0, plus the six degenerate logarithm shapes. The two new points immediately
turned up two more wrong answers, filed as #892:

"abs(sgn(x))".Simplify()  =>  1        and at x = 0 the value is 0
"sgn(abs(x))".Simplify()  =>  1        and at x = 0 the value is 0

That is the third instance of one shape after #884 and #887 — an identity that holds off a thin set,
written as though it held everywhere. Not fixed here; #892 has the analysis, including that the
neighbouring sgn(z)*|z| -> z in the same file is sound at zero, so that file has the same
sound-half/unsound-half split as the other two.

Measured on this branch, .NET 10

C# tests 6280 passed, 0 failed, 14 skipped
F# tests 130 passed, 0 failed
casbench 117/119, 0 wrong, 0 error, 0 timeout
propcheck 1340 checks, 0 failures
rootcheck 596/596 clean
simpsweep 10463/10463 agree, 0 disagree
boundcheck unchanged at the five master already has; the six new logarithm shapes pass

Ten new regression cases: log(1, 1) is NaN through both Simplify and EvalNumerical, the
ordinary bases still collapse to 0, the symbolic base carries its condition and evaluates to NaN
at x = 1, log(b, 0) follows the sign of its base, and the symbolic case is left alone.

log_b(z) is ln(z)/ln(b), and three answers did not follow from that.

log(1, 1) was 0. It is 0/0, and every division by zero in this library is NaN --
0/0, 2/0 and -2/0 all are. The cause was the real shortcut in Number.Log: LogN
answers 0 for log_1(1) and +oo for log_1(2), where falling through to Ln(x)/Ln(base)
gives NaN for both. A base of 1 is now excluded from the shortcut, so the two paths
agree.

log(b, 1) was 0 for any base at all, including 1. It is 0/ln(b), which is 0 for every
base but 1, so the answer carries `not b = 1`. A condition is right here, unlike the
interval cases in #884: at b = 1 the expression genuinely is undefined rather than
merely something else.

log(b, 0) was -oo for any base. It is -oo/ln(b), so the sign of the answer is the sign
of ln(b): -oo above 1 and +oo between 0 and 1, which made log(1/2, 0) wrong in exact
form. It now answers where the base can be placed on one side of 1 and leaves the node
alone otherwise, there being no signed answer to give for a base it cannot place.

boundcheck gains the six degenerate logarithm shapes, and -- more usefully -- the two
points that make a rule's own arithmetic rather than its branch degenerate, x = 1 and
x = 0. All 366 existing shapes had passed at 23 points chosen for branch cuts and
principal intervals; the two new points immediately turned up abs(sgn(x)) and
sgn(abs(x)) both simplifying to 1 where they are 0, filed as #892.

Measured: 6280 C# tests pass, 130 F# tests pass, casbench 117/119 with 0 wrong,
propcheck 1340 checks with 0 failures, rootcheck 596/596, boundcheck unchanged at the
five master already had.
Two conflicts, both additive on each side, so both sides are kept.

BREAKING-CHANGES.md: #888's arccotan row and section went in at the same anchors as
the logarithm ones. The arccotan entries come first, continuing the
inverse-trigonometric entry above them.

SimplificationRegressionTest.cs: #888's arccotan cases and this branch's logarithm
cases were appended at the same point.
@Rafael-SOWNet
Rafael-SOWNet merged commit ca8e9f5 into master Aug 11, 2026
25 checks passed
Rafael-SOWNet added a commit that referenced this pull request Aug 11, 2026
#893's logarithm section went into BREAKING-CHANGES.md at the same anchor this branch
uses, so both are kept -- as with #888 before it. Three of the entries in this file now
sit next to each other because three changes to the same table landed in the same
afternoon, which is a good problem to have.
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.

1 participant