3

I have a numpy array of complex numbers and need to create a new array with rounded real and imaginary parts, where the rounding at half is either toward zero or away from zero.

There are several recommendations on stackoverflow for using the decimal package which allow one to specify different types of rounding. For an array of complex numbers x, the following code worked, but was very slow:

    rounded_array = np.array([
        float(Decimal(x.real).quantize(0, rounding=ROUND_HALF_DOWN)) + 1j * \
        float(Decimal(x.imag).quantize(0, rounding=ROUND_HALF_DOWNs)) for x in arr])

What are some simple but faster alternatives to this?

The precise meanings of ROUND_HALF_UP and ROUND_HALF_DOWN are shown here: https://docs.python.org/3/library/decimal.html#decimal.ROUND_HALF_UP.

To be very clear, for rounding away from zero or toward zero, for say the real parts of the complex number, I seek (notice the differences at the halves)

      toward zero(ROUND_HALF_DOWN) away from zero (ROUND_HALF_UP)
-4.00         -4.0                 -4.0
-3.75         -4.0                 -4.0
-3.50         -3.0                 -4.0
-3.25         -3.0                 -3.0
-3.00         -3.0                 -3.0
-2.75         -3.0                 -3.0
-2.50         -2.0                 -3.0
-2.25         -2.0                 -2.0
-2.00         -2.0                 -2.0
-1.75         -2.0                 -2.0
-1.50         -1.0                 -2.0
-1.25         -1.0                 -1.0
-1.00         -1.0                 -1.0
-0.75         -1.0                 -1.0
-0.50         -0.0                 -1.0
-0.25         -0.0                 -0.0
 0.00          0.0                  0.0
 0.25          0.0                  0.0
 0.50          0.0                  1.0
 0.75          1.0                  1.0
 1.00          1.0                  1.0
 1.25          1.0                  1.0
 1.50          1.0                  2.0
 1.75          2.0                  2.0
 2.00          2.0                  2.0
 2.25          2.0                  2.0
 2.50          2.0                  3.0
 2.75          3.0                  3.0
 3.00          3.0                  3.0
 3.25          3.0                  3.0
 3.50          3.0                  4.0
 3.75          4.0                  4.0
 4.00          4.0                  4.0

The accepted solution to How to always round up a XX.5 in numpy is both slow and does not provide the type of rounding I'm interested in.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
rhz
  • 960
  • 14
  • 29
  • Add/subtract half and truncate – Mad Physicist Jun 30 '20 at 19:40
  • Is there a reason `np.fix`, `np.ceil` and `np.floor` don't do what you need? – bnaecker Jun 30 '20 at 19:47
  • "Please leave a comment instead of voting to close this question" is not a good thing to write. It implies that you know something is wrong with your question. Please write a question whose quality precludes the necessity for such a disclaimer, or you are likely to be closed down. – Mad Physicist Jun 30 '20 at 19:59
  • @MadPhysicist A previous version was closed down I believe because there were those who didn't understand the question. It was my intention to have the opportunity to clarify the question first if it's unclear. – rhz Jun 30 '20 at 20:01
  • @rhz. I highly recommend you remove the disclaimer and make sure you are following site rules. Remember that people are helping you for free here. It is your job to ask a proper question, not direct their voting or closing. – Mad Physicist Jun 30 '20 at 20:02
  • @MadPhysicist, BTW, you are right. It appears that at least for a real numbers: if x < 0: return np.trunc(x - .5) else: return np.trunc(x + 0.5) works for rounding away from zero. – rhz Jun 30 '20 at 20:02
  • @MadPhysicist No problem. Disclaimer removed. – rhz Jun 30 '20 at 20:03
  • In addition to posting here, I've provided a proper answer to the linked question: https://stackoverflow.com/a/62665628/2988730. – Mad Physicist Jun 30 '20 at 20:28

1 Answers1

1

The idea behind the linked answer is sound, but np.vectorize does not really vectorize anything. Let's look at ROUND_HALF_DOWN, which rounds towards zero. This means that for x >= 0, you want to get ceil(x - 0.5). For x < 0 you want to do floor(x + 0.5). This can be accomplished using a vectorized operation, e.g.

mask = (x >= 0)
output = x.copy()
np.add(output, -0.5, where=mask, out=output)
np.add(output, 0.5, where=~mask, out=output)
np.ceil(output, where=mask, out=output)
np.floor(output, where=~mask, out=output)

More space-consumingly, but less verbosely:

mask = (x >= 0)
output = np.empty_like(x)
output[mask] = np.ceil(x[mask] - 0.5)
output[~mask] = np.floor(x[~mask] + 0.5)

To do the operations in-place, just operate on x instead of output. The second part is treating the real and imaginary parts of the array separately. For contiguous arrays, this is easily done with a view, e.g.:

x = x.view(np.float)

You can convert back with

x = x.view(np.complex)

If your arrays are non-contiguous, your best bet is to process the real and imaginary components separately. x.real and x.imag are views into the data, so you can manipulate it in-place.

TL;DR

def round_half_down(arr):
    output = arr.copy().view(np.float)   # This won't fail because the copy is contiguous
    mask = (output >= 0)
    np.subtract(output, 0.5, where=mask, out=output)
    np.ceil(output, where=mask, out=output)
    np.invert(mask, out=mask)
    np.add(output, 0.5, where=mask, out=output)
    np.floor(output, where=mask, out=output)
    return output.view(np.complex)
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • @rhz. Yes, for the first part of the question I used `x`, then switched to `arr`. They both represent the array you want to work with. – Mad Physicist Jul 02 '20 at 00:37
  • sorry I meant to say that under the TL;DR, you meant to put mask = (output >= 0) rather than mask = (arr >= 0), right? – rhz Jul 02 '20 at 03:01
  • @rhz. Excellent catch. Thank you. Fixed – Mad Physicist Jul 02 '20 at 03:27
  • 1
    Note that there are some floating-point corner cases to watch out for. These may not matter to the OP. For example, if `x = 5000000000000001.0` (which is exactly representable as an IEEE 754 binary64 float) then `ceil(x - 0.5)` evaluates to `5000000000000000.0`, because `x - 0.5` is _not_ exactly representable, so is rounded, and then that rounded result is passed to the `ceil` operation. There are similar issues with `floor(x + 0.5)` for positive `x` (e.g., it rounds `0.49999999999999994` to `1.0`). – Mark Dickinson Jul 02 '20 at 16:37
  • @MarkDickinson. Those are excellent points. The goal is to set up OP to handle this in a vectorized manner. You can add bulk special case handling by adding a mask for `|x| > 499999....9` (or whatever the actual threshold is for the number always being an integer is), and a mask for numbers whose difference from the nearest odd multiple of 0.5 is within 1e-15 or so. I suspect that even operations like that will be cheaper than using `np.vectorize`, and hopefully OP can figure out how to do them now. – Mad Physicist Jul 02 '20 at 17:10