Exercise 1

In [1]:
import numpy as np

We create two arrays of shape 2x2, both of type unsigned integer with 8 bits (1 byte), the first C-contiguous and the second F-contiguous.

In [2]:
x = np.array([[1, 2], [3, 4]], order='C', dtype=np.uint8)
y = np.array([[1, 2], [3, 4]], order='F', dtype=np.uint8)
print(x)
print(y)
[[1 2]
 [3 4]]
[[1 2]
 [3 4]]

We inspect the string representing the data behind the two arrays.

In [3]:
# Option 1
print(x.tobytes('A'))
print(y.tobytes('A'))
# Option 2 (more readable)
print([hex(el) for el in x.tobytes(order='A')])
print([hex(el) for el in y.tobytes(order='A')])
b'\x01\x02\x03\x04'
b'\x01\x03\x02\x04'
['0x1', '0x2', '0x3', '0x4']
['0x1', '0x3', '0x2', '0x4']

The numerical values of the two arrays are the same, but the data is not.

We create two arrays of shape 2x2, both of type unsigned integer with 32 bits (4 bytes), the first C-contiguous and the second F-contiguous.

In [4]:
x = np.array([[1, 2], [3, 4]], order='C', dtype=np.uint32)
y = np.array([[1, 2], [3, 4]], order='F', dtype=np.uint32)
print(x)
print(y)
[[1 2]
 [3 4]]
[[1 2]
 [3 4]]

We inspect the string representing the data behind the two arrays.

In [5]:
# Option 1
print(x.tobytes('A'))
print(y.tobytes('A'))
# Option 2 (more readable)
print([hex(el) for el in x.tobytes(order='A')])
print([hex(el) for el in y.tobytes(order='A')])
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00'
b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00'
['0x1', '0x0', '0x0', '0x0', '0x2', '0x0', '0x0', '0x0', '0x3', '0x0', '0x0', '0x0', '0x4', '0x0', '0x0', '0x0']
['0x1', '0x0', '0x0', '0x0', '0x3', '0x0', '0x0', '0x0', '0x2', '0x0', '0x0', '0x0', '0x4', '0x0', '0x0', '0x0']

This time, each number is represented by 4 elements, with the least significant byte first.

We create two arrays of shape 2x2, both of type signed integer with 32 bits (4 bytes), the first C-contiguous and the second F-contiguous.

In [6]:
x = np.array([[1,2],[3,4]],order='C',dtype=np.int32)
y = np.array([[1,2],[3,4]],order='F',dtype=np.int32)
print(x)
print(y)
[[1 2]
 [3 4]]
[[1 2]
 [3 4]]

We inspect the string representing the data behind the two arrays.

In [7]:
# Option 1
print(x.tobytes('A'))
print(y.tobytes('A'))
# Option 2 (more readable)
print([hex(el) for el in x.tobytes(order='A')])
print([hex(el) for el in y.tobytes(order='A')])
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00'
b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00'
['0x1', '0x0', '0x0', '0x0', '0x2', '0x0', '0x0', '0x0', '0x3', '0x0', '0x0', '0x0', '0x4', '0x0', '0x0', '0x0']
['0x1', '0x0', '0x0', '0x0', '0x3', '0x0', '0x0', '0x0', '0x2', '0x0', '0x0', '0x0', '0x4', '0x0', '0x0', '0x0']

The string looks like in the previous case.

We switch the sign of the elements of the two arrays.

In [8]:
x = -x
y = -y

We inspect the string representing the data behind the two arrays.

In [9]:
# Option 1
print(x.tobytes('A'))
print(y.tobytes('A'))
# Option 2 (more readable)
print([hex(el) for el in x.tobytes(order='A')])
print([hex(el) for el in y.tobytes(order='A')])
b'\xff\xff\xff\xff\xfe\xff\xff\xff\xfd\xff\xff\xff\xfc\xff\xff\xff'
b'\xff\xff\xff\xff\xfd\xff\xff\xff\xfe\xff\xff\xff\xfc\xff\xff\xff'
['0xff', '0xff', '0xff', '0xff', '0xfe', '0xff', '0xff', '0xff', '0xfd', '0xff', '0xff', '0xff', '0xfc', '0xff', '0xff', '0xff']
['0xff', '0xff', '0xff', '0xff', '0xfd', '0xff', '0xff', '0xff', '0xfe', '0xff', '0xff', '0xff', '0xfc', '0xff', '0xff', '0xff']

Can you tell what is the meaning of the characters in the string? With 32 bits, the largest number than can be represented is 2 to the power of 32 minus 1, which is 4294967295. In hexadecimal, this corresponds to FFFFFFFF. As the type is signed, we can only go from 0 to half of 4294967295 and from 0 to minus half of 4294967295. In particular, FFFFFFFF is the representation of -1, FFFFFFFE is the representation of -2, and so on.

In [10]:
z = np.array([[2**32/2,-2**32/2],[2**32/2+1,-2**32/2+1]],order='C',dtype=np.int32)
print(z)

# Option 1
print(z.tobytes('A'))
# Option 2 (more readable)
print([hex(el) for el in z.tobytes(order='A')])
[[-2147483648 -2147483648]
 [-2147483647 -2147483647]]
b'\x00\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x80'
['0x0', '0x0', '0x0', '0x80', '0x0', '0x0', '0x0', '0x80', '0x1', '0x0', '0x0', '0x80', '0x1', '0x0', '0x0', '0x80']

How many different numbers can you represent with a certain type? Consider, as an example, the type unsigned integer with 8 bits (1 byte). What is the largest number you can represent? What happens when you try to represent the numbers above it?

In [11]:
t = np.array([[255, 256], [257, 258]], order='C', dtype=np.uint8)
print(t)
[[255   0]
 [  1   2]]
In [12]:
import sys
sys.byteorder
Out[12]:
'little'

Exercise 2

Part 1

In [13]:
a = np.random.rand(5,5)
In [14]:
a
Out[14]:
array([[0.15126955, 0.39361423, 0.48981087, 0.35107628, 0.86359342],
       [0.39477557, 0.68215507, 0.37404898, 0.3571689 , 0.99782611],
       [0.38535697, 0.92979597, 0.43054749, 0.87675195, 0.15031662],
       [0.79519236, 0.88253785, 0.11405266, 0.80739847, 0.12253218],
       [0.10125282, 0.2111301 , 0.51900862, 0.63876047, 0.49688694]])
In [15]:
i1 = np.arange(5)
In [16]:
a[i1,i1]
Out[16]:
array([0.15126955, 0.68215507, 0.43054749, 0.80739847, 0.49688694])

Part 2

In [17]:
b = np.random.rand(10,5)
In [18]:
b
Out[18]:
array([[0.03174437, 0.32699488, 0.18329588, 0.87016361, 0.43021697],
       [0.12316312, 0.3304492 , 0.44731251, 0.78567626, 0.54101026],
       [0.29418133, 0.84621793, 0.93150255, 0.01514524, 0.39395155],
       [0.36236496, 0.69028573, 0.72414792, 0.70349112, 0.06883808],
       [0.10760388, 0.13809415, 0.20477398, 0.51350776, 0.65955981],
       [0.19913987, 0.33455484, 0.7999268 , 0.19630587, 0.49372358],
       [0.4452452 , 0.47890967, 0.96434673, 0.01849262, 0.81575671],
       [0.10944004, 0.42229476, 0.74494661, 0.752404  , 0.91277012],
       [0.53486817, 0.40048493, 0.86983496, 0.604855  , 0.70603707],
       [0.91860685, 0.32888574, 0.94190422, 0.73181855, 0.35435522]])
In [19]:
# For each column, index of the row with the value closest to 0.66.
np.argmax(-np.abs(b-0.66),axis=0) 
Out[19]:
array([8, 3, 3, 3, 4])
In [20]:
# For each row, index of the column with the value closest to 0.66.
np.argmax(-np.abs(b-0.66),axis=1) 
Out[20]:
array([3, 4, 1, 1, 4, 2, 4, 2, 4, 3])
In [21]:
i0=np.arange(10)
i1=np.argmax(-np.abs(b-0.66),axis=1)
b[i0,i1]
Out[21]:
array([0.87016361, 0.54101026, 0.84621793, 0.69028573, 0.65955981,
       0.7999268 , 0.81575671, 0.74494661, 0.70603707, 0.73181855])
In [22]:
i1=np.arange(5)
i0=np.argmax(-np.abs(b-0.66),axis=0) 
b[i0,i1]
Out[22]:
array([0.53486817, 0.69028573, 0.72414792, 0.70349112, 0.65955981])

Part 3

In [23]:
x = np.empty((12, 7, 5))
In [24]:
x[0,0,0]
Out[24]:
0.0
In [25]:
idx0 = np.zeros((2, 9)).astype(int)
idx1 = np.zeros((2, 1)).astype(int)
idx2 = np.zeros((1, 1)).astype(int)
In [26]:
x[idx0, idx1, idx2].shape
Out[26]:
(2, 9)

Additional example of fancy indexing

In [27]:
a = np.arange(100).reshape(10,10)
In [28]:
np.random.seed(10)
In [29]:
i0 = np.random.randint(0,10,(8,1,8))
i1 = np.random.randint(0,10,(2,8))
print(i0)
print(i1)
[[[9 4 0 1 9 0 1 8]]

 [[9 0 8 6 4 3 0 4]]

 [[6 8 1 8 4 1 3 6]]

 [[5 3 9 6 9 1 9 4]]

 [[2 6 7 8 8 9 2 0]]

 [[6 7 8 1 7 1 4 0]]

 [[8 5 4 7 8 8 2 6]]

 [[2 8 8 6 6 5 6 0]]]
[[0 6 9 1 8 9 1 2]
 [8 9 9 5 0 2 7 3]]
In [30]:
a[i0,i1]
Out[30]:
array([[[90, 46,  9, 11, 98,  9, 11, 82],
        [98, 49,  9, 15, 90,  2, 17, 83]],

       [[90,  6, 89, 61, 48, 39,  1, 42],
        [98,  9, 89, 65, 40, 32,  7, 43]],

       [[60, 86, 19, 81, 48, 19, 31, 62],
        [68, 89, 19, 85, 40, 12, 37, 63]],

       [[50, 36, 99, 61, 98, 19, 91, 42],
        [58, 39, 99, 65, 90, 12, 97, 43]],

       [[20, 66, 79, 81, 88, 99, 21,  2],
        [28, 69, 79, 85, 80, 92, 27,  3]],

       [[60, 76, 89, 11, 78, 19, 41,  2],
        [68, 79, 89, 15, 70, 12, 47,  3]],

       [[80, 56, 49, 71, 88, 89, 21, 62],
        [88, 59, 49, 75, 80, 82, 27, 63]],

       [[20, 86, 89, 61, 68, 59, 61,  2],
        [28, 89, 89, 65, 60, 52, 67,  3]]])
In [31]:
a[9,0]
Out[31]:
90
In [ ]: