Skip to content

Read a factorial's logarithm outside the power form too (#765) - #766

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
feat/stirling-outside-the-power-form
Aug 6, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
feat/stirling-outside-the-power-form

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Closes #765. Follows #764, which is merged.

What was wrong

Stirling's expansion arrived inside SolveAsIndeterminatePower — the rule that turns f^g into e^(g * ln f) — so it found a factorial only under a vanishing exponent. ((x!)/x^x)^(1/x) was answered and ln(x!)/x was not, which is the harder question answered and the easier one left. ln(n!) ~ n*ln(n) is the standard statement of the result.

lim x->+oo ln(x!) / (x * ln(x))     was  unevaluated   is  1
lim x->+oo ln(x!) / x               was  unevaluated   is  +oo
lim x->+oo ln(x!) / x^2             was  unevaluated   is  0
lim x->+oo ln(x!) - x * ln(x)       was  unevaluated   is  -oo
lim x->+oo ln((2*x)!) / (x*ln(x))   was  unevaluated   is  2
lim x->+oo ln(x!) / ln(x)           was  NaN           is  +oo

The last was a wrong answer rather than a missing one — NaN claims the limit does not exist, and that quotient is asymptotic to x, reaching 9.6e11 at x = 1e12.

The guard is the work

Applying the expansion wherever a factorial's logarithm appears is not sound. What Stirling drops is 1/(12f), and an expression can be built around exactly that term:

x * (ln(x!) - (x*ln(x) - x + ln(2*pi*x)/2))   is  1/12,  and an unguarded rewrite answers 0

#764 could state its guard as power / f -> 0 because it knew the shape it was in: the answer there is e^(power * ln(base)), so power is the coefficient the dropped term gets multiplied by. Anywhere else that coefficient has to be found — by putting a variable where the logarithm is and differentiating with respect to it. The rewrite is refused unless coefficient / f vanishes.

The coefficient is put back in terms of the logarithm before it is judged, since an expression need not be linear in it: for ln(x!)^2 the derivative is 2*ln(x!), which grows like 2*x*ln(x) and is refused — correctly, since the difference of the squares is ln(x)/6 and diverges.

The guard is sufficient rather than necessary, and a test records the gap: x*ln(x!) - x^2*ln(x) is -oo, which the dropped 1/12 would not have changed, and it is refused anyway because nothing available here tells it apart from the case that is exactly 1/12.

Placement

In SolveByRewriting, the position reached only once it is settled that the expression as written has no answer. That is where it belongs: the rewrite asks for sub-limits of its own (the factorial's argument must diverge, and the coefficient must be judged), so it must not sit in the descent that visits every part of every expression.

What it costs

lim x->+oo ln((x+1)!) / (x*ln(x)) was unevaluated in about a second and now takes longer than 20 s to be unevaluated. The rewrite is right and the expression it hands over is right — the machinery cannot take that limit quickly, which is demonstrable without any of this:

lim x->+oo ((x+1)*ln(x+1) - (x+1) + ln(2*pi*(x+1))/2) / (x*ln(x))    times out on master

So the cost is the machinery's rather than this rewrite's. Recorded in BREAKING-CHANGES.md.

Measured

70 generated factorial expressions — x!, (2*x)!, (x+1)!, x!/x^x, x!/e^x, x^x/x!, (x+1)!/x! under ten wrappers — diffed against master:

count
unevaluated → a value 13
NaN → +oo 3
unevaluated → timeout 1

24 of the 70 already timed out before this change. Every value checked numerically with mpmath at 40 digits up to x = 1e12 — ln(x!)/(x*ln(x)) approaches 1 from below and slowly, reaching only 0.9638 there, so the corpus value is easy to get wrong by eye.

  • casbench 113/117, 0 wrong, 0 error, 0 timeout — unchanged
  • unit tests 5322 passed, 0 failed (5307 on master; 15 added)
  • F# wrapper tests 130 passed
  • propcheck 0 failures, rootcheck 596/596, simpsweep 0 disagreements of 10463

Stirling's expansion arrived with SolveAsIndeterminatePower, which is where f^g
becomes e^(g * ln f), so it found a factorial only under a vanishing exponent.
((x!)/x^x)^(1/x) was answered and ln(x!)/x was not -- the harder question answered
and the easier one left, when ln(n!) ~ n*ln(n) is the standard statement of the
result.

Applied now wherever a diverging factorial's logarithm appears, from the position
that is reached only once nothing else has a reading. That is where it belongs:
the rewrite asks for a sub-limit of its own, so it must not sit in the descent
that visits every part of every expression.

It is not a rewrite that may be applied unconditionally, and the guard is the
work. What Stirling drops is 1/(12f), and what that costs the answer is the rate
at which the answer moves with the logarithm -- so the coefficient is found by
putting a variable where the logarithm is and differentiating with respect to it,
and the rewrite is refused unless coefficient/f vanishes.
x * (ln(x!) - (x*ln(x) - x + ln(2*pi*x)/2)) is 1/12, an expression built out of the
dropped term itself, and a rewrite that did not ask would answer it 0. The
coefficient is put back in terms of the logarithm before it is judged, since an
expression need not be linear in it: for ln(x!)^2 the derivative is 2*ln(x!),
which grows and is refused -- correctly, since the difference of the squares is
ln(x)/6 and diverges.

The guard is sufficient rather than necessary, and a test records the gap:
x*ln(x!) - x^2*ln(x) is -oo, which the dropped 1/12 would not have changed, and it
is refused anyway because nothing available here tells it apart from the case that
is exactly 1/12.

lim x->+oo ln(x!)/ln(x) was NaN -- the claim that the limit does not exist, where
it is +oo and the quotient is asymptotic to x. That is a wrong answer rather than
a missing one, and it is the only one in this family.

Measured over 70 generated factorial expressions against master: 13 unevaluated
became values, 3 NaN became +oo, and one moved from unevaluated-in-a-second to a
timeout -- ln((x+1)!)/(x*ln(x)), where the rewrite is right and the expression it
hands over is right, and the machinery cannot take that limit quickly. Writing the
expanded form out by hand times out on master too, so the cost is the machinery's
rather than this rewrite's. 24 of the 70 already timed out before this change.

Every value checked numerically at up to x = 1e12. casbench 113/117 with 0
timeouts and unchanged, unit tests 5307 -> 5322 passing with 15 added, F# 130,
propcheck 0 failures, rootcheck 596/596, simpsweep 0 disagreements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit 184dd5a into master Aug 6, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the feat/stirling-outside-the-power-form branch August 6, 2026 14:38
Rafael-SOWNet added a commit that referenced this pull request Aug 6, 2026
The two readings of Stirling's expansion already here both need the factorial to
be under a logarithm: one supplies it, by turning f^g into e^(g * ln f), and the
other rewrites a logarithm that is written down. Neither reaches x! / x^x, which
has none, so that shape and its relatives had no limit.

The logarithm is supplied instead: for a positive expression lim H is
e^(lim ln H), and ln H is where the expansion applies. The guard is the power of
the factorial the expression depends on -- exactly the coefficient ln(f!) carries
in ln H -- so the dropped 1/(12f) has to vanish against it, as everywhere else.
(x!)^x depends on its factorial through the power x, x/(12x) does not vanish, and
it is refused.

**Not by substituting e^(Stirling(f)) for the factorial**, which is the obvious
move and was measured to be far worse. It puts an e to a large exponent into the
expression, the machinery evaluates that constant to a hundred-digit decimal, and
everything downstream drags it: lim x->+oo (x!/e^x)^(1/x) went from half a second
to over a minute, on an expression the library already answered. Going through the
logarithm keeps the only e in the final answer. That was found by timing the
calculus tests -- 46 s to 2 m 9 s -- and not by reading the code.

The power is read off the shape rather than by differentiating and simplifying.
The symbolic route needs a full Simplify to cancel the target back out, since
InnerSimplified leaves p * (1/x^x) / (p/x^x) standing, and it is asked on every
expression holding a factorial.

The rewrite that reads a logarithm is broadened from ln(f!) to any logarithm
holding a diverging factorial, and this is not tidiness. Without it, ln(x!/x^x)
stays one opaque node, the descent assembles -oo/+oo, reads NaN off it, and three
limits that had been slow non-answers become wrong answers. Measuring caught that
too; the sweep now reports no NaN at all.

Measured over 70 generated factorial expressions against master: 24 timeouts and
9 unevaluated became values, and nothing was lost -- 33 answered before, 66 now,
with timeouts 25 -> 1. The one that remains is ln((x+1)!)/(x*ln(x)), already
recorded when PR #766 landed. Values spot-checked numerically at up to x = 1e6.

Calculus tests 46 s -> 55 s for fifteen more of them, casbench 113/117 with 0
wrong and 0 timeout, unit tests 5322 -> 5337 passing with 15 added, F# 130,
propcheck 0 failures, rootcheck 596/596, simpsweep 0 disagreements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rafael-SOWNet added a commit that referenced this pull request Aug 7, 2026
The two readings of Stirling's expansion already here both need the factorial to
be under a logarithm: one supplies it, by turning f^g into e^(g * ln f), and the
other rewrites a logarithm that is written down. Neither reaches x! / x^x, which
has none, so that shape and its relatives had no limit.

The logarithm is supplied instead: for a positive expression lim H is
e^(lim ln H), and ln H is where the expansion applies. The guard is the power of
the factorial the expression depends on -- exactly the coefficient ln(f!) carries
in ln H -- so the dropped 1/(12f) has to vanish against it, as everywhere else.
(x!)^x depends on its factorial through the power x, x/(12x) does not vanish, and
it is refused.

**Not by substituting e^(Stirling(f)) for the factorial**, which is the obvious
move and was measured to be far worse. It puts an e to a large exponent into the
expression, the machinery evaluates that constant to a hundred-digit decimal, and
everything downstream drags it: lim x->+oo (x!/e^x)^(1/x) went from half a second
to over a minute, on an expression the library already answered. Going through the
logarithm keeps the only e in the final answer. That was found by timing the
calculus tests -- 46 s to 2 m 9 s -- and not by reading the code.

The power is read off the shape rather than by differentiating and simplifying.
The symbolic route needs a full Simplify to cancel the target back out, since
InnerSimplified leaves p * (1/x^x) / (p/x^x) standing, and it is asked on every
expression holding a factorial.

The rewrite that reads a logarithm is broadened from ln(f!) to any logarithm
holding a diverging factorial, and this is not tidiness. Without it, ln(x!/x^x)
stays one opaque node, the descent assembles -oo/+oo, reads NaN off it, and three
limits that had been slow non-answers become wrong answers. Measuring caught that
too; the sweep now reports no NaN at all.

Measured over 70 generated factorial expressions against master: 24 timeouts and
9 unevaluated became values, and nothing was lost -- 33 answered before, 66 now,
with timeouts 25 -> 1. The one that remains is ln((x+1)!)/(x*ln(x)), already
recorded when PR #766 landed. Values spot-checked numerically at up to x = 1e6.

Calculus tests 46 s -> 55 s for fifteen more of them, casbench 113/117 with 0
wrong and 0 timeout, unit tests 5322 -> 5337 passing with 15 added, F# 130,
propcheck 0 failures, rootcheck 596/596, simpsweep 0 disagreements.

Co-authored-by: Claude Opus 5 <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.

Stirling reaches only the power form, so lim x->+oo ln(x!)/x and x!/x^x are still unevaluated

1 participant