Skip to content

Dubious type casting for augmented arithmetic operators #435

@sadielbartholomew

Description

@sadielbartholomew

We currently allow, and validate via testing in test_Data_BINARY_AND_UNARY_OPERATORS, some behaviour relating to input and output data types for augmented arithmetic assignment operators that is not allowed by NumPy, and we should consider whether this is suitable or not. I am inclined to say we should make appropriate changes to adopt the NumPy behaviour.

Specifics

Namely, when an augmented assignment is performed using inputs with data types which lead to a change in data type for the output, e.g. for a simplified scalar case something like a = 1; a += 1.0, we permit an in-place change of array dtype. As a minimal example, note how NumPy raises a type casting error for the equivalent operation below, whereas we go ahead and produce an output with a changed data type, the same type that the operation not in-place would produce:

>>> import cf
>>> import numpy as np
>>> 
>>> # Setup equivalent arrays
>>> i_np = np.array([1, 2, 3])
>>> i_cf = cf.Data(i_np)
>>> 
>>> # NumPy raises a type casting error:
>>> i_np + 1.0  # operation not in-place is fine
array([2., 3., 4.])
>>> i_np += 1.0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
numpy.core._exceptions.UFuncTypeError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int64') with casting rule 'same_kind'
>>> 
>>> # ... whereas cf performs the operation to give the same result data type
>>> # as the non in-place operation would:
>>> i_cf + 1.0
<CF Data(3): [2.0, 3.0, 4.0]>
>>> i_cf += 1.0
>>> i_cf
<CF Data(3): [2.0, 3.0, 4.0]>

and the equivalent behaviour occurs for the various __i<operator>__ operators.

Relevant cases in test suite

For reference, the tests in test_Data_BINARY_AND_UNARY_OPERATORS which were checking for this (dubious) behaviour, which remain as such from before the LAMA to Dask migration, are:

cf-python/cf/test/test_Data.py

Lines 2062 to 2154 in 0033743

a = a0.copy()
try:
a += x
except TypeError:
pass
else:
e = d.copy()
e += x
message = "Failed in {!r}+={}".format(d, x)
self.assertTrue(
e.equals(cf.Data(a, "m"), verbose=1), message
)
a = a0.copy()
try:
a *= x
except TypeError:
pass
else:
e = d.copy()
e *= x
message = "Failed in {!r}*={}".format(d, x)
self.assertTrue(
e.equals(cf.Data(a, "m"), verbose=1), message
)
a = a0.copy()
try:
a /= x
except TypeError:
pass
else:
e = d.copy()
e /= x
message = "Failed in {!r}/={}".format(d, x)
self.assertTrue(
e.equals(cf.Data(a, "m"), verbose=1), message
)
a = a0.copy()
try:
a -= x
except TypeError:
pass
else:
e = d.copy()
e -= x
message = "Failed in {!r}-={}".format(d, x)
self.assertTrue(
e.equals(cf.Data(a, "m"), verbose=1), message
)
a = a0.copy()
try:
a //= x
except TypeError:
pass
else:
e = d.copy()
e //= x
message = "Failed in {!r}//={}".format(d, x)
self.assertTrue(
e.equals(cf.Data(a, "m"), verbose=1), message
)
# TODODASK SB: re-instate this once _combined_units is sorted,
# presently fails with error, as with __pow__:
# AttributeError: 'Data' object has no attribute '_size'
# a = a0.copy()
# try:
# a **= x
# except TypeError:
# pass
# else:
# e = d.copy()
# e **= x
# message = "Failed in {!r}**={}".format(d, x)
# self.assertTrue(
# e.equals(cf.Data(a, "m2"), verbose=1), message
# )
a = a0.copy()
try:
a.__itruediv__(x)
except TypeError:
pass
else:
e = d.copy()
e.__itruediv__(x)
message = "Failed in {!r}.__itruediv__({})".format(d, x)
self.assertTrue(
e.equals(cf.Data(a, "m"), verbose=1), message
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionGeneral questiontestingIssues related to units tests and their coverage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions