# RoQNN
This is a general Quantum Neural Network library we developed as part of 
the contribution of ICLR 2022 submission "RoQNN: Noise-Aware Training for Robust Quantum Neural Networks".

Since the library is PyTorch-based, we name the library
as `torchquantum`.

## Features
- Easy construction of parameterized QNN model in PyTorch.
- Support **batch mode** inference and training on CPU/GPU.
- Support **both dynamic and static computation graph** for easy debugging.
- Support easy extraction of noise models and **insert noise** during training.
- Support easy **deployment on real quantum devices** such as IBMQ.


## Installation
Download all files to local `roqnn` folder
```bash
cd roqnn
pip install --editable .
```

## Usage
Construct a parameterized QNN model as simple as constructing a normal
pytorch NN model.
```python
import torch.nn as nn
import torch.nn.functional as F 
import torchquantum as tq
import torchquantum.functional as tqf

class QFCModel(nn.Module):
  def __init__(self):
    super().__init__()
    self.n_wires = 4
    self.q_device = tq.QuantumDevice(n_wires=self.n_wires)
    self.measure = tq.MeasureAll(tq.PauliZ)
    
    self.encoder_gates = [tqf.rx] * 4 + [tqf.ry] * 4 + \
                         [tqf.rz] * 4 + [tqf.rx] * 4
    self.rx0 = tq.RX(has_params=True, trainable=True)
    self.ry0 = tq.RY(has_params=True, trainable=True)
    self.rz0 = tq.RZ(has_params=True, trainable=True)
    self.crx0 = tq.CRX(has_params=True, trainable=True)

  def forward(self, x):
    bsz = x.shape[0]
    # down-sample the image
    x = F.avg_pool2d(x, 6).view(bsz, 16)
    
    # reset qubit states
    self.q_device.reset_states(bsz)
    
    # encode the classical image to quantum domain
    for k, gate in enumerate(self.encoder_gates):
      gate(self.q_device, wires=k % self.n_wires, params=x[:, k])
    
    # add some trainable gates (need to instantiate ahead of time)
    self.rx0(self.q_device, wires=0)
    self.ry0(self.q_device, wires=1)
    self.rz0(self.q_device, wires=3)
    self.crx0(self.q_device, wires=[0, 2])
    
    # add some more non-parameterized gates (add on-the-fly)
    tqf.hadamard(self.q_device, wires=3)
    tqf.sx(self.q_device, wires=2)
    tqf.cnot(self.q_device, wires=[3, 0])
    tqf.qubitunitary(self.q_device, wires=[1, 2], params=[[1, 0, 0, 0],
                                                           [0, 1, 0, 0],
                                                           [0, 0, 0, 1j],
                                                           [0, 0, -1j, 0]])
    
    # perform measurement to get expectations (back to classical domain)
    x = self.measure(self.q_device).reshape(bsz, 2, 2)
    
    # classification
    x = x.sum(-1).squeeze()
    x = F.log_softmax(x, dim=1)

    return x

```


## MNIST Example
Train a QNN model to perform MNIST task and deploy on the real IBM
Yorktown (ibmqx2) quantum computer as in [mnist_example.py](./mnist_example.py)
script:
```python
python mnist_example.py
```

## Files
| File      | Description |
| ----------- | ----------- |
| devices.py      | QuantumDevice class which stores the statevector |
| encoding.py   | Encoding layers to encode classical values to quantum domain |
| functional.py   | Quantum gate functions |
| operators.py   | Quantum gate classes |
| noise_model.py | Contains several noise injection methods: gate insertion, measurement/rotation angle perturbation, readout error insertion |
| layers.py   | Layer templates such as RandomLayer |
| measure.py   | Measurement of quantum states to get classical values |
| node.py   | Quantum block that contains encoder, parameterized quantum layers, and measurement |
| graph.py   | Quantum gate graph used in static mode |
| plugins/qiskit*   | Convertors and processors for easy deployment on IBMQ |
| examples/| More examples for training QML models |

## More Examples
The `examples/` folder contains more examples to train the QML
models. Example usage for QML models:
```python
# train the QNN for Fashion 4 classification with gate insertion (U3CU3 design space)
# noise factor = 1.5 and post-measurement quantization level as 4
python examples/train.py examples/configs/fashion/four0123/train/addnoise/bnormnolast/qiskitreadnoi/nothermal/santiago/u3cu3_0/n2b6/fac1.5/quant/l4/default.yml

# evaluate the circuit with torchquantum
python examples/eval.py examples/configs/fashion/four0123/eval/tq/act_quant/300_loadop.yml --run-dir=runs/fashion.four0123.train.addnoise.bnormnolast.qiskitreadnoi.nothermal.santiago.u3cu3_0.n2b6.fac1.quant.l3.default/

# evaluate the circuit with real IBMQ-Santiago quantum computer
python examples/eval.py examples/configs/fashion/four0123/eval/santiago/real/opt2/noancilla/act_quant/300_loadop_s18000.yml --run-dir=runs/fashion.four0123.train.addnoise.bnormnolast.qiskitreadnoi.nothermal.santiago.u3cu3_0.n2b6.fac1.quant.l3.default/
```

```python
# train the QNN for MNIST 10 classification with gate insertion (U3CU3 design space)
# noise factor = 0.5 and post-measurement quantization level as 5
python examples/train.py examples/configs/mnist/ten/train/addnoise/bnormnolast/qiskitreadnoi/nothermal/melbourne/u3cu3_0/n2b1/fac0.5/quant/l5/default.yml

# evaluate the circuit with torchquantum
python examples/eval.py examples/configs/mnist/ten/eval/tq/all.yml --run-dir=runs/mnist.ten.train.addnoise.bnormnolast.qiskitreadnoi.nothermal.melbourne.u3cu3_0.n2b1.fac0.5.quant.l5.default/

# evaluate the circuit with real IBMQ-Melbourne quantum computer
python examples/eval.py examples/configs/mnist/ten/eval/melbourne/real/opt2/noancilla/act_quant/300_loadop_s44600.yml --run-dir=runs/mnist.ten.train.addnoise.bnormnolast.qiskitreadnoi.nothermal.melbourne.u3cu3_0.n2b1.fac0.5.quant.l5.default/
```

```python
# train the baseline QNN for Fashion 2 classification (without post-measurement normalization, noise injection and quantization)
python examples/train.py examples/configs/fashion/two36/train/noaddnoise/nonorm/u3cu3_0/n2b2/default.yml
```

## Dependencies
- Python >= 3.7
- PyTorch >= 1.8.0
- configargparse >= 0.14
- Qiskit >= 0.24.1
- GPU model training requires NVIDIA GPUs

