Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow ceil/trunc/floor on complex arguments #51141

Closed
wants to merge 2 commits into from

Conversation

Seelengrab
Copy link
Contributor

See #42060 for why this is not easy to define, and hence is (for now) better left to error, instead of working through a fallback.

This also rewords the docstring of the existing round function with two rounding modes, to make it clear that the imaginary rounding mode defaults to the same as the one that's set for the real part.

See JuliaLang#42060 for why this is not easy to define, and hence is (for now)
better left to error, instead of working through a fallback.
@KristofferC
Copy link
Sponsor Member

This is breaking, or?

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Sep 1, 2023

No. All existing releases already throw a MethodError here, the refactor in #50812 just happened to make this work, without taking the implications on #42060 into account. Existing working code should not be affected at all.

This PR just improves on the error message and "keeps it broken" (which arguably should have been done in the API refactor..).

@KristofferC KristofferC added this to the 1.11 milestone Sep 1, 2023
@LilithHafner
Copy link
Member

If we were doing it all over, I would prefer to have neither round(::Complex, RoundDown) nor floor(::Complex). Given that we already have round(::Complex) and round(::Complex, RoundDown) in 1.0 through 1.10-beta, I think floor(::Complex) is acceptable, with appropriate documentation. That said, I'm not necessarily opposed to special casing a prohibition for it. (My opinions on ceil and trunc are analogous)

julia> round(1.1 + 1.2im, RoundUp)
2.0 + 2.0im

julia> round(1.1 + 1.2im, RoundDown)
1.0 + 1.0im

julia> round(1.1 + 1.2im, RoundToZero)
1.0 + 1.0im

julia> round(1.1 + 1.2im, RoundNearest)
1.0 + 1.0im

julia> round(1.1 + 1.2im)
1.0 + 1.0im

julia> versioninfo()
Julia Version 1.9.3
Commit bed2cd540a1 (2023-08-24 14:43 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: macOS (arm64-apple-darwin22.4.0)
  CPU: 8 × Apple M2
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, apple-m1)
  Threads: 1 on 4 virtual cores
Environment:
  JULIA_EDITOR = code

I don't see much distinction between floor(x) and round(x, RoundDown).

@Seelengrab
Copy link
Contributor Author

I don't see much distinction between floor(x) and round(x, RoundDown).

I agree that single or zero-arg round (in terms of rounding modes) are bad for Complex, but removing those is definitely breaking, because the behavior of round with just one rounding mode is already documented. In regards to floor though, the distinction is in intentionality - explicitly passing RoundDown is saying "yes that is exactly the behavior I want".

The issue with floor by itself is that this subtlety is entirely lost - and until we decide on the kind of semantic & properties we want to have for that by default, it's better to keep these undefined and not just "make them work" through a default fallback (especially without proper tests & documentation). This is especially important because the fallback has semantic implications for Complex (see the list of properties e.g. APL sees as desirable here - in particular, just flooring both real and imaginary components violates the core property of any floor function). The argument is analogous for ceil and trunc.

Considering these difficulties, it's IMO better to just keep this as an error for now.

@Seelengrab
Copy link
Contributor Author

CI is green - what a wonderful thing 🎉

@PallHaraldsson
Copy link
Contributor

Shouldn't this just be merged?

Also to get PkgEval. But after, or even before backport to 1.10? It would be bad is something defined in 1.10.0 went away in later version. If this turns out to be bad to (not) define, then it can be undone later.

It seems we don't want to define this, since problematic mathematically:

#42060 (comment)

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Nov 10, 2023

floor(1im) doesn't work on 1.10 (or any release candidate), so it should be fine to only have this in 1.11. That said, I do agree that this can just be merged, since CI is/was green, there are no conflicts and this PR has not received comments to the contrary.

@LilithHafner
Copy link
Member

this PR has not received comments to the contrary.

Given that we already have round(::Complex) and round(::Complex, RoundDown) in 1.0 through 1.10-beta, I think floor(::Complex) is acceptable, with appropriate documentation. ... I don't see much distinction between floor(x) and round(x, RoundDown).

We've already picked the elementwise semantic for rounding complex numbers. A reasonable choice IMO, and it's certainly too late to revise that before 2.0.

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Nov 10, 2023

No, you unilaterally happened to make trunc etc. work during your refactor. There has not been a resolution to what should be done about the issues discussed in #42060, and hence this should for now stay broken. Please refer to my earlier comment about this PR keeping the single-arg round(::Complex, ::RoundingMode) intact, as that indeed cannot be removed.

I'll label this triage so it can be discussed & decided on next time.

@Seelengrab Seelengrab added the triage This should be discussed on a triage call label Nov 10, 2023
@LilithHafner
Copy link
Member

We're committed to round(::Complex, RoundDown) performing elementwise floor.
Question for triage, should we also have floor(::Complex) perform elementwise floor?

(analogous for ceil & trunc with the appropriate rounding modes)

@PallHaraldsson
Copy link
Contributor

PallHaraldsson commented Dec 6, 2023

We're committed to round(::Complex, RoundDown) performing elementwise floor.

I'm not sure this is wise. You could say people asked for this, and document the definition used. But you might not get the nearest integer number, at least with Float16. Its maxintfloat(Float16) is only 2048, and very small for Float8 (in a package). For minifloat it's only 16, I think Float8 might be the same, there might be more than one definition.

It seems odd to special-case round/ceil of Complex to return Float64 (or Float32 ok?). Or just leave out Float16. It might seem odd to some that those do not work on some or any Complex type, but it's justified, and could be done by throwing an error?

@Seelengrab
Copy link
Contributor Author

It seems odd to special-case round/ceil of Complex to return Float64

That's not what's happening; the current definition for round(ComplexF64, RoundDown) does return a ComplexF64, with both components rounded own. My argument is that, per the discussion #42060, we shouldn't take this previous mistake as guidance for making more mistakes.

@PallHaraldsson
Copy link
Contributor

It seems odd to special-case round/ceil of Complex to return Float64
That's not what's happening

I know it's not happening, I mean complex based on Float16 returning same type seems like a problem, and special casing it to return a different larger type (while NOT type-unstable) would be surprising workaround.

@LilithHafner
Copy link
Member

Triage says that we should keep round, trunc, RoundNearest, and RoundToZero for Complex.

Three possibilities for floor and ceil:

  1. Keep round(::Complex, RoundUp) & ceil(::Complex)
  2. Make both round(::Complex, RoundUp) & ceil(::Complex) deprecated
  3. Make both round(::Complex, RoundUp) & ceil(::Complex) error

We should see what PkgEval has to say about options 2 & 3.

@LilithHafner LilithHafner removed the triage This should be discussed on a triage call label Dec 7, 2023
@Seelengrab
Copy link
Contributor Author

I feel like what this PR does has been misrepresented in triage. I couldn't join because it was at 2:30 AM my time. Has there been any discussion whatsoever about the implications of keeping these methods has on #42060?

This is not a matter of whether PkgEval agrees or not, this is about what floor/trunc mean mathematically - and with the current definitions on master, they break that meaning.

@StefanKarpinski
Copy link
Sponsor Member

To be more explicit about it, the rounding modes that were deemed to make sense for complex numbers are are ones that don't involve any nothing of "up" or "down", i.e.:

  • RoundNearest
  • RoundNearestTiesAway
  • RoundToZero
  • RoundFromZero

The other ones depend on some notion of "up" and "down" and therefore should not work for complex numbers. There were basically two sensible end situations that triage felt this could end up in:

  1. Be restrictive and disallow rounding that implicitly relies on a concept of "up" and "down" since those aren't meaningful for complex numbers. This means to deprecate any existing methods that do this. My understanding is that's only some methods of round with explicit rounding modes; leave the rest as errors.
  2. Be permissive and just allow all rounding modes on complex values operating component-wise. Since all modes that do make sense happen to work this way anyway, why not just allow all of them?

To avoid breaking things, we would deprecate any "disallowed" methods that already exist rather than deleting pre-existing methods. Any allowed methods would be implemented. In the restrictive option, top-level rounding functions would be allowed iff the corresponding method round(z, r) with an explicit rounding mode is, e.g.:

  • floor(x) and round(x, RoundDown) mean the same thing and are both disallowed for complex numbers
  • trunc(x) and round(x, RoundToZero) mean the same thing and are both allowed for complex numbers.

@Seelengrab, if I'm understanding you correctly, you're advocating for a third option, where floor(z::Complex) is disallowed but round(z::Complex, RoundDown) is allowed, decoupling the meaning of floor(_) slightly from the meaning of round(_, RoundDown). The idea being that floor(z) is iffy because someone may not realize that it doesn't make sense for complex values whereas round(z, RoundDown) is better because the rounding mode is explicit? Is that right?

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Dec 9, 2023

@Seelengrab, if I'm understanding you correctly, you're advocating for a third option, where floor(z::Complex) is disallowed but round(z::Complex, RoundDown) is allowed, decoupling the meaning of floor(_) slightly from the meaning of round(_, RoundDown). The idea being that floor(z) is iffy because someone may not realize that it doesn't make sense for complex values whereas round(z, RoundDown) is better because the rounding mode is explicit? Is that right?

Not quite. In an ideal world, we wouldn't have round(z, RoundDown) either because it's not clear what "rounding down" a complex value means in the first place; the version of round passing two rounding modes is better, because there it's clear what should be done with each component of the complex number. In view of the fact that we currently already have the single rounding mode version, it can't be removed (and this PR doesn't try to since that would be breaking).

The idea that floor(x) == round(x, RoundDown) for arbitrary x is fundamentally flawed; that interpretation only makes sense on a "number line"-like object like a real number - it breaks down as soon as you have a more complicated object like a complex number, because there is no directionality in "rounding down", as you mention. Because of that lack of correspondence, we ought to ask what floor(::Complex) means by itself - and there it comes up that (at least with the current definition on master that just happened to fall out of reworking the round API) we're not consistent with what it means to floor a number. The argument is similar for ceil and trunc. I really do encourage you to check out the literature I linked in #42060, which extends floor/ceil for APL to the complex numbers. Crucially, it's NOT just rounding per-component, as master currently does! This is also why I'm fine with having round(::Complex, ::RoundingMode, ::RoundingMode), because there you make it VERY explicit that you're trying to round each component individually. That doesn't mean we can default floor(z::Complex) = round(z, RoundDown, RoundDown) of course, as has been laid out in #42060.

As such, my objection to the current state on master (and reason for this PR) is that if we choose the current (bad) floor behavior, we're "stuck" and can't replace it with a (perhaps more correct/APL-like version) in the future. This is the main point I want to get across - we need to make a decision on what it means to floor(::Complex) (which may require taking a cut, similar to how you have to make a choice of returning a positive number when taking a square root). And since that decision has not been reached in #42060, we shouldn't define floor(z::Complex) = round(z, RoundDown) just because it happens to fall out of an API rework.

To be more explicit about it, the rounding modes that were deemed to make sense for complex numbers are are ones that don't involve any nothing of "up" or "down", i.e.:

  • RoundNearest
  • RoundNearestTiesAway
  • RoundToZero
  • RoundFromZero

The other ones depend on some notion of "up" and "down" and therefore should not work for complex numbers.

This is wrong - all of these depend on a notion of "up" and "down". For example, for RoundNearest, what is the "nearest" to 0.5 + 0.5im? Objectively, there are four such points; 0.0+0.0im, 1.0+0.0im, 0.0+1.0im and 1.0+1.0iom. All of these are a closest gaussian integer to 0.5+0.5im. "evenness" doesn't exist here anymore, so that can't serve as a tiebreaker and you must choose some other direction as a tiebreaker. There are a number of tiebreakers you could choose, but all of them choose a scheme that is incompatible with "nearest even" because "nearest even" isn't breaking ties when there are two choices to be made (iseven(::Complex) "just" checks the real part, because iseven doesn't otherwise make sense for complex numbers!).

Similarly, RoundNearestTiesAway implies rounding "away from zero" when it comes to ties; for 0.5+0.5im that works out to rounding to 1.0+1.0im, but deviate even slightly from that perfect 45° angle and now you have a different problem: which lattice point of the gaussian integer lattice do you chose? The one closest to the line when drawing a line perpendicular to the axis implied by the complex number? That might be very far out. The one closest to the point where that axis intersects with the unit square? The point where the magnitude of the complex number is an integer number (effectively rotating the entire complex plane to align with the direction given by the complex number and interpreting it as a distance on a Real number line)?

RoundToZero and RoundFromZero inherit the same problems - is the complex number a vector-like, giving a direction (to round in)? Or, if you want to take an abstract algebra POV, is it a rotation, where it doesn't really make sense to "round" the rotation? And if you do want to round it, what to? Perhaps the next full integer number of rotations? Either way, you need to pick an interpretation/direction to be able to say what "rounding from zero" means. For Real numbers that is easy, since there is only one possible direction to pick! For complex numbers, you need complex rounding modes; you can't easily repurpose those from real numbers.

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Dec 9, 2023

I also want to repeat that this PR doesn't break any existing/released code. There is no current release where floor(1+1im) works. This PR only ensures that this behavior stays until we decide what we want our floor(::Complex) (ceil/trunc..) to actually be. Whether that is the implementation I gave in #42060:

function aplFloor(z::Complex{T}) where T
   r = real(z)
   i = imag(z)
   b = floor(r) + floor(i)im
   # not sure whether this should be `rem` or `mod`..
   x = mod(r, one(T)) 
   y = mod(i, one(T))
   if one(T) > x+y
      return b
   else
      return b + (T(x ≥ y) + T(x < y)im)
   end
end

is a different matter.

@oscardssmith
Copy link
Member

oscardssmith commented Dec 11, 2023

The idea that floor(x) == round(x, RoundDown) for arbitrary x is fundamentally flawed; that interpretation only makes sense on a "number line"-like object like a real number

Triage's point is that round(x, RoundDown) is also fundamentally flawed for arbitrary x, and similarly requires a "number line"-like object.

what is the "nearest" to 0.5 + 0.5im

0.0 + 0.0im. RoundNearest (which defaults to ties to even) fairly naturally rounds to the nearest Gaussian integer, using even real and imaginary components in the case of ties. (admittedly towards even is somewhat ambiguous here, but Z[i]/2 and Z[i]/2i are reasonable constructions here).

is the complex number a vector-like

yes. The way to interpret RoundToZero and RoundFromZero is to pick from the 4 surrounding lattice points, the one with the least/greatest norm. RoundNearestTiesAway works similarly, picking the nearest lattice point and in the case of ties, picking the one with the greatest norm.

To be clear, Triage agrees that floor, and ceil are not well defined for Complex numbers. The disagreement here is why. The opinion from triage was that the ill-definedness comes directly from the ill-defindeness of round(::Complex, Union{RoundDown,RoundUp}). As such, rather than removing ceil/floor, we thought the better answer was to try to delete round(Complex, Union{RoundDown,RoundUp}), and if PkgEval discovers that this would be breaking, deprecate them (which will cause ceil and floor of Complex to be deprecated as well.

Also, if we can't reach consensus on this before next triage, we can talk about it then since next triage is at a better time for you.

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Dec 11, 2023

The disagreement here is why. The opinion from triage was that the ill-definedness comes directly from the ill-defindeness of round(::Complex, Union{RoundDown,RoundUp}).

As such, rather than removing ceil/floor, we thought the better answer was to try to delete round(Complex, Union{RoundDown,RoundUp}), and if PkgEval discovers that this would be breaking, deprecate them (which will cause ceil and floor of Complex to be deprecated as well.

I agree with that! I'm fully on board with removing one-arg round. That's not what this PR is supposed to do though.

All this PR does is explicitly remove the as-of-yet unreleased equivalence of floor(x) == round(x, RoundDown) on x isa Complex, which is not in any released julia version. Nothing relies on floor(::Complex) existing (because it doesn't yet exist in a release), so there is no need whatsoever to make it work in any potential release, no matter whether we decide to deprecate round(::Complex, RoundDown) or not.

To be clear, Triage agrees that floor, and ceil are not well defined for Complex numbers.

Then why not merge this PR, which literally only disallows these forwarding methods?

I don't understand why this is controversial.

Also, I feel like noone on triage has read the literature I've linked multiple times now, which precisely defines what it means to be a floor, and then extends that concept to complex numbers. This is not ill-defined, it's just incompatible with "floor each component and call it a day" (which is what I'm objecting to and what master currently does).

0.0 + 0.0im. RoundNearest (which defaults to ties to even) fairly naturally rounds to the nearest Gaussian integer, using even real and imaginary components in the case of ties. (admittedly towards even is somewhat ambiguous here, but Z[i]/2 and Z[i]/2i are reasonable constructions here).

I disagree - both 0.0 + 0.0im and 1.0 + 1.0im are considered "even" for gaussian integers. The usual extension of evenness is through multiples of 1 + im, since that leads to two equally sized equivalence classes when used as a modulus. Hence, iseven cannot serve as a tiebreaker, because there are two choices to be made. Not to mention that such a scheme breaks the requirements laid out for floor in Complex Floor, Eugene McDonnell, IBM Scientific Center
Philadelphia, PA 19063, USA
.

Is there some mathematical disagreement triage has with that literature that I'm missing?

Triage's point is that round(x, RoundDown) is also fundamentally flawed for arbitrary x, and similarly requires a "number line"-like object.

That sounds even more of an argument that the API redesign, while nice conceptually, does not work out well in practice.

@oscardssmith
Copy link
Member

oscardssmith commented Dec 11, 2023

The apl definition is interesting, although I'm not sure how important some of their criteria are (specifically the requirement of distance<1 seems odd since it would imply that distance<.5 needs to be preserved for round which is impossible. our implementation also matches Mathematica https://mathworld.wolfram.com/FloorFunction.html for what it's worth.

The key point from triage is that all the problems of floor come from problems in RoundDown, and as such, the preferred solution to the problem is to fix RoundDown. The APL paper is not especially compelling to me, but even if it were, IMO, it would be madness to implement floor the APL way, while keeping RoundDown with an incompatible definition. Fundamentally, floor(x) == round(x, RoundDown) is correct for Complex x, it's just that both should probably error (or follow APL or matlab, but the key point is that they should do the same thing).

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Dec 11, 2023

The apl definition is interesting, although I'm not sure how important some of their criteria are (specifically the requirement of distance<1 seems odd since it would imply that distance<.5 needs to be preserved for round which is impossible.

That again is only possible in the "number line"-like interpretation of a number. That requirement for round can't hold if you're dealing with an object higher dimension, which is precisely why this leans only on the magnitude of the floored number being less than the magnitude of the original number, thereby transforming a non-orderable object (like a complex number) to an orderable object (a real number). There is no implication about round because the two are fundamentally different.

our implementation also matches Mathematica https://mathworld.wolfram.com/FloorFunction.html for what it's worth.

I don't think that is worth a whole lot, see https://www.johndcook.com/blog/2021/08/03/complex-floor/ which talks about this (which I ALSO linked in #42060 here...). This is not clear cut, and just pointing to Mathematica is not a good "source of truth". Note:

[2] I suspect Mathematica’s definition is not common. I don’t know of any other source that even defines the floor of a complex number, much less defines it as Mathematica does.

So it seems to me that the Mathematica definition is not at all common, whereas the one from APL is shared J and others.

it's just that both should probably error (or follow APL or matlab, but the key point is that they should do the same thing).

Sure, but again, that doesn't mean we should make floor(::Complex) work wrongly NOW. That's what this PR is about. If you want me to instead deprecate the single-arg round method please just say so instead of going on long semi-related tangents about different rounding modes, while simultaneously closing the PR.

Also, it feels VERY wrong to introduce a new floor(::Complex) method and then have it immediately be deprecated already. There is no reason to do that, so let's just keep the call broken until we DO decide on an implementation for floor, and possibly deprecate round(::Complex, RoundDown) in the meantime. Removing the option of having a correct floor just because that means having a potential conflict with what round(::Complex, RoundDown) returns feels very backwards to me.

@oscardssmith
Copy link
Member

That again is only possible in the "number line"-like interpretation of a number.

no, this comes directly from the APL paper "3. Fractionality: The magnitude of the difference of a number and its floor shall be less than one". This is the key requirement that they use to define the floor function as they do (which relies only on Vector-Space properties, not number line properties). My point is that the analogous reasoning would say that round would require a magnitude of <.5.

Also, it feels VERY wrong to introduce a new floor(::Complex) method and then have it immediately be deprecated already.

The reason to do this is that it is a method that logically exists (by the correspondence between floor and RoundDown, which I still haven't seen an objection to), but which should not be used for the same reason RoundDown shouldn't. It's a little ugly, but practically the impact will be that pretty much no one uses it and we can remove it in 2.0. (or hopefully if #52490 can be merged without breakage, then floor and ceil will not be introduced for Complex numbers at all).

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Dec 11, 2023

no, this comes directly from the APL paper "3. Fractionality: The magnitude of the difference of a number and its floor shall be less than one".

Which implies nothing whatsoever about round. That is a connection YOU make because you want floor(::Complex) == round(x, RoundDown, RoundDown), which just isn't true. It doesn't preserve useful identities of floor, as described in the article I linked above:

https://www.johndcook.com/blog/2021/08/03/complex-floor/

This is the key requirement that they use to define the floor function as they do (which relies only on Vector-Space properties, not number line properties).

"Taking the magnitude" is a projection from the vector space to a number line. All vector space properties are lost in that projection; you are only left with an ordinary, orderable, Real number. The APL paper argues that it is useful to keep this fractionality property for floor around.

My point is that the analogous reasoning would say that round would require a magnitude of <.5.

Good thing that the APL paper is not arguing for that then, but for floor. It's explicitly not talking about round.

(by the correspondence between floor and RoundDown, which I still haven't seen an objection to)

There is no correspondence between floor and round(x, RoundDown). I have objected to this since the original opening of the PR, and no argument you have made so far is convincing of this being correct. In fact, you yourself claimed that this connection is bunk:

Triage's point is that round(x, RoundDown) is also fundamentally flawed for arbitrary x, and similarly requires a "number line"-like object.

@tecosaur
Copy link
Contributor

tecosaur commented Dec 29, 2023

Having first heard of this PR when it was mentioned in Triage, the complexity of complex rounding has been rattling around my mind a bit since.

This has prompted the thought that perhaps we can gain some insight/perspective by considering an even more complex class of numbers: quaternions.

A quick search turned up an interesting document by Bob Smith (who seems to have been quite involved with APL): Hypercomplex GCD in APL. This document links to an ACM paper published after McDonnell's complex floor titled Complex floor revisited.

I found the conclusion of that paper quite insightful, perhaps it might help with the discussion here?

No complex floor function is perfect. HF fails to be compatible with the ‘present floor function. FF fails to possess ‘fractionality’ and cannot be extended to the quaternions. MF fails to have symmetry or separation.

My feeling is that attempting to choose a floor function by satisfying identities puts the cart before the horse. The point of insisting on identities is to ensure a useful function. Nevertheless, the acid test of a primitive must be its utility - its ability to combine with the other primitives to form useful functions.

On this basis, I prefer FF. It can be used to find the integer parts of a complex number, or the nearest integer to a complex number (by using it to express HF). A GCD function written using FF (by substituting the FR expression for HR into GCD) converges faster than any GCD function written with MR. Similarly continued fractions found with Hurwtiz’s algorithm (easily expressed with FF) require fewer terms for the same degree of accuracy than those found with Shallit’'s algorithm. The classic test for being integral, (⌊X)=⌈X, works when FF and FC are used, but not when MF and MC are (of course X=⌊X fails equally well under both definitions when X is near 0).

The difficulty in extending these functions can probably be traced to the definition of floor X: the largest of the integers less than or equal to X. The complex domain is not ordered, so any attempt to extend such a definition is bound to be imperfect. Hurwitz’s function which computes the nearest integer does not suffer from this problem however. It must resolve equal distances in some arbitrary fashion, but it has this problem even on the reals. There is a direct analogy with the arbitrary choice for the phase angle of 16.

My own recommendation is that FF be adopted for complex floor and that if and when APL is extended to the quaternions Hurwitz’s functions be adopted as primitives also. This latter recommendation applies even if MF is eventually chosen as the definition of complex floor, since there would otherwise be no convenient expression for it.

@StefanKarpinski
Copy link
Sponsor Member

To do a little translation of the abbreviations from that paper:

  • MF = "McDonnell's floor" = APL's diagonal "floor" function
  • HF = "Hurwitz's floor" = round to zero
  • FF = component-wise floor

So this recommendation is basically he prefers component-wise floor (FF) and things APL should adopt it for complex numbers, contrary to what it does. He also believes that complex/quaternion truncation (HF) is a useful primitive that should be exposed.

Back to McDonnell's APL paper: the "fractionality" criterion strikes me as very poorly motivated. Of course, the name sound good—yes, of course, the difference between a value and its floor should be fractional! And on the real number line that means it should have magnitude less than one. But blindly applying the "magnitude less than one" to the complex plane doesn't really make any sense. It means that the numbers that can be considered fractional in ℂ are the ones inside the unit circle. But contrary to that, I would consider any complex number with both real and imaginary parts less than one to be fractional, which is a very different criterion. If I had started with criteria that led me to such a frankly odd floor function, I would go back and reconsider my criteria.

Citing J as an independent data point supporting APL's floor behavior is a bit disingenuous—J and K after it are just APL clones with syntax that you can type on a qwerty keyboard. This weird floor behavior seems to have originated in APL with one person who thought it was a good idea and been regretted in hind sight.

@Seelengrab
Copy link
Contributor Author

Seelengrab commented Jan 10, 2024

Citing J as an independent data point supporting APL's floor behavior is a bit disingenuous—J and K after it are just APL clones with syntax that you can type on a qwerty keyboard. This weird floor behavior seems to have originated in APL with one person who thought it was a good idea and been regretted in hind sight.

Since it seems to (again) have been lost in the discussion above, my reason for opening this PR was to make floor on Complex undefined because it's not clear that there even is a consistent definition. I have certainly never claimed that we should implement the APL version, and only brought it up in the original issue as an example for why this is not trivial to just define. That's a stance I have consistently held in the original issue, this PR as well as on triage.

As it turns out, Hypercomplex GCD in APL also has this to say:

For Octonions, neither definition of Floor has Fractionality as the maximum diagonal distances in both cases are greater than one, and as such there is no suitable definition of Floor on Octonions.

(the definitions referred to here are MF & HF)

which tracks completely with the stance I've held in these threads so far. This even extends to higher dimensions:

Why no fractionality?

The short reason why is that as the dimension increases (in this case from 1 to 2 to 4 to 8) the diagonals (which are the longest distances from the origin to any corner in Figures 1 and 2 when extended to Quaternions) get longer and longer – when they meet or exceed one, we lose Fractionality. Concerning higher dimensions, “As J. H. Conway once explained to Martin Gardner, There is a lot of room up there.”

This is very reminiscent of the sphere packing paradoxon in higher dimensions, where with high enough dimension, packing spheres into a unit cube can result in spheres with a larger volume that than unit cube.


I don't know why everyone keeps talking as if I just want the APL version. I don't particularly want the APL version (though if we were going for it or any other complex floor, I'd still want to check what exactly we gain and what properties hold), and I've since read enough about this to convince me that we shouldn't have a generic floor at all. I don't know why that means this PR can't be fixed up/integrated more properly (which is the impression I've gotten since day one, enforced through the forced closure instead of proper feedback), but since @oscardssmith said on triage that he'd take up this work, I'll gladly step away and let this be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
complex Complex numbers maths Mathematical functions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants