Skip to content

Commit

Permalink
more careful treatment of integers in bitmasks
Browse files Browse the repository at this point in the history
  • Loading branch information
weaverba137 committed Jan 10, 2025
1 parent 4969d29 commit ea16d11
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 27 deletions.
20 changes: 11 additions & 9 deletions py/desiutil/bitmask.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
.. _desispec: https://desispec.readthedocs.io/en/latest/
"""
import numpy as np


class _MaskBit(int):
Expand Down Expand Up @@ -222,7 +223,8 @@ def names(self, mask=None):
----------
mask : :class:`int`, optional
The mask integer to convert to names. If not supplied,
return names of all known bits.
return names of all known bits. `mask` can also be a numpy scalar
or a numpy array of length 1.
Returns
-------
Expand All @@ -236,14 +238,14 @@ def names(self, mask=None):
for bitnum in sorted(bitnums):
names.append(self._bits[bitnum].name)
else:
# The line below throws a lot of warnings:
# DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated,
# and will error in future. Ensure you extract a single element from your array
# before performing this operation. (Deprecated NumPy 1.25.)
# It's not obvious where this is coming from though, since `mask`
# clearly should not be an array.
# https://github.com/numpy/numpy/issues/2955 was likely fixed in 2022.
mask = int(mask) # workaround numpy issue #2955 for uint64
if isinstance(mask, int) or isinstance(mask, np.integer):
pass
elif isinstance(mask, np.ndarray) and mask.shape == (1, ):
mask = mask[0]
else:
raise ValueError('Unknown type or invalid shape for mask!')
if mask < 0:
mask = np.int64(mask).astype(np.uint64)
bitnum = 0
while 2**bitnum <= mask:
if (2**bitnum & mask):
Expand Down
51 changes: 33 additions & 18 deletions py/desiutil/test/test_bitmask.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_access(self):
self.assertEqual(self.ccdmask.HOT.mask, 2)
self.assertEqual(self.ccdmask.HOT, 2)
self.assertEqual(self.ccdmask.HOT.comment, "Hot pixel")
self.ccdmask.names()
self.assertListEqual(self.ccdmask.names(), ['BAD', 'HOT', 'DEAD', 'SATURATED', 'COSMIC'])

def test_badname(self):
"""Test raising AttributeError for bad names.
Expand All @@ -97,7 +97,7 @@ def test_badname(self):
def test_str(self):
"""Verify yaml-ness of string representation"""
bitmask = BitMask('ccdmask', yaml.safe_load(str(self.ccdmask)))
self.assertEqual(bitmask.names(), self.ccdmask.names())
self.assertListEqual(bitmask.names(), self.ccdmask.names())
for name in bitmask.names():
self.assertEqual(bitmask[name].mask, self.ccdmask[name].mask)
self.assertEqual(bitmask[name].comment, self.ccdmask[name].comment)
Expand All @@ -120,29 +120,44 @@ def test_uint64(self):
_bitdefs['ccdmask'].append(['BIG31', 31, "blat31..."])
_bitdefs['ccdmask'].append(['BIGGER32', 32, "blat32..."])
_bitdefs['ccdmask'].append(['WOW62', 62, "blat62..."])
# _bitdefs['ccdmask'].append(['BIGGEST63', 63, "blat63..."])
_bitdefs['ccdmask'].append(['BIGGEST63', 63, "blat63..."])

num2name = dict([(m[1], m[0]) for m in _bitdefs['ccdmask']])
mask = BitMask('ccdmask', _bitdefs)

self.assertEqual(mask.names(1), ['BAD'])
self.assertEqual(mask.names(2), ['HOT'])
self.assertEqual(mask.names(3), ['BAD', 'HOT'])
self.assertEqual(mask.names(4), ['UNKNOWN2'])
self.assertEqual(mask.names(8), ['UNKNOWN3'])
self.assertEqual(mask.names(2**16), ['TEST'])
self.assertEqual(mask.names(2**31), ['BIG31'])
self.assertEqual(mask.names(2**32), ['BIGGER32'])
self.assertEqual(mask.names(2**62), ['WOW62'])
# self.assertEqual(mask.names(2**63), ['BIGGEST63'])
allmasks = 2**63 | 2**62 | 2**32 | 2**31 | 2**16 | 2**1 | 2**0

self.assertListEqual(mask.names(allmasks),
['BAD', 'HOT', 'TEST', 'BIG31', 'BIGGER32', 'WOW62', 'BIGGEST63'])
self.assertListEqual(mask.names(np.uint64(allmasks)),
['BAD', 'HOT', 'TEST', 'BIG31', 'BIGGER32', 'WOW62', 'BIGGEST63'])
self.assertListEqual(mask.names(np.uint64(allmasks).astype(np.int64)),
['BAD', 'HOT', 'TEST', 'BIG31', 'BIGGER32', 'WOW62', 'BIGGEST63'])

for i in range(64):
if i in num2name:
n = [num2name[i]]
else:
n = ['UNKNOWN' + str(i)]
names = mask.names(2**i)
# names = mask.names(int(2**i))
self.assertListEqual(names, n)
names = mask.names(np.uint64(2**i))
# Also happens to work with length-1 arrays; maybe it shouldn't
self.assertListEqual(names, n)
# Allow testing of negative numbers while avoiding overflow errors.
names = mask.names(np.uint64(2**i).astype(np.int64))
self.assertListEqual(names, n)
# Test with length 1 arrays.
names = mask.names(np.array([2**i], dtype=np.uint64))
if i < 63:
names = mask.names(np.array([2**i], dtype=np.int64))
self.assertListEqual(names, n)
names = mask.names(np.array([2**i], dtype=np.uint64).astype(np.int64))
self.assertListEqual(names, n)

# Arrays of more than one element should throw ValueError.
with self.assertRaises(ValueError):
names = mask.names(np.array([2**10, 2**20], dtype=np.uint64))
# Try with an unexpected type
with self.assertRaises(ValueError):
names = mask.names(float(2**32))

def test_print(self):
"""Test string representations.
Expand All @@ -165,4 +180,4 @@ def test_print(self):
self.assertEqual(blat, _bitdefyaml)
for i, name in enumerate(self.ccdmask.names()):
self.assertEqual(str(self.ccdmask[name]), bit_str[i])
self.assertEqual(repr(self.ccdmask[name]), bit_repr[i])
self.assertEqual(repr(self.ccdmask[name]), bit_repr[i])#

0 comments on commit ea16d11

Please sign in to comment.