FixedPointNumbers.jl icon indicating copy to clipboard operation
FixedPointNumbers.jl copied to clipboard

WIP/RFC/Scary: allow signed Normed numbers

Open timholy opened this issue 6 years ago • 43 comments

This is a partial implementation of a speculative change, allowing signed variants of Normed. For example, S7f8 would represent the range -128.502 to 128.498; like N0f8 it is still scaled by 255, but also has 1 sign bit and 7 bits to store absolute values bigger than 1.

The motivation here is to support differences, related to #126, #41, and coming up with a good accumulator type in CheckedArithmetic.

Speculatively, we could use S7f8 for subtraction (and maybe addition) with N0f8. Pros: computing differences among pixels in images is a common operation, and it would be nice for this to be less of a gotcha than it is currently. Cons: this is against convention in the Julia ecosystem, and in opposition to #27. Thoughts? I'd especially like to hear from @vchuravy in addition to other recent contributors.

timholy avatar Nov 27 '19 13:11 timholy

Wow, this is a great challenge! I agree with the aim and concept, but this is a dramatic change, you know. So, I cannot immediately determine whether the benefits of generalization exceed those of specialization.

From a practical point of view, since N0f8 cannot be converted to 8-bit signed Normed, I don't think it has much merit. For addition / subtraction, N8f8 can be used instead of S7f8, and Float32 is suitable for many other operations. Of course, I'll do all I can for this challenge.

Edit: Perhaps adding SNormed{<:Signed} instead of Normed{<:Signed} may alleviate my anxiety.

kimikage avatar Nov 27 '19 14:11 kimikage

From a practical point of view, since N0f8 cannot be converted to 8-bit signed Normed, I don't think it has much merit... For addition / subtraction, N8f8 can be used instead of S7f8

Not entirely. I don't entirely subscribe to what follows, but for the purposes of argument let me make the extreme case. Consider

julia> 0x01 - 0x02
0xff

If magnitude matters, it's bad when smaller - small = big. This is much more serious a problem than 0xff + 0x01 == 0x00, because it's much more important to be able to accurately measure numbers close to 0 than it is to typemax(T). (We often use small integers on a 64-bit machine, but rarely do we use integers close to ±2^63.) A very common operation is to look at the difference between images, e.g., to detect changes in a scene, and the default arithmetic rules here make this sufficiently fraught that newbies are almost guaranteed to write buggy code. Somewhat along the lines of https://stackoverflow.com/questions/10168079/why-is-size-t-unsigned, one could declare that one might ban unsigned types from arithmetic altogether.

With this line of thinking, there are two options: (1) deliberately lose one bit of precision, and for an 8-bit image demote it to 7-bit and then use the extra bit as a sign bit (so that 8-bit images get loaded as S0f7); (2) for any arithmetic operation involving unsigned types, immediately go to the next wider signed type. In option (2), when doing any mathematics at all, we promote Nmfn (where m+n is 8, 16, or 32) to S(2m+n-1)fn so that we get a sign bit with no loss of precision; as a bonus we can also store larger values, but that's not really the main goal. Note that arithmetic with Spfq (where q + q is 7, 15, or 31) does not promote, consistent with the rest of Julia. This is just an unsigned-types-are-bad-news decision, not a decision to widen all arithmetic operations.

Again, I'm making the "extreme" case to show why this is worth considering. A more balanced view would point out two important negatives:

  • it's different from the rest of Julia
  • when applied to arrays, it causes the memory requirement to double

The latter is a problem, but the fact that it's limited to a doubling (and not, say, 8x) and that RAM is cheap means that I don't think the latter is fatal. I think the former is the more serious disadvantage of such a change.

If you ask me, what is my honest opinion, I am not sure. I support the fact that operations with UInt8 return UInt8. I am less sure that I support operations with N0f8 returning N0f8; to me it seems that no one chooses UInt8 without thinking about it, but for image processing I bet most users don't choose an element type at all, they take whatever arrives when you load the image from disk.

timholy avatar Nov 27 '19 15:11 timholy

We should make sure that major contributors to/watchers of JuliaImages are aware of this discussion: CC @kmsquire, @rsrock, @SimonDanisch, @Evizero, @tejus-gupta, @annimesh2809, @zygmuntszpak, @RalphAS, @Tokazama, @juliohm, @bjarthur, @ianshmean, @jw3126, @tlnagy. Sorry for the spam (and sorry if I omitted someone who might want to know).

timholy avatar Nov 27 '19 15:11 timholy

I'm skeptical of its practical usage.

I am less sure that I support operations with N0f8 returning N0f8

unless all operations are done under N0f8 without conversion, which is impossible as far as I can see, this feature is of less practical usage. A more common situation at present is:

# not returning N0f8 for N0f8 input
img_in = load(imgfile) # N0f8
img_temp = foo1(img_in) # N0f8->Float32
img_out = foo2(img_temp) # Float32
img_out = n0f8(img_out) # Float32->N0f8

Note that there're only two conversions.

But if it became:

# returning N0f8 for N0f8 input
img_in = load(imgfile) # N0f8
img_temp = foo1(img_in) # N0f8->Float32->N0f8
img_out = foo2(img_temp) # N0f8->Float32->N0f8

There're four now, where two of them are unnecessary. If I understand it right, what you're trying to do here is purely replacing Float32 with signed normed numbers. If it's about saving memory usage, Float16 might be a better choice (although the current performance is said poor).

I once opened an issue related to what I said here (preserving type): https://github.com/JuliaImages/ImageCore.jl/issues/82.

johnnychen94 avatar Nov 27 '19 16:11 johnnychen94

It's been a while since I deeply thought about FixedPointNumbers and the difference between Fixed and Normed.

Cons: this is against convention in the Julia ecosystem, and in opposition to #27.

I see there are two orthogonal concerns.

  1. Is a number normed or fixed point. This is a subtle concern that tripped me up in the past when trying to unify these two concepts. Both have fractional bits, but the meaning differs slightly.

  2. Is there a sign bit.

From a representational perspective I don't think there is any strong reason to not have signed normed numbers, although looking at my comment history I once remarked that I had no longer a strong reason for having unsigned fixed point numbers.

The normed numbers always had an odd place in the ecosystem, since I see them primarily as storage numbers and not as computational numbers. Then there is the question of semantics, which normed numbers mostly derive from what is needed in the Color ecosystem and as far as I can tell if one does computation on them, one might want to do saturating computation or one needs to change representation. But the semantics we have for Normed is overflow/underflow and that makes the least sense in image calculations.

In conclusion I don't have strong opinions and only existential questions. It might make sense to divorce fixed and normed and make normed an explicit color representational/computational type instead of having to worry about general semantics.

vchuravy avatar Nov 27 '19 17:11 vchuravy

From @johnnychen94 :

A more common situation at present is:

# not returning N0f8 for N0f8 input
img_in = load(imgfile) # N0f8
img_temp = foo1(img_in) # N0f8->Float32

and @vchuravy:

The normed numbers always had an odd place in the ecosystem, since I see them primarily as storage numbers and not as computational numbers.

Yep. Both of these express the notion that the current values are mostly storage and not computation. If you already think this way, there's little advantage to this proposal.

However, we do support computation with such values. Since I think I may not have been sufficiently clear, let me walk you through an interesting experiment. This is one that someone might do to compare two images, or look for frame-by-frame temporal differences in a movie:

julia> using ImageView, TestImages

julia> mri = testimage("mri");

julia> diff1 = mri[:,:,1:end-1] - mri[:,:,2:end];

julia> diff2 = float.(mri[:,:,1:end-1]) - float.(mri[:,:,2:end]);

julia> imshow(diff1); imshow(diff2)

It's worth doing this on your own, but to make life easy here are the first frames from each: d1 d2

The second one is "correct," with mid-gray encoding no difference. Or you can use colorsigned from ImageCore: d4 d3

(magenta is positive and green is negative)

Now, if you're used to calling float(img) before doing any computations, there is nothing to worry about. But new users often are bothered by this kind of overflow (e.g., @johnnychen94, you got bit by this in https://github.com/JuliaImages/ImageCore.jl/issues/63).

To the specifics of this proposal: aside from defining & supporting the type (which, as @kimikage indicated, is a burden), the change that would "fix" these issues is a single line, which could be written:

-(x::N0f8, y::N0f8) = S7f8(Int16(x.i) - Int16(y.i), 0)

This is definitely a breaking change, but it's not a huge computational burden:

julia> minus1(x, y) = x - y   # this is essentially what we do now
minus1 (generic function with 1 method)

julia> minus2(x, y) = Int16(x) - Int16(y)    # this is what we could do if we had S7f8
minus2 (generic function with 1 method)

when passed UInt8s is just the difference in "padding" with a byte of all zeros:

julia> @btime minus1(x, y) setup=(x = rand(UInt8); y = rand(UInt8))
  1.240 ns (0 allocations: 0 bytes)
0x22

julia> @btime minus2(x, y) setup=(x = rand(UInt8); y = rand(UInt8))
  1.239 ns (0 allocations: 0 bytes)
187

So in contrast to what @johnnychen94 said above, there isn't much/any of a conversion overhead to worry about. (For comparison, float(x) is more expensive, clocking in at 2ns.)

Now, just about the only operation this would affect are + and -; as soon as you multiply by a AbstractFloat you convert to floats and the rest of the computations would not be affected.

So, to recap, the costs of this are:

  • more breaking changes
  • more types to support
  • a doubling in size of images if you add or subtract them

and the benefits are

  • adding and subtracting will more often work more in the way users expect them to.

That's pretty much the bottom line, I think.

timholy avatar Nov 27 '19 20:11 timholy

I'm satisfied with the "breaking change" and it's more like bug fixes to me.

An alternative way to fix this issue is making Normed a pure storage type and promotes to float or Fixed for every relevant operation. That could really save a lot of effort.

johnnychen94 avatar Nov 27 '19 21:11 johnnychen94

So in contrast to what @johnnychen94 said above, there isn't much/any of a conversion overhead to worry about. (For comparison, float(x) is more expensive, clocking in at 2ns.)

As I showed in the past benchmarks, the benchmarks of a single operation can easily mislead us.:confused:

kimikage avatar Nov 27 '19 22:11 kimikage

It might be better to clarify what we need.

  • Extra bits
  • Sign bit (i.e. negative numbers)
  • Newbie-friendly promotion
  • Speed

It is premature to discuss the speed because the current Fixed and Normed are not fully optimized. (Edit: Moreover, CheckedArithmetic will provide a reasonable interface, but it is not always the fastest way. cf. https://github.com/JuliaMath/FixedPointNumbers.jl/issues/41#issuecomment-557806476) If we just need the extra bits for subtractions, we can use N8f8 instead of S7f8, although it is never newbie-friendly. With regards to the image processing, it is important that there are no negative RGB colors. Thus, we should (implicitly) apply the mapping.

Edit: From @vchuravy:

I see there are two orthogonal concerns.

  1. Is a number normed or fixed point. This is a subtle concern that tripped me up in the past when trying to unify these two concepts. Both have fractional bits, but the meaning differs slightly.
  2. Is there a sign bit.

I also agree about that. So, I think there are two types of the type hierarchy:

Fraction-first

  • FixedPoint
    • AbstractFixed
      • Fixed
      • UFixed
    • AbstractNormed
      • Normed
      • SNormed

Sign-first

  • FixedPoint
    • SignedFixedPoint
      • Fixed
      • SNormed
    • UnsignedFixedPoint
      • UFixed
      • Normed

I think it is a good idea to add the signed Normed, but Normed{<:Signed} is not good, as mentioned above.

kimikage avatar Nov 29 '19 03:11 kimikage

I also agree about that. So, I think there are two types of the type hierarchy:

Yeah I have a strong preference for the first one since it emphasises the semantic difference between Normed and Fixed.

vchuravy avatar Nov 29 '19 21:11 vchuravy

Since we can only choose one for now, I agree completely. However, I think choosing the former is not denying the latter. If we explicitly choose the former, we should be responsible for the implicit nature of the latter. Normed{<:Signed} breaks the nature of the latter.

kimikage avatar Nov 30 '19 00:11 kimikage

With regards to the image processing, it is important that there are no negative RGB colors. Thus, we should (implicitly) apply the mapping.

Not sure I fully agree here. We allow folks to construct out-of-gamut colors routinely. This is important in, e.g., computing the mean color of an image patch, where the natural implementation of mean is sum(a)/length(a). It's nice to have a type that enforces in-gamut, but that's not essential. Similar considerations apply in https://www.aristeia.com/Papers/C++ReportColumns/sep95.pdf (linked from the C++ link above).

Normed{<:Signed} is not good, as mentioned above.

I wasn't sure which place, can you elaborate? I don't think anything in the concept of "normed" enforces "unsignedness," so I don't see any problem with Normed{<:Signed}. You can always check for Signed in the type paramter if you need to differentiate (which I needed to do in a couple of places in this PR, though did not check in enough!).

timholy avatar Nov 30 '19 10:11 timholy

I'm sorry. I'm not good with words.

We allow folks to construct out-of-gamut colors routinely.

What I want to say is not about the gamut, but about the visualization. Since our LCDs cannot represent negative RGB colors, we need to project abstract numbers into concrete colors. It usually involves the type conversion. If you do type conversion after all, is there any reason not to convert the numbers to a more convenient type (e.g. Float32, Float16, Fixed) first? If you do not visualize the data, what is the difference between N8f8 and S7f8 in RAMs?

Although signed Normeds are useful as an intermediate storage type, I don't realize the need to implement those arithmetic.

kimikage avatar Nov 30 '19 12:11 kimikage

Good! This focuses the discussion down. Really I think we have 3 options:

  1. The current approach: implement math with Normed numbers, with the results also Normed. Advantage: consistent with the rest of Julia. Disadvantage: "wrong" results are returned for very simple operations.
  2. Declare that Normed numbers are storage only. Math always returns floating-point equivalents, e.g., +(x::N0f8, y::N0f8) = float(x) + float(y). Advantages: (1) "correct" results are always returned, although roundoff will become more annoying, and (2) considerable simplification of this package and the type landscape. Disadvantages: (1) less consistent with the rest of Julia. (2) there's a 4x blowup of size for most operations. (Float16 is too slow to be a practical result type.) (3) For some algorithms, like median filtering and histogram equalization, there are particularly efficient implementations that rely on the discretization provided by 8-bit values.
  3. Implement signed variants of Normed numbers. Advantages: "correct" results are usually returned (barring overflow), and roundoff won't happen since the underlying types are integral. Disadvantages: (1) less consistent with the rest of Julia, (2) more types we have to support, (3) like option 2, discretization becomes less of an advantage even by moving to 16-bit types (there are more bins you have to check).

timholy avatar Nov 30 '19 12:11 timholy

In comparing 2 vs 3, perhaps the key question is whether the extra complexity is worth optimizing a small number of operations (basically, addition and subtraction). For reference,

julia> function mysum(acc, A)
           @inbounds @simd for a in A
               acc += a
           end
           return acc
       end
mysum (generic function with 1 method)

julia> using FixedPointNumbers

julia> A = collect(rand(N0f8, 100));

julia> using BenchmarkTools

julia> Base.:(+)(x::N56f8, y::N0f8) = N56f8(x.i + y.i, 0)

julia> @btime mysum(zero(N56f8), A)
  23.733 ns (1 allocation: 16 bytes)
50.255N56f8

julia> @btime mysum(0.0f0, A)
  23.544 ns (1 allocation: 16 bytes)
50.254906f0

julia> @btime mysum(zero(N56f8), A)
  22.852 ns (1 allocation: 16 bytes)
50.255N56f8

julia> @btime mysum(0.0f0, A)
  23.782 ns (1 allocation: 16 bytes)
50.254906f0

so at least on my machine the performance difference seems to be within the noise. (I am surprised, TBH.)

Can others try this benchmark and report back?

timholy avatar Nov 30 '19 13:11 timholy

These trends apply not only to x86_64 but also to ARM (with NEON).

julia> versioninfo()
Julia Version 1.0.3
Platform Info:
  OS: Linux (arm-linux-gnueabihf)
  CPU: ARMv7 Processor rev 4 (v7l)
  WORD_SIZE: 32
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, cortex-a53)

julia> @btime mysum(zero(N56f8), A)
  1.219 μs (2 allocations: 32 bytes)
47.776N56f8

julia> @btime mysum(0.0f0, A)
  1.698 μs (1 allocation: 8 bytes)
47.776474f0

Of course, the 32-bit accumulator is faster.

julia> Base.:(+)(x::N24f8, y::N0f8) = N24f8(x.i + y.i, 0)

julia> @btime mysum(zero(N24f8), A)
  757.864 ns (2 allocations: 16 bytes)
47.776N24f8

kimikage avatar Nov 30 '19 15:11 kimikage

OK, so there are architectures where it makes a 2x difference. Is that (along with its domain of applicability, which is basically summation/differencing) enough to matter?

timholy avatar Nov 30 '19 18:11 timholy

I think this is a compiler issue rather than a CPU architecture or CPU performance issue. Of course, compiler-friendly types are better. However, is that really what you wanted to know with the benchmark?

Edit: I mainly support the option 1, but I do not object to adding isolated signed Normed numbers. I am strongly against the option 2. If you do that so, you should split FixedPointNumbers into FixedPointTypes and FixedPointArithmetic.

Edit2: My other concern is that FixedPointNumbers is immature. The packages which cannot follow this change can use the current (old) version, but who will support backporting in the future? The generalization is not so simple. We do easily (innocently) anti-generalization (see: #139).

kimikage avatar Nov 30 '19 23:11 kimikage

There are two orthogonal concepts for accumulation.

  • Using Float for the accumulator
  • Converting each element to Float
julia> function mysum_raw(acc, A)
           @inbounds @simd for a in A
               acc += reinterpret(a)
           end
           return acc / FixedPointNumbers.rawone(eltype(A))
       end

This is just a conceptual code and not practical. This difference might be noticeable for large (more practical) arrays.

It is premature to discuss the speed because the current Fixed and Normed are not fully optimized.

kimikage avatar Dec 01 '19 01:12 kimikage

I don't think most algorithms should dive into this level of detail about their element types. Your code above should be equivalent to mysum(zero(N56f8), A) (on a 64-bit machine) in all respects except the result type, and of course it can be converted to Float64 depending on what you do with it next. Relying on Julia's abstraction properties means we can write generic algorithms that don't have to be hand-tuned for each element type, without paying a performance penalty. But what we decide here will end up determining what that inner loop looks like.

timholy avatar Dec 01 '19 10:12 timholy

My conceptual code is never never never practically good. Is it related to this discussion? :confused: I don't want to explain the problems of your benchmark code in detail

kimikage avatar Dec 01 '19 13:12 kimikage

I mainly support the option 1, but I do not object to adding isolated signed Normed numbers. I am strongly against the option 2. If you do that so, you should split FixedPointNumbers into FixedPointTypes and FixedPointArithmetic.

Is it related to this discussion?

https://github.com/JuliaMath/FixedPointNumbers.jl/pull/143#issuecomment-559965758 is, in my mind (flaws included), a benchmark for whether 3 provides any advantage over 2. Given that you don't like option 2, and that at least on some architectures 3 does perform better than 2, it seems our main options are 1 and 3. Option 3 would allow one to fix #41, #126, https://github.com/JuliaImages/ImageCore.jl/issues/63. Option 1 is the status quo.

Can we agree on 3 as a reasonable consensus solution?

timholy avatar Dec 01 '19 15:12 timholy

First of all, a wrong benchmark misleads us. Since sum is a special case of addition, it is not fair. Moreover, the optimizer still has room for improvement. Do you want to "redesign" every time the compiler changes?

I think the main problem of #41 (and https://github.com/JuliaImages/ImageCore.jl/issues/63 ?) will be solved by @checked in the near future. I think #126 can be solved without Option 3.

What are the specific problems which Option 1 cannot solve?

kimikage avatar Dec 01 '19 16:12 kimikage

will be solved by @checked in the near future

I don't doubt you already know everything I'm about to say, so I may not be understanding your true question. But let me try: @checked supports "normal" arithmetic with the extra feature that it throws an error if the results overflow. For any nontrivial image pair, @checked img1 - img2 would result in an error. Is that a solution? I guess it depends on your perspective. Would one always want @checked arithmetic? Doubtful, since image-processing is performance-sensitive. So what is @checked for? Mainly tests and debugging, I think. It only works on surface syntax; to go "deeper" we'd need a separate package, CheckedFixedPointNumbers that checks all operations by default. I am fairly certain that there isn't a good mechanism that (1) lets you have just one type, (2) works with precompilation, (3) allows you to switch between checked and unchecked arthmetic, and (4) doesn't also incur a performance penalty. And again, the only thing that buys you is an error when overflow is detected.

Let me try to get others to chime in by making the options here crystal clear:

  1. we can have img1 - img2 work for arbitrary images but use "wraparound" behavior. This is the status quo.
  2. we can have img1 - img2 throw a MethodError, forcing users to write float.(img1) - float.(img2). This will avoid the "wraparound" behavior. Implementing this option would simply delete the code supporting arithmetic from FixedPointNumbers.
  3. we can have img1 - img2 promote eltypes automatically to something that doesn't typically wrap around. This comes in two flavors: a. promote to float for all operations b. promote to Normed{<:Signed} for at least addition and subtraction

My hope in submitting this PR was that option 3b would "thread the needle" and be an acceptable option to everyone. I think that's pretty clearly not the case. If I understand the voting so far, it looks like this (:+1: means approve, :-1: means disapprove, nothing means relatively neutral):

Option 1: status quo Option 2: force conversion Option 3a: promotion to float Option 3b: promotion to Normed{<:Signed}
@kimikage :+1: :-1: :-1:
@johnnychen94 :-1:
@timholy :+1: :-1: :-1: :+1:
@ianshmean :-1: slight :+1:
@tlnagy :-1: :-1: :+1:

If I've misunderstood people's votes, please correct me. How do others vote?

timholy avatar Dec 01 '19 18:12 timholy

Your explanation is very easy to understand. :+1:

Option 2: force conversion: :+1: delete the code supporting arithmetic: :-1: (Edit: I think the MethodError is not user-friendly. We can communicate with the users by means of a error message.)

I don't know much about the costs of education, but generally it's not bad to be able to manage your own workspace yourself.

Whether the answer is correct or not is determined by the specifications, not by the absence of "overflow". FixedPointNumbers has no way of knowing the user's specifications. Wait for someone to create FormalMethods.jl. However, the higher the level, the closer to the users.

In the first place, where do Normed numbers come from in image processing? Do the users really create them? I think breaking or narrowing the entry pathway is a common tactics. I can't agree with this much, but this is better than Option 3.

kimikage avatar Dec 02 '19 00:12 kimikage

To clarify, option 2 = "force conversion" is equivalent to "delete the code supporting arithmetic." The only way to force users to convert to some other type before doing arithmetic is for arithmetic on Normed types to throw an error. Option 2 is the "Normed types are for storage only" option. So your votes on Option 2 are in conflict with one another. If you don't want to delete the code supporting arithmetic, you have to decide what type arithmetic returns, leaving options 1, 3a, and 3b.

where do Normed numbers come from in image processing

img = load("mypicture.png"). They are what we return for any integer-encoded image (e.g., 8- and 16-bit grayscale and color images, where for color 8/16 means per channel). The majority of users never make a conscious decision about the element type they receive; nor have many read the documentation on FixedPointNumbers.

I've checked every package in JuliaRegistries/General that depends on FixedPointNumbers. The only one that isn't related to image processing or colors is https://github.com/JuliaAudio/SampledSignals.jl, which from their README would apparently benefit from Normed{<:Signed}.

timholy avatar Dec 02 '19 01:12 timholy

Option 2: neutral 😑 I'm only unsatisfied with the status quo. Option 2 is what users of FixedPointNumbers.jl can do to solve this issue at present without touching this package. I can see the importance of Option 3(especially Option 3b), but since I'm not familiar with the implementation details and not care much about the sizes at present, really I have nothing much to comment on.

What are the specific problems which Option 1 cannot solve?

If it's on test stage, ReferenceTest.jl already bring us a tool to examine the numerical results on different types. The problem with Option 1 is that we can't guarantee the numerical accuracy without test.

Plug-and-play is a very common style when we're processing images. As a playground, we generally don't write tests. When wrap around behavior happens without detected, we would tend to conclude that "my processing pipeline should be modified" and it's actually quite severe mislead in many research scenarios.

Where do Normed numbers come from in image processing? Do the users really create them?

I only manually convert Float32 to Normed when I need to benchmark different algorithms -- take advantages of the inaccuracy of representation -- just to have a more stable benchmark result, e.g.,

julia> float_x = 0.1
0.1

julia> float_y = 0.103
0.103

julia> N0f8(float_x) - N0f8(float_y)
0.0N0f8

johnnychen94 avatar Dec 02 '19 01:12 johnnychen94

@johnnychen94, thanks for clarifying; I've updated your votes in the table accordingly.

One thing I should also make sure is understood: Python's scikit-image uses Option 1; Matlab uses Option 1 but uses saturating arithmetic. If we choose something different, we're blazing a different path. But we're already doing that with FixedPointNumbers anyway; other frameworks that support both "fixed point" (integer) images and floating-point images are content to live with the equation 255 == 1.0 (sometimes), whereas we are not. Moreover, both Matlab & Python have to make this decision in the context of general operations on arrays with a very limited palette of element types, whereas we're in the situation of having an image-specific element type and hence have greater freedom to set our own rules. Consequently, I don't think it's at all crazy to do something "better" than what they do, if indeed it is possible to do anything that is better.

Consequently I understand @johnnychen94's dissatisfaction with the current status quo, even if I also acknowledge that it's one of the most self-consistent options we have.

timholy avatar Dec 02 '19 01:12 timholy

@timholy:

To clarify, option 2 = "force conversion" is equivalent to "delete the code supporting arithmetic."

I understand and clearly disagree with Option 2.

The only way to force users to convert to some other type before doing arithmetic is for arithmetic on Normed types to throw an error.

I cannot completely agree with that. I just proposed another way. (The way is not pretty good, though.)

I've checked every package in JuliaRegistries/General that depends on FixedPointNumbers.

Great survey! However, isolated signed Normed and Normed{<:Signed} should be clearly distinguished. The former should not be detrimental to the users (except for the unnatural naming), but the latter is a breaking change, even though it may be a minor change for you. Again, "optimization is specialization".

@johnnychen94:

The problem with Option 1 is that we can't guarantee the numerical accuracy without test.

That's right! Therefore, I think testing is very important. A program with no specifications or tests is a poem. Although I do never hate poetry, it is not the basis for software design. If the errors mislead us, we should consider throwing correct and useful errors first. I don't think this is an obligation of low-level packages like FixedPointNumbers. Of course, it is good to improve FixedPointNumbers for the better errors.


This is just a thought experiment. What about NormedFloat instead of Normed{<:Signed}? I can assure you that the fully-optimized NormedFloat is not inferior to Normed{<:Signed} in terms of speed and size. However, I don't think it is good. What is the advantage of Normed{<:Signed} over NormedFloat?

Edit: BTW, I'm somewhat violent or rude, because it is a little stressful for me to express my (abstract) thoughts in English and I'm afraid that the discussion will go on while I am translating. Forgive me for being so rude. How much time do we have for this discussion?

kimikage avatar Dec 02 '19 03:12 kimikage

This is just a thought experiment. What about NormedFloat instead of Normed{<:Signed}? I can assure you that the fully-optimized NormedFloat is not inferior to Normed{<:Signed} in terms of speed and size. However, I don't think it is good. What is the advantage of Normed{<:Signed} over NormedFloat?

What is NormedFloat?

How much time do we have for this discussion?

It can go on paralyzing development forever, probably :wink:. Seriously, we need to reach some kind of consensus here. Why don't you propose what you think we should do, rather than just argue against what I am proposing? AFAICT the only solution currently is the status quo. I can live with that but I'm not happy with it. Perhaps you feel you have proposed a solution, but if so I didn't understand it.

EDIT: To be concrete, I was hoping to put in a grant on Dec. 17th to try to fund work on JuliaImages. It would be great to reach consensus well before that.

timholy avatar Dec 02 '19 12:12 timholy