Skip to content

Reworking ComplexInfinity - #87

Open
PatrickHaecker wants to merge 5 commits into
JuliaMath:masterfrom
PatrickHaecker:complex_direction
Open

PatrickHaecker wants to merge 5 commits into
JuliaMath:masterfrom
PatrickHaecker:complex_direction

Conversation

@PatrickHaecker

@PatrickHaecker PatrickHaecker commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Ok, this is my take on fixing the problems around ComplexInfinity by storing the angle in an UInt64. With this, they should be accurate whenever the users wants them to be accurate. They should be bijective, so no more multiple internal representations which mean the same. No more different quantizations depending on where you are on the complex circle.
So overall they should now behave as similar to Base as possible. However, it's not all perfect. The angle Float64 interface which is important in Base is not a natural fit to this representation and the conversion is not really what I would call elegant.

Update:
Running through the issues and actually using the package showed up to be very useful. I fixed some inconsistencies and simplified the printing.

Details from the 🤖:

Makes a direction and a value the same thing. Two ComplexInfinitys pointing the same way were not equal and did not hash alike:

julia> ComplexInfinity(0.5) == ComplexInfinity(2.5)   # same direction
false

The representation

The field is a UInt64 counting turns in units of 2^-64, wrapping where the circle does. Every count names exactly one direction, the group operation is a machine add, and the resolution is uniform around the circle.

Most code needs no constructor, since multiplying by ∞ takes the direction from the other operand:

julia> im*∞
0 + ∞*im

julia> ComplexInfinity(0x4000000000000000) ≡ ComplexInfinity(halfturns = 1//2) ≡ im*∞
true

halfturns and the x*∞ forms go through angle, so they round. They are exact on the axes and diagonals, but exp(im*π/8)*∞ lands 256 counts past a sixteenth turn. Pass the UInt64 where another direction has to be exact, and read it back with reinterpret(UInt64, x).

Breaking

master this PR
ComplexInfinity(0.5) exp(0.5*im*π)∞ MethodError, use halfturns = 0.5
the type ComplexInfinity{Float64} ComplexInfinity, no parameter
5 < ComplexInfinity() true MethodError
div(ComplexInfinity(), 5) exp(false*im*π)∞ MethodError
angle(ComplexInfinity(halfturns = 1.5)) 4.71238898038469 -1.5707963267948966
angle(∞), angle(ℵ₀) 0 0.0
im*∞ exp(0.5*im*π)∞ 0 + ∞*im
∞ + im*∞ NotANumber() ∞ + ∞*im
NaN * ComplexInfinity() NotANumber() NotANumber() + NotANumber()*im

angle now reports on Base's branch (-π, π] and always returns a float. Ordering and the integer operations are gone, because the complex plane has neither, exactly as for Complex.

The type parameter is gone

It described how the direction had been spelled, never the value. Whether an infinity lies on the real axis is now asked of the value:

julia> isreal(ComplexInfinity()), isreal((1+im)*∞)
(true, false)

julia> RealInfinity((1+im)*∞)
ERROR: InexactError

Consistent with Base's Complex

On the axes and diagonals each part is infinite or exactly zero, so addition works part by part as it does for Complex. That makes complex(x, y) == x + im*y hold with infinite parts too:

julia> ∞ + im*∞ ≡ complex(∞, ∞) ≡ complex(0.0, Inf) + ∞
true

Any other two different directions give an undefined result. These eight directions print as their parts, the way Base prints 0.0 + Inf*im. Other directions print as cispi(h)∞ where that reads back exactly, and as the count otherwise, so every printed form evaluates to the value it came from.

Worth a look

Equality and hash read the count, never the angle. The count has 64 bits and a Float64 angle has 53, so directions a float cannot tell apart must still compare unequal. Ordinary arithmetic reaches them: ComplexInfinity(halfturns = 0.5) * ComplexInfinity(halfturns = 2.0^-63).

@codecov

codecov Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (08ccb94) to head (1d36d39).

Additional details and impacted files
@@            Coverage Diff            @@
##            master       #87   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            6         6           
  Lines          336       352   +16     
=========================================
+ Hits           336       352   +16     

☔ 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.

@PatrickHaecker PatrickHaecker mentioned this pull request Sep 6, 2026
@PatrickHaecker
PatrickHaecker force-pushed the complex_direction branch 2 times, most recently from a5f3669 to 421714b Compare September 7, 2026 03:48
@PatrickHaecker
PatrickHaecker force-pushed the complex_direction branch 2 times, most recently from 3e63d0d to 4991b0d Compare September 23, 2026 09:55
This was referenced Sep 23, 2026
Patrick Häcker added 3 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.
@PatrickHaecker
PatrickHaecker marked this pull request as ready for review September 25, 2026 08:57
@PatrickHaecker

Copy link
Copy Markdown
Contributor Author

Thanks, @dlfivefifty. So I rebased #87 on master, so it is ready for review.

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