将 numpy 数组重新排序为没有循环的新位长元素

Reorder numpy array to new bitlength elements without loop

如果我有一个 numpy 数组,每个元素代表例如一个 9 位整数,是否有一种简单的方法(可能没有循环)以一种方式重新排序它,使得每个结果数组元素代表一个 8 位整数,在前一个元素的末尾添加“丢失的位”在下一个元素的开头? 例如得到以下

np.array([0b100111000, 0b100101100, 0b110011100, 0b110010100])  # initial array in binarys
# convert to
np.array([0b10011100, 0b01001011, 0b00110011, 0b10011001, 0b01000000])  # resulting array

我希望我要归档的内容是可以理解的。 附加信息,我不知道这是否有任何区别: 我所有的 9 位数字都以 msb beeing 1 开头(它们大于 255),最后两位始终为 0,如上例所示。 我要处理的数组更大,有数千个元素。

提前感谢您的帮助!

编辑:

我目前的(复杂的)解决方案如下:

import numpy as np
def get_bits(data, offset, leng):
    data = (data % (1 << (offset + leng))) >> offset
    return data

data1 = np.array([0b100111000, 0b100101100, 0b110011100, 0b110010100])
i = 1
part1 = []
part2 = []
for el in data1:
    if i == 1:
        part2.append(0)
    part1.append(get_bits(el, i, 8))
    part2.append(get_bits(el, 0, i)<<(8-i))
    if i == 8:
        i = 1
        part1.append(0)
    else:
        i += 1
if i != 1:
    part1.append(0)
res = np.array(part1) + np.array(part2)

我想我理解了你想要的大部分内容,并且鉴于你可以对 numpy 数组进行位操作,在这种情况下,如果对两个数组进行操作(或者对所有数组都相同,则可以明智地获得所需的位操作元素)它是一个数组与一个数字),那么你需要构造适当的数组来做这件事,所以像这样

>>> import numpy as np
>>> x = np.array([0b100111000, 0b100101100, 0b110011100, 0b110010100])
>>> goal=np.array([0b10011100, 0b01001011, 0b00110011, 0b10011001, 0b01000000])
>>> x
array([312, 300, 412, 404])
>>> goal
array([156,  75,  51, 153,  64])
>>> shift1 = np.array(range(1,1+len(x)))
>>> shift1
array([1, 2, 3, 4])
>>> mask1  = np.array([2**n -1 for n in range(1,1+len(x))])
>>> mask1
array([ 1,  3,  7, 15])
>>> res=((x>>shift1)|((x&mask1)<<(9-shift1)))&0b11111111
>>> res
array([156,  75,  51, 153], dtype=int32)
>>> goal
array([156,  75,  51, 153,  64])
>>> 

我不明白为什么你的目标数组多了一个元素,但是上面的操作给了其他的数字,多加一个并不复杂,所以根据需要调整。

现在解释((x>>shift1)|((x&mask1)<<(9-shift1)))&0b11111111

首先我注意到你按元素做了更大的偏移,这很简单

>>> x>>shift1
array([156,  75,  51,  25], dtype=int32)
>>> 
>>> list(map(bin,x>>shift1))
['0b10011100', '0b1001011', '0b110011', '0b11001']
>>> 

我们还想捕获会因移位而丢失的位,使用 以及适当的掩码,我们得到那些

>>> x&mask1
array([0, 0, 4, 4], dtype=int32)
>>> list(map(bin,mask1))
['0b1', '0b11', '0b111', '0b1111']
>>> list(map(bin,x&mask1))
['0b0', '0b0', '0b100', '0b100']
>>> 

然后我们将结果向右移动互补量

>>> 9-shift1
array([8, 7, 6, 5])
>>> ((x&mask1)<<(9-shift1))
array([  0,   0, 256, 128], dtype=int32)
>>> list(map(bin,_))
['0b0', '0b0', '0b100000000', '0b10000000']
>>> 

然后我们一起

>>> (x>>shift1) | ((x&mask1)<<(9-shift1))
array([156,  75, 307, 153], dtype=int32)
>>> list(map(bin,_))
['0b10011100', '0b1001011', '0b100110011', '0b10011001']
>>> 

最后我们用0b11111111只保留我们想要的8位


此外,您提到最后 2 位始终为零,那么更简单的解决方案是将其简单地移位 2,然后向另一个方向移回原位即可恢复原来的状态

>>> x
array([312, 300, 412, 404])
>>> y = x>>2
>>> y
array([ 78,  75, 103, 101], dtype=int32)
>>> y<<2
array([312, 300, 412, 404], dtype=int32)
>>>     

您可以使用 np.unpackbits and np.packbits 分两步完成。首先将您的数组转换为小端列向量:

>>> z = np.array([0b100111000, 0b100101100, 0b110011100, 0b110010100], dtype='<u2').reshape(-1, 1)
>>> z.view(np.uint8)
array([[ 56,   1],
       [ 44,   1],
       [156,   1],
       [148,   1]], dtype=uint8)

您可以通过解包直接将其转换为位数组。事实上,在某些时候 (PR #10855) 我添加了 count 参数来为您截断高零值:

>>> np.unpackbits(z.view(np.uint8), axis=1, bitorder='l', count=9)
array([[0, 0, 0, 1, 1, 1, 0, 0, 1],
       [0, 0, 1, 1, 0, 1, 0, 0, 1],
       [0, 0, 1, 1, 1, 0, 0, 1, 1],
       [0, 0, 1, 0, 1, 0, 0, 1, 1]], dtype=uint8)

现在你可以重新打包反转的数组:

>>> u = np.unpackbits(z.view(np.uint8), axis=1, bitorder='l', count=9)[:, ::-1].ravel()
>>> result = np.packbits(u)
>>> result.dtype
dtype('uint8')
>>> [bin(x) for x in result]
['0b10011100', '0b1001011', '0b110011', '0b10011001', '0b1000000']

如果您的机器是本地小端(例如,大多数英特尔架构),您可以在一行中执行此操作:

z = np.array([0b100111000, 0b100101100, 0b110011100, 0b110010100])
result = np.packbits(np.unpackbits(z.view(np.uint8), axis=1, bitorder='l', count=9)[:, ::-1].ravel())

否则,您可以 z.byteswap().view(np.uint8) 获得正确的开始顺序(不过我想仍然是一个班轮)。

一直困扰我的是 np.packbitsnp.unpackbits 效率低下,所以我想出了一个有点无聊的答案。

总体思路是像任何重采样器一样使用它:创建一个输出数组,然后找出每个输出片段在输入中的来源。你有 N 个元素,每个元素都是 9 位,所以输出是:

data = np.array([0b100111000, 0b100101100, 0b110011100, 0b110010100])
result = np.empty(np.ceil(data.size * 9 / 8).astype(int), dtype=np.uint8)

每九个输出字节相对于相应的八个输入字节具有以下模式。我使用 {...} 来表示每个输入整数中的(包含)位:

result[0] =              data[0]{8:1}
result[1] = data[0]{0:0} data[1]{8:2}
result[2] = data[1]{1:0} data[2]{8:3}
result[3] = data[2]{2:0} data[3]{8:4}
result[4] = data[3]{3:0} data[4]{8:5}
result[5] = data[4]{4:0} data[5]{8:6}
result[6] = data[5]{5:0} data[6]{8:7}
result[7] = data[6]{6:0} data[7]{8:8}
result[8] = data[7]{7:0}

result 的索引(称之为 i)实际上是给定模 9。因此数据中的索引被 8 * (i // 9) 偏移。字节的低位部分由 data[...] >> (i + 1) 给出。上部由 data[...] & ((1 << i) - 1) 给出,左移 8 - i 位。

这使得提出矢量化解决方案变得非常容易:

i = np.arange(result.size)
j = i[:-1]
result[i] = (data[8 * (i // 9) + (i % 9) - 1] & ((1 << i % 9) - 1)) << (8 - i % 9)
result[j] |= (data[8 * (j // 9) + (j % 9)] >> (j % 9 + 1)).astype(np.uint8)

你需要裁剪低部分的索引,因为它可能会越界。您不需要剪掉高部分,因为 -1 是一个完全有效的索引,您不关心它访问的是哪个元素。当然,numpy 不会让您或将 int 元素添加到 uint8 数组,因此您必须强制转换。

>>> [bin(x) for x in result]
['0b10011100', '0b1001011', '0b110011', '0b10011001', '0b1000000']

这个解决方案应该可以扩展到任何大小的数组,我写它是为了让你可以计算出不同的轮班组合,而不仅仅是朝九晚八。