# QuantumArray#

class QuantumArray(qtype, shape=0, qs=None, name=None)[source]#

This class allows the convenient management of multiple QuantumVariables of one type. As a subclass of numpy’s ndarray, the QuantumArray supports many convenient array manipulation methods. Similar to the numpy equivalent, creating a QuantumArray can be achieved by specifying a shape and a `qtype`:

```>>> import numpy as np
>>> from qrisp import QuantumArray, QuantumFloat
>>> qtype = QuantumFloat(5, -2)
>>> q_array = QuantumArray(qtype = qtype, shape = (2, 2, 2))
```

Note that `qtype` is not a type object but a QuantumVariable which serves as an “example”.

To retrieve the entries (i.e. QuantumVariables) from the QuantumArray, we simply index as with regular numpy arrays:

```>>> from qrisp import h
>>> qv = q_array[0,0,1]
>>> h(qv)
>>> print(q_array)
{OutcomeArray([[[0., 0.],
[0., 0.]],
[[0., 0.],
[0., 0.]]]): 0.5,
OutcomeArray([[[0.  , 0.25],
[0.  , 0.  ]],
[[0.  , 0.  ],
[0.  , 0.  ]]]): 0.5}
```

We see the value 0.25 in the second entry because we applied an H-gate onto the 0-th qubit of the QuantumVariable at position (0,0,1). Since the type of this array is a QuantumFloat, with exponent -2, the significance of this qubit is 0.25.

Note that the keys of the dictionary returned by the get_measurement method are no regular numpy arrays, as key objects need to be hashable. Instead, they are objects of an immutable subclass of np.ndarray called OutcomeArray, that supports hashing.

For QuantumArrays, many methods known from numpy arrays work here too:

```>>> q_array = q_array.reshape(2,4)
```

Not only do the ndarray methods work but also many other convenience functions from the numpy module:

```>>> q_array_swap = np.swapaxes(q_array, 0, 1)
>>> print(q_array_swap)
{OutcomeArray([[0., 0.],
[0., 0.],
[0., 0.],
[0., 0.]]): 0.5,
OutcomeArray([[0.  , 0.  ],
[0.25, 0.  ],
[0.  , 0.  ],
[0.  , 0.  ]]): 0.5}
```

To initiate the array, we use the `encode` method. Similar to QuantumVariables, we can also use the slicing operator, but this time non-trivial slices are possible as well:

```>>> q_array[1:,:] = 2*np.ones((1,4))
>>> print(q_array)
{OutcomeArray([[0., 0., 0., 0.],
[2., 2., 2., 2.]]): 0.5,
OutcomeArray([[0.  , 0.25, 0.  , 0.  ],
[2.  , 2.  , 2.  , 2.  ]]): 0.5}
```

The shape of a QuantumArray does not have to be specified at creation. We can either set it through the `set_shape` method or by initiating:

```>>> q_array_1 = QuantumArray(qtype = qtype)
>>> q_array_1.set_shape((2,2))
>>> print(q_array_1)
{OutcomeArray([[0., 0.],
[0., 0.]]): 1.0}
>>> q_array_2 = QuantumArray(qtype = qtype)
>>> q_array_2[:] = np.eye(2)
>>> print(q_array_2)
{OutcomeArray([[1., 0.],
[0., 1.]]): 1.0}
```

Quantum indexing

QuantumArrays can be dereferenced by QuantumFloats. This returns a Quantum Environments in which the corresponding entry is avaliable as a QuantumVariable.

```from qrisp import QuantumBool, QuantumArray, QuantumFloat, h, multi_measurement

q_array = QuantumArray(QuantumBool(), shape = (4,4))
index_0 = QuantumFloat(2)
index_1 = QuantumFloat(2)

index_0[:] = 2
index_1[:] = 1

h(index_0)

with q_array[index_0, index_1] as entry:
entry.flip()
```
```>>> print(multi_measurement([index_0, index_1, q_array]))
{(2, 1, OutcomeArray([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 0., 0.]])): 0.5,
(3, 1, OutcomeArray([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 1., 0., 0.]])): 0.5}
```

Note

This only works for arrays which have a size of an integer power of 2.

Matrix multiplication

For QuantumArrays with `qtype` QuantumFloat, matrix multiplication is available.

```>>> q_array_1 = QuantumArray(qtype)
>>> q_array_2 = QuantumArray(qtype)
>>> q_array_1[:] = 2*np.eye(2)
>>> q_array_2[:] = [[1,2],[3,4]]
>>> print(q_array_1 @ q_array_2)
{OutcomeArray([[2., 4.],
[6., 0.]]): 1.0}
```

Note

By default, the output matrix will have the same `qtype` as the first input matrix. Here, the `qtype` is a QuantumFloat with 5 mantissa bits and exponent -2, implying that the result 8 yields overflow. Since qrisps unsigend arithmetic is modular, we get a 0.

It is also possible to multiply classical and quantum matrices

```>>> q_array = QuantumArray(qtype)
>>> q_array[:] = 3*np.eye(2)
>>> cl_array = np.array([[1,2],[3,4]])
>>> print(q_array @ cl_array)
{OutcomeArray([[3., 6.],
[1., 4.]]): 1.0}
```

# Methods#

 `QuantumArray.delete`([verify]) Performs the `delete` method on all QuantumVariables in this array. `QuantumArray.get_measurement`([backend, ...]) Method for acquiring measurement results for the given array. `QuantumArray.most_likely`(**kwargs) Performs a measurement and returns the most likely outcome. `QuantumArray.duplicate`([init, qs]) This method returns a fresh QuantumArray, with equal `qtype` and shape. Method to specify a shape for arrays which have been created without one.

## De/Encoding states#

 `QuantumArray.decoder`(code_int) The decoder method specifies how a QuantumArray turns the outcomes of measurements into human-readable values. `QuantumArray.encoder`(encoding_array) The encoder reverses the decoder, it turns arrays into integers based on the encoder of the `qtype` of this array. `QuantumArray.encode`(encoding_array) The encode method allows to quickly bring a QuantumArray in a desired computational basis state. `QuantumArray.init_state`(tuple_list) Method to initiate arbitrary quantum states in this array. Performs the `init_from` method on all containing QuantumVariables based on their equivalent in the QuantumArray other.