Skip to content

Opt-in for non-Real types to work with Infinities.jl - #95

Draft
PatrickHaecker wants to merge 14 commits into
JuliaMath:masterfrom
PatrickHaecker:archimedean
Draft

PatrickHaecker wants to merge 14 commits into
JuliaMath:masterfrom
PatrickHaecker:archimedean

Conversation

@PatrickHaecker

Copy link
Copy Markdown
Contributor

Infinity.jl hs both InfExtendedReal and
InfExtendedTime. In order to replace this package, Infinities.jl needs to support these
use cases, too.

I wanted to have one unified approach, because time is just one physical dimension
and I don't think there is a reason why it should be treated separately.

So I dug into the math and tried to find why intuitively we want to be able to support
infinity for some types, but not for others. It seems like an
Archimedean group
is the relevant criterion, i.e. every type where you can step from one element to the
next one in an ordered way and which models something which is unbounded.

I implemented a macro so that a type can opt-in into enabling basic functionality
with infinity. The suitable types from Dates now do that with a simple package
extension.

Builds on #94.

Patrick Häcker added 14 commits September 25, 2026 10:46
A direction is an angle modulo a full turn, so the field now counts turns in
units of 2^-64 and wraps where the circle does. Every `UInt64` names a
direction and every direction has exactly one count, which the old half-turn
field did not manage: half turns of 0.5 and 2.5 pointed the same way yet
compared unequal and hashed apart. Wrapping also makes the group operation a
machine add, and the resolution is uniform instead of thinning out towards a
full turn.

The count is what the constructor takes. An angle has to be rounded to reach
it, so that step is now named at the call site with the `halfturns` keyword,
and the old `ComplexInfinity(0.5)` is a `MethodError` rather than a silent
reinterpretation. Most code needs neither form, since multiplying by `∞`
takes the direction from the other operand: `im*∞` and `(1+im)*∞`.

The element type carried no information about the value, only about how the
direction had been spelled, so it is gone. It had been standing in for "this
infinity lies on the real axis", and that was never what it meant: a zero
angle written as a float pointed along the positive real axis just as
`ComplexInfinity()` does, yet only the latter could be ordered or divided.
The union types that used the parameter as a proxy now leave a `ComplexInfinity`
out entirely, so the complex plane carries no order and takes no integer
operation, exactly as `Base` treats a `Complex`.

The count has 64 bits and an angle in a `Float64` has 53, so the two are kept
apart wherever the difference shows. Equality and `hash` read the count, never
the angle. `angle` reports on `Base`'s branch of `(-π, π]`. And `show` gives
the readable `cispi(h)∞` only where that reads back, and the count itself
otherwise, so every printed form evaluates to the value it came from.
Dropping the element type removed the only way to ask a `ComplexInfinity`
whether it points along the real axis, and that question was worth keeping:
it just belongs to the value rather than to the type. `isreal` takes it over,
and `RealInfinity` converts a direction that lies on the axis and throws an
`InexactError` otherwise, which is what `Real` does with a `Complex`.

So an order or an integer operation is still reachable, by naming the
conversion, and an infinity off the axis throws instead of quietly behaving
as if the imaginary part were not there.
`Base` returns `NaN` where two reals have no result and `NaN + NaN*im` where
either operand is complex, and this package only did half of that. Anything
computed *from* a `NotANumber` already came back complex against a complex
operand, but every place that *produced* one returned the real value, so
`NaN * ComplexInfinity()` and `NotANumber() * ComplexInfinity()` disagreed.

`_undefined` picks the value from the two operands and now stands wherever an
undefined result is made: a `NaN` argument, a zero times an infinity, two
infinities divided, and two infinities added in different directions. Only
five of those sites can see a complex operand at all; for the rest the choice
is decided at compile time and costs nothing.
This takes over ideas from JuliaMath#20 created by @cscherrer. So thanks for that contribution!
As the PR is 5 years old, quite some things are now obsolete.

A nice side-effect was finding and fixing a missing `infpromote` for `Bool`.
This would benefit from someone looking at it who knows ForwardDiff
better than I do.

Fixes: JuliaMath#13
There is only a negative or a positive infinity in the real space.
However, there can still be an arbitrary number of Julia implementations
of both values. So keep `RealInfinity` as an abstract type and let it
have a proper interface.

Fixes JuliaMath#78
I am not fully convinced, that these are a good idea. This is addressing item 2
in JuliaMath#7.
If we merge the current change, JuliaMath#7, item 2 can be considered done.
Otherwise we should reject it there and also consider this item done
The complex plane carries no order, so `Base` does not defines `isless`, `<`, `≤`,
`min` and `max` for `Complex`. This package ordered a `ComplexInfinity`
against a real one anyway, which was also inconsistent: `isless(im*∞, +∞)`
was `true` while `isless(0, im*∞)` was a `MethodError`.

Restrict the ordering to the real infinities that lie on the real line.
Convert a `ComplexInfinity` with `RealInfinity` to compare it.

Addresses JuliaMath#7, item 5
`fld` and `cld` rounded away from zero and ignored the sign of the divisor,
giving `cld(0, ∞) == 1`, `fld(-1, ∞) == -1` and `fld(1, -∞) == 0`. `Base`
rounds such a quotient to zero in all three modes and signs the zero by the
divisor. Follow it, so that an infinity behaves like the float infinity.

An infinite or `NaN` numerator now gives `NotANumber`, as `div(Inf, Inf)` gives `NaN`.
Before Julia 1.13, `Base` itself answers `fld(-1.5, Inf) === NaN` and
`fld(-0.0, Inf) === -0.0`, so the comparison failed on the LTS runners. The
expected zeros are still checked on every version by the loop above it.
`ℵ₀` is an `Integer`, and `Base` divides two integers in floating point, so `2 / ℵ₀` is `0.0`
rather than `0`. This also keeps the sign of a negative numerator, as `-2 / ℵ₀` is `-0.0`.
[Infinity.jl](https://github.com/cjdoris/Infinity.jl) hs both `InfExtendedReal` and
`InfExtendedTime`. In order to replace this package, Infinities.jl needs to support these
use cases, too.

I wanted to have one unified approach, because time is just one physical dimension
and I don't think there is a reason why it should be treated separately.

So I dug into the math and tried to find why intuitively we want to be able to support
infinity for some types, but not for others. It seems like an
[Archimedean group](https://en.wikipedia.org/wiki/Archimedean_group)
is the relevant criterion.

I implemented a macro so that a type can opt-in into enabling basic functionality
with infinity. The suitable types from `Dates` now do that with a simple package
extension.
@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.25373% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.75%. Comparing base (08ccb94) to head (e36bf71).

Files with missing lines Patch % Lines
src/archimedean.jl 94.11% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##            master      #95      +/-   ##
===========================================
- Coverage   100.00%   99.75%   -0.25%     
===========================================
  Files            6        9       +3     
  Lines          336      400      +64     
===========================================
+ Hits           336      399      +63     
- Misses           0        1       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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