### Installation of Packages

```
!pip install qiskit
```

    Collecting qiskit
      Downloading https://files.pythonhosted.org/packages/96/6b/1c1a5ed225ac2e42ebc07df49e9c1ce0d3221c6d0744e7d6d5983ff0e69e/qiskit-0.23.6.tar.gz
    Collecting qiskit-terra==0.16.4
      Downloading https://files.pythonhosted.org/packages/56/e2/c256863ec8fab2162bf17d7e9c8564186a4636b4f24b5ea5d38a014b809e/qiskit_terra-0.16.4-cp36-cp36m-manylinux2010_x86_64.whl (8.5MB)
         |████████████████████████████████| 8.5MB 3.7MB/s
    Collecting qiskit-aer==0.7.5
      Downloading https://files.pythonhosted.org/packages/0e/4e/cf4493e8d8a7785b945b483429aa4b7960e30576eb8f3b24444991781460/qiskit_aer-0.7.5-cp36-cp36m-manylinux2010_x86_64.whl (17.6MB)
         |████████████████████████████████| 17.6MB 248kB/s
    Collecting qiskit-ibmq-provider==0.11.1
      Downloading https://files.pythonhosted.org/packages/32/b9/f99bec4fdc4dec234d8a85a8da378750441a203c614be35353f5e8738316/qiskit_ibmq_provider-0.11.1-py3-none-any.whl (195kB)
         |████████████████████████████████| 204kB 45.0MB/s
    Collecting qiskit-ignis==0.5.2
      Downloading https://files.pythonhosted.org/packages/b2/1d/d5df8cf48cf20a26deb6e0535f020cd2ad625cb27457abb0ab39c2104e38/qiskit_ignis-0.5.2-py3-none-any.whl (203kB)
         |████████████████████████████████| 204kB 47.6MB/s
    Collecting qiskit-aqua==0.8.2
      Downloading https://files.pythonhosted.org/packages/0e/82/528741676960a7fd1c75151639601826118f2729ad53034fa3b39290c056/qiskit_aqua-0.8.2-py3-none-any.whl (2.1MB)
         |████████████████████████████████| 2.1MB 37.3MB/s
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (0.3.3)
    Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (1.19.5)
    Collecting ply>=3.10
      Downloading https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl (49kB)
         |████████████████████████████████| 51kB 5.7MB/s
    Collecting retworkx>=0.5.0
      Downloading https://files.pythonhosted.org/packages/b7/21/5d4498cedb23927a31173176975e2162e0d6a2b80846ef254dd5500e10da/retworkx-0.7.2-cp36-cp36m-manylinux2010_x86_64.whl (1.3MB)
         |████████████████████████████████| 1.3MB 38.0MB/s
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (5.4.8)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (2.8.1)
    Requirement already satisfied: scipy>=1.4 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (1.4.1)
    Requirement already satisfied: networkx>=2.2 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (2.5)
    Collecting sympy>=1.3
      Downloading https://files.pythonhosted.org/packages/ff/69/b16fc81b939d3efdd0b552f2e3e54f7fa1423d0c320cced2e69e675dde26/sympy-1.7.1-py3-none-any.whl (5.9MB)
         |████████████████████████████████| 5.9MB 33.5MB/s
    Collecting fastjsonschema>=2.10
      Downloading https://files.pythonhosted.org/packages/89/1c/8be51fa42aadc1c1611a52b866e1a5a1032a504f24789cf140b4e6d7c940/fastjsonschema-2.15.0-py3-none-any.whl
    Collecting python-constraint>=1.4
      Downloading https://files.pythonhosted.org/packages/37/8b/5f1bc2734ca611943e1d6733ee244238679f6410a10cd45ede55a61a8402/python-constraint-1.4.0.tar.bz2
    Collecting contextvars>=2.4; python_version < "3.7"
      Downloading https://files.pythonhosted.org/packages/83/96/55b82d9f13763be9d672622e1b8106c85acb83edd7cc2fa5bc67cd9877e9/contextvars-2.4.tar.gz
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.16.4->qiskit) (2.6.0)
    Collecting pybind11>=2.4
      Downloading https://files.pythonhosted.org/packages/8d/43/7339dbabbc2793718d59703aace4166f53c29ee1c202f6ff5bf8a26c4d91/pybind11-2.6.2-py2.py3-none-any.whl (191kB)
         |████████████████████████████████| 194kB 46.1MB/s
    Requirement already satisfied: cython>=0.27.1 in /usr/local/lib/python3.6/dist-packages (from qiskit-aer==0.7.5->qiskit) (0.29.21)
    Collecting websockets>=8
      Downloading https://files.pythonhosted.org/packages/bb/d9/856af84843912e2853b1b6e898ac8b802989fcf9ecf8e8445a1da263bf3b/websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl (78kB)
         |████████████████████████████████| 81kB 8.2MB/s
    Collecting requests-ntlm>=1.1.0
      Downloading https://files.pythonhosted.org/packages/03/4b/8b9a1afde8072c4d5710d9fa91433d504325821b038e00237dc8d6d833dc/requests_ntlm-1.1.0-py2.py3-none-any.whl
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from qiskit-ibmq-provider==0.11.1->qiskit) (1.24.3)
    Requirement already satisfied: nest-asyncio!=1.1.0,>=1.0.0 in /usr/local/lib/python3.6/dist-packages (from qiskit-ibmq-provider==0.11.1->qiskit) (1.5.1)
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.6/dist-packages (from qiskit-ibmq-provider==0.11.1->qiskit) (2.23.0)
    Requirement already satisfied: scikit-learn>=0.17 in /usr/local/lib/python3.6/dist-packages (from qiskit-ignis==0.5.2->qiskit) (0.22.2.post1)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.6/dist-packages (from qiskit-ignis==0.5.2->qiskit) (53.0.0)
    Collecting yfinance
      Downloading https://files.pythonhosted.org/packages/7a/e8/b9d7104d3a4bf39924799067592d9e59119fcfc900a425a12e80a3123ec8/yfinance-0.1.55.tar.gz
    Requirement already satisfied: pandas in /usr/local/lib/python3.6/dist-packages (from qiskit-aqua==0.8.2->qiskit) (1.1.5)
    Collecting dlx
      Downloading https://files.pythonhosted.org/packages/54/c0/b8fb5bb727e983b6f5251433ef941b48f38c65bb0bd6ec509e9185bcd406/dlx-1.0.4.tar.gz
    Requirement already satisfied: fastdtw in /usr/local/lib/python3.6/dist-packages (from qiskit-aqua==0.8.2->qiskit) (0.3.4)
    Collecting docplex==2.15.194
      Downloading https://files.pythonhosted.org/packages/9b/76/ebf5b75a25c4357c9a4758cae7fea778dd7f282b83c2f22e150dc7e3c852/docplex-2.15.194.tar.gz (582kB)
         |████████████████████████████████| 583kB 37.8MB/s
    Collecting quandl
      Downloading https://files.pythonhosted.org/packages/c2/58/9f0e69d836045e3865d263e9ed49f42b23a58526fdabb30f74c430baee3f/Quandl-3.6.0-py2.py3-none-any.whl
    Requirement already satisfied: dataclasses; python_version < "3.7" in /usr/local/lib/python3.6/dist-packages (from qiskit-aqua==0.8.2->qiskit) (0.8)
    Requirement already satisfied: h5py in /usr/local/lib/python3.6/dist-packages (from qiskit-aqua==0.8.2->qiskit) (2.10.0)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.6/dist-packages (from python-dateutil>=2.8.0->qiskit-terra==0.16.4->qiskit) (1.15.0)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.6/dist-packages (from networkx>=2.2->qiskit-terra==0.16.4->qiskit) (4.4.2)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.6/dist-packages (from sympy>=1.3->qiskit-terra==0.16.4->qiskit) (1.1.0)
    Collecting immutables>=0.9
      Downloading https://files.pythonhosted.org/packages/4a/52/e64a14a99c509cbdfe0405e9f076aef0331cb9548a3efa1d5bacd524978a/immutables-0.15-cp36-cp36m-manylinux1_x86_64.whl (100kB)
         |████████████████████████████████| 102kB 10.4MB/s
    Collecting ntlm-auth>=1.0.2
      Downloading https://files.pythonhosted.org/packages/ff/84/97c550164b54942b0e908c31ef09d9469f3ba4cd7332a671e2125732f63b/ntlm_auth-1.5.0-py2.py3-none-any.whl
    Collecting cryptography>=1.3
      Downloading https://files.pythonhosted.org/packages/f8/1f/acde6ff69864c5e78b56488e3afd93c1ccc8c2651186e2a5f93d93f64859/cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl (3.2MB)
         |████████████████████████████████| 3.2MB 45.1MB/s
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.11.1->qiskit) (2020.12.5)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.11.1->qiskit) (2.10)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.11.1->qiskit) (3.0.4)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.6/dist-packages (from scikit-learn>=0.17->qiskit-ignis==0.5.2->qiskit) (1.0.0)
    Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.6/dist-packages (from yfinance->qiskit-aqua==0.8.2->qiskit) (0.0.9)
    Collecting lxml>=4.5.1
      Downloading https://files.pythonhosted.org/packages/bd/78/56a7c88a57d0d14945472535d0df9fb4bbad7d34ede658ec7961635c790e/lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl (5.5MB)
         |████████████████████████████████| 5.5MB 39.2MB/s
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas->qiskit-aqua==0.8.2->qiskit) (2018.9)
    Collecting inflection>=0.3.1
      Downloading https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl
    Requirement already satisfied: more-itertools in /usr/local/lib/python3.6/dist-packages (from quandl->qiskit-aqua==0.8.2->qiskit) (8.7.0)
    Requirement already satisfied: cffi>=1.12 in /usr/local/lib/python3.6/dist-packages (from cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.11.1->qiskit) (1.14.4)
    Requirement already satisfied: pycparser in /usr/local/lib/python3.6/dist-packages (from cffi>=1.12->cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.11.1->qiskit) (2.20)
    Building wheels for collected packages: qiskit, python-constraint, contextvars, yfinance, dlx, docplex
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.23.6-cp36-none-any.whl size=2883 sha256=dd9090c0f69a814ebedf101c3157e530864bd24c35ba34cc8dd5bd69371c7328
      Stored in directory: /root/.cache/pip/wheels/b1/5e/2a/a9afb84cb4e8a08ea994982aaf040b2c668ef5fee81430f1f5
      Building wheel for python-constraint (setup.py) ... done
      Created wheel for python-constraint: filename=python_constraint-1.4.0-py2.py3-none-any.whl size=24079 sha256=c565d75fe7f26447658c26944fcbd958ab10ef7af10767f0d5bac60876344012
      Stored in directory: /root/.cache/pip/wheels/34/31/15/7b070b25d0a549d20ce2e9fe6d727471c2c61ef904720fd40c
      Building wheel for contextvars (setup.py) ... done
      Created wheel for contextvars: filename=contextvars-2.4-cp36-none-any.whl size=7667 sha256=6c7e59261ca604254700f707e75a0af0fe3eced2d8a243e7997c8ab81bd2841d
      Stored in directory: /root/.cache/pip/wheels/a5/7d/68/1ebae2668bda2228686e3c1cf16f2c2384cea6e9334ad5f6de
      Building wheel for yfinance (setup.py) ... done
      Created wheel for yfinance: filename=yfinance-0.1.55-py2.py3-none-any.whl size=22616 sha256=7db72a24a2942b5f75b593fd124688e40b737834a5fddb421d57e64e35e2c9b5
      Stored in directory: /root/.cache/pip/wheels/04/98/cc/2702a4242d60bdc14f48b4557c427ded1fe92aedf257d4565c
      Building wheel for dlx (setup.py) ... done
      Created wheel for dlx: filename=dlx-1.0.4-cp36-none-any.whl size=5711 sha256=464b2c56da4f7ed14ee756a61bed40354754a59fe7c163b3b222a851f5e1a0b3
      Stored in directory: /root/.cache/pip/wheels/bb/ba/15/fdd0deb104df3254912998150ba9245668db06b00af5912d1a
      Building wheel for docplex (setup.py) ... done
      Created wheel for docplex: filename=docplex-2.15.194-cp36-none-any.whl size=645116 sha256=ac5c34368ca40a09bb75c7bfc473582275078c91d8f78bb3c257bd809a6c7088
      Stored in directory: /root/.cache/pip/wheels/ce/fa/5d/8f4d5fed1b8edb72e453cb2ac6fb75c6776b49d7174eb70457
    Successfully built qiskit python-constraint contextvars yfinance dlx docplex
    Installing collected packages: ply, retworkx, sympy, fastjsonschema, python-constraint, immutables, contextvars, qiskit-terra, pybind11, qiskit-aer, websockets, ntlm-auth, cryptography, requests-ntlm, qiskit-ibmq-provider, qiskit-ignis, lxml, yfinance, dlx, docplex, inflection, quandl, qiskit-aqua, qiskit
      Found existing installation: sympy 1.1.1
        Uninstalling sympy-1.1.1:
          Successfully uninstalled sympy-1.1.1
      Found existing installation: lxml 4.2.6
        Uninstalling lxml-4.2.6:
          Successfully uninstalled lxml-4.2.6
    Successfully installed contextvars-2.4 cryptography-3.4.6 dlx-1.0.4 docplex-2.15.194 fastjsonschema-2.15.0 immutables-0.15 inflection-0.5.1 lxml-4.6.2 ntlm-auth-1.5.0 ply-3.11 pybind11-2.6.2 python-constraint-1.4.0 qiskit-0.23.6 qiskit-aer-0.7.5 qiskit-aqua-0.8.2 qiskit-ibmq-provider-0.11.1 qiskit-ignis-0.5.2 qiskit-terra-0.16.4 quandl-3.6.0 requests-ntlm-1.1.0 retworkx-0.7.2 sympy-1.7.1 websockets-8.1 yfinance-0.1.55

```
!pip install pylatexenc
```

    Collecting pylatexenc
      Downloading https://files.pythonhosted.org/packages/40/d0/cad5f06137b8e1924a9b2c5b5ca07b75c6272bdfb14cb9df904d6fc5c05f/pylatexenc-2.8.tar.gz (158kB)
         |████████████████████████████████| 163kB 4.1MB/s
    Building wheels for collected packages: pylatexenc
      Building wheel for pylatexenc (setup.py) ... done
      Created wheel for pylatexenc: filename=pylatexenc-2.8-cp36-none-any.whl size=131377 sha256=8df0028b5c7dfd47df61a886576bc2a28baf9a7b2eebe7cfa435f844315e6ac3
      Stored in directory: /root/.cache/pip/wheels/88/23/90/4cf49f59c1eb67af40a2b3a580536bb5f91d40bc0d4cc8e035
    Successfully built pylatexenc
    Installing collected packages: pylatexenc
    Successfully installed pylatexenc-2.8

## **Task:1- SWAP Test**

```
from qiskit import AncillaRegister,QuantumRegister, ClassicalRegister, QuantumCircuit, execute, Aer
```

```
q= QuantumRegister(2)
c= ClassicalRegister(2)
qc= QuantumCircuit(q,c)

#q= [0,1]
qc.h(q[1])
qc.cnot(q[0],q[1])
qc.cnot(q[1],q[0])
qc.cnot(q[0],q[1])
job = execute(qc,Aer.get_backend('qasm_simulator'),shots=1024)

counts = job.result().get_counts(qc)
print(counts)
qc.measure(q,c)

qc.draw()
```

    {'00': 1024}

                ┌───┐     ┌─┐
q3_0: ───────■──┤ X ├──■──┤M├───
      ┌───┐┌─┴─┐└─┬─┘┌─┴─┐└╥┘┌─┐
q3_1: ┤ H ├┤ X ├──■──┤ X ├─╫─┤M├
      └───┘└───┘     └───┘ ║ └╥┘
c1: 2/═════════════════════╩══╩═
                           0  1

```
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from numpy import pi

qreg = QuantumRegister(2, 'q')
creg = ClassicalRegister(2, 'c')
qc1 = QuantumCircuit(qreg, creg)

qc1.x(qreg[0])

qc1.swap(qreg[0], qreg[1])
qc1.measure(qreg[0], creg[0])
qc1.measure(qreg[1], creg[1])
qc1.draw()
```

     ┌───┐   ┌─┐
q_0: ┤ X ├─X─┤M├───
     └───┘ │ └╥┘┌─┐
q_1: ──────X──╫─┤M├
              ║ └╥┘
c: 2/═════════╩══╩═
              0  1

```
import numpy as np
from qiskit.visualization import plot_state_qsphere

si= QuantumRegister(1,'si')
phi= QuantumRegister(1,'phi')
a = AncillaRegister(1,'ancilla')
c= ClassicalRegister(1)
qc2= QuantumCircuit(si,phi,a,c)

P= 5  #representing the number of times the algorithm will be executed
for j in range(1,P):
  qc2.h(a[0])       #Apply a Hadamard gate to the ancilla qubit
  #iterating over each pair of qubits in the two registers
  for i in [si,phi]:
    qc2.cswap(a[0],si[0],phi[0])
    qc2.h(a[0])       #Apply a Hadamard gate to the ancilla qubit
    qc2.z(a[0])
    M= qc2.measure(a[0],c[0])
  print(M)
qc2.measure(si[0],c[0])
qc2.measure(phi[0],c[0])
qc2.draw()

#s= 1 - (2/P*np.sum())

```

    <qiskit.circuit.instructionset.InstructionSet object at 0x7f0088f59550>
    <qiskit.circuit.instructionset.InstructionSet object at 0x7f0088f589e8>
    <qiskit.circuit.instructionset.InstructionSet object at 0x7f0088f58be0>
    <qiskit.circuit.instructionset.InstructionSet object at 0x7f0088d917b8>

                                                                             »
     si_0: ──────X───────────────X────────────────────X───────────────X──────»
                 │               │                    │               │      »
    phi_0: ──────X───────────────X────────────────────X───────────────X──────»
           ┌───┐ │ ┌───┐┌───┐┌─┐ │ ┌───┐┌───┐┌─┐┌───┐ │ ┌───┐┌───┐┌─┐ │ ┌───┐»
ancilla_0: ┤ H ├─■─┤ H ├┤ Z ├┤M├─■─┤ H ├┤ Z ├┤M├┤ H ├─■─┤ H ├┤ Z ├┤M├─■─┤ H ├»
           └───┘   └───┘└───┘└╥┘   └───┘└───┘└╥┘└───┘   └───┘└───┘└╥┘   └───┘»
    c28: 1/═══════════════════╩═══════════════╩════════════════════╩═════════»
                              0               0                    0         »
«                                                                             »
«     si_0: ──────────────X───────────────X────────────────────X──────────────»
«                         │               │                    │              »
«    phi_0: ──────────────X───────────────X────────────────────X──────────────»
«           ┌───┐┌─┐┌───┐ │ ┌───┐┌───┐┌─┐ │ ┌───┐┌───┐┌─┐┌───┐ │ ┌───┐┌───┐┌─┐»
«ancilla_0: ┤ Z ├┤M├┤ H ├─■─┤ H ├┤ Z ├┤M├─■─┤ H ├┤ Z ├┤M├┤ H ├─■─┤ H ├┤ Z ├┤M├»
«           └───┘└╥┘└───┘   └───┘└───┘└╥┘   └───┘└───┘└╥┘└───┘   └───┘└───┘└╥┘»
«    c28: 1/══════╩════════════════════╩═══════════════╩════════════════════╩═»
«                 0                    0               0                    0 »
«                           ┌─┐
«     si_0: ─X──────────────┤M├───
«            │              └╥┘┌─┐
«    phi_0: ─X───────────────╫─┤M├
«            │ ┌───┐┌───┐┌─┐ ║ └╥┘
«ancilla_0: ─■─┤ H ├┤ Z ├┤M├─╫──╫─
«              └───┘└───┘└╥┘ ║  ║
«    c28: 1/══════════════╩══╩══╩═
«                         0  0  0

```
#Representing Swap test output on BlockSphere
job = execute(qc2,Aer.get_backend('statevector_simulator'),shots=1024)
final_state = job.result().get_statevector(qc2)
plot_state_qsphere(final_state)
```

```
#Calculate matrix of swap Test
job = execute(qc2, Aer.get_backend('statevector_simulator'),shots=1,optimization_level=0)
current_unitary = job.result().get_statevector(qc2)
for row in current_unitary:
  print(row)
```

    (1-3.6739403974420584e-16j)
    0j
    0j
    0j
    -0j
    (-0+0j)
    (-0+0j)
    (-0+0j)

# **Task:2-Bit-flip code and the sign-flip code**

```
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from numpy import pi

qreg_q = QuantumRegister(2, 'q')
creg_c = ClassicalRegister(2, 'c')
qc2 = QuantumCircuit(qreg_q, creg_c)

qc2.reset(qreg_q[0])
qc2.reset(qreg_q[1])
qc2.h(qreg_q[0])
qc2.cx(qreg_q[0], qreg_q[1])
qc2.z(qreg_q[0])
qc2.measure(qreg_q[0], creg_c[0])
qc2.measure(qreg_q[1], creg_c[1])
qc2.draw()
```

          ┌───┐     ┌───┐┌─┐
q_0: ─|0>─┤ H ├──■──┤ Z ├┤M├
          └───┘┌─┴─┐└┬─┬┘└╥┘
q_1: ─|0>──────┤ X ├─┤M├──╫─
               └───┘ └╥┘  ║
c: 2/═════════════════╩═══╩═
                      1   0

```

```


## Large Deviations for Volatility Option in rough volatility
### Authors: Chloe Lacombe, Aitor Muguruza and Henry Stone

### In this notebook we provide the code to compute the rate function in the Mixed rough Bergomi model

The mixed rough Bergomi model is given in terms of log stock price process $X$ and instantaneous variance process $v^{(\gamma,\nu)}$ as
 \begin{equation}\label{eq:mixedvarianceProcess}
\begin{array}{rll}
X_t & = \displaystyle - \frac{1}{2} \int_0^t v_s^{(\gamma,\nu)} d s + \int_0^t \sqrt{ v_s^{(\gamma,\nu)} } d B_s,
 \quad &  X_0 = 0 , \\v_t^{(\gamma,\nu)}
 &= v_0 \sum_{i=1}^n \gamma_i \exp\left( \frac{\nu_i}{\eta}Z_t - \frac{\nu_i^2}{2}t^{2\alpha +1}\right) ,\quad & v_0>0
 \end{array}
\end{equation}
where  $\gamma:=(\gamma_1,...,\gamma_n)\in[0,1]^n$ such that $\sum_{i=1}^n \gamma_i =1$ and $\nu:=(\nu_1,...,\nu_n)\in\mathbb{R}^n$, such that $ 0<\nu_1<...<\nu_n$. \begin{equation}\label{eq:SDEZ}
Z_t := \int_0^t K_\alpha(s,t)d W_s,
\qquad\text{for any }t \in\mathcal{T}\subset\mathbb{R}_+,
\end{equation}
where $\alpha \in \left(-\frac{1}{2},0\right)$, $W$ a standard Brownian motion,
and where the kernel
$K_{\alpha}:\mathbb{R}_+\times\mathbb{R}_+ \to \mathbb{R}_+$ reads
\begin{equation}\label{eq:K}
K_{\alpha}(s,t) := \eta \sqrt{2\alpha + 1}(t-s)^{\alpha},
\qquad \text{for all } 0\leq s<t,
\end{equation}
for some strictly positive constant $\eta$.

## Load relevant libraries

```python
import numpy as np
from matplotlib.pyplot import plot
from scipy.optimize import minimize, brute, fmin_l_bfgs_b
from scipy.integrate import quad, quadrature, fixed_quad
import warnings
warnings.simplefilter("ignore", DeprecationWarning)
warnings.simplefilter("ignore", Warning)
import time
import functools
from scipy import special as special
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
```

```python
%reload_ext cython
```

## Main Code in Cython for computational performance

```cython
%%cython
from cython.parallel import prange

from libc.math cimport exp, sqrt, pow, log, erf, fmax
cimport cython
cimport numpy as cnp
import numpy as np
import scipy
from scipy import special
from scipy import integrate
cdef int c_degree=80
cdef double[:] c_x,c_w
c_x, c_w = np.polynomial.legendre.leggauss(c_degree)
cdef double[:] c_xaux=np.ones(c_degree)
for i in range(c_degree):
    c_xaux[i]=0.5*(1.0+c_x[i])
@cython.cdivision(True)
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cdef double PolySquared(double s,double coef1, double[:] coef, int n, double H) nogil:
    cdef double suma=coef1
    cdef int i=0
    for i in range(1,n):
        suma+=coef[i]*pow(s,i)
    return suma*suma

@cython.cdivision(True)
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cdef class RateFunction:

    cdef double[:] Hypergeommetric
    cdef double[:] etaSqrtTwoH
    cdef double[:] gamma
    cdef double H
    cdef double alpha
    cdef int n
    cdef int m
    def __init__(self,double alpha,int n,double [:] eta,double[:] gamma):
        self.alpha=alpha
        cdef double H=alpha+0.5
        self.Hypergeommetric = np.ones(n)
        self.m=len(eta)
        self.etaSqrtTwoH = np.ones(self.m)
        self.n=n

        for k in range(self.m):
            self.etaSqrtTwoH[k]=eta[k]*sqrt(2*H)
        self.H=H
        self.gamma=gamma
        for k in range(n):
            self.Hypergeommetric[k]=special.hyp2f1(k+1.0,0.5-H,k+2.0,1.0)/(k+1.0)
    cpdef double IV(self,double coef1,double[:] coef,double y):
        cdef double aa=0
        cdef double IV=0
        cdef double aux=0
        cdef int k=0,i=0,j=0,u=0;
        with nogil:
            for j in prange(c_degree):
                aa=coef1*pow(c_xaux[j],0.5+self.H)*self.Hypergeommetric[0]
                for k in range(1,self.n):
                    aa+=coef[k]*pow(c_xaux[j],k+0.5+self.H)*self.Hypergeommetric[k]
                aux=0.0
                for u in range(self.m):
                    aux+=self.gamma[u]*exp(self.etaSqrtTwoH[u]*aa)
                IV+=aux*c_w[j]
            IV*=0.5
        return (exp(y)-IV)*(exp(y)-IV)

    cpdef cnp.ndarray[cnp.float64_t,ndim=1] IVPrime(self,double coef1,double[:] coef,double y):
        cdef double aa=0
        cdef double IV=0,IVPrime=0.0
        cdef double aux=0,auxPrime=0;
        cdef int k=0,i=0,j=0,u=0;
        with nogil:
            for j in prange(c_degree):
                aa=coef1*pow(c_xaux[j],0.5+self.H)*self.Hypergeommetric[0]
                for k in range(1,self.n):
                    aa+=coef[k]*pow(c_xaux[j],k+0.5+self.H)*self.Hypergeommetric[k]
                aux=0.0
                auxPrime=0.0
                for u in range(self.m):
                    aux+=self.gamma[u]*exp(self.etaSqrtTwoH[u]*aa)
                    auxPrime+=self.gamma[u]*exp(self.etaSqrtTwoH[u]*aa)*self.etaSqrtTwoH[u]*pow(c_xaux[j],0.5+self.H)*self.Hypergeommetric[0]
                IV+=aux*c_w[j]
                IVPrime+=auxPrime*c_w[j]
            IV*=0.5
            IVPrime*=0.5

        return np.array([-2*(exp(y)-IV)*IVPrime])

    cpdef IV2(self,double coef1,double[:] coef,double y):
        cdef double aa=0
        cdef double IV=0,aux=0
        cdef int k,i,j,u
        for j in range(c_degree):
            aa=coef1*pow(c_xaux[j],0.0+0.5+self.H)*self.Hypergeommetric[0]
            for k in range(1,self.n):
                aa+=coef[k]*pow(c_xaux[j],k+0.5+self.H)*self.Hypergeommetric[k]
            aux=0.0
            for u in range(self.m):
                aux+=self.gamma[u]*exp(self.etaSqrtTwoH[u]*aa)

            IV+=aux*c_w[j]
        IV*=0.5
        return IV
    cpdef compute(self,double[:] coef,double y,Disp=False):
        # coef 0 to n-1 are the polynomial coefficients
        # coef n represents \log(y-sigma0)=coef[n]=> y>sigma0

        #condition IV=y
        I=scipy.optimize.minimize(self.IV,0.0,args=(coef,y),jac=self.IVPrime,tol=1e-14)
        cdef double coefaux=I.x
        cdef double result=0

        if Disp==True:
            print("e^y=",exp(y)," IV=",self.IV2(coefaux,coef,y))
        cdef int j=0
        with nogil:
            for j in prange(c_degree):
                result+=PolySquared(c_xaux[j],coefaux, coef,self.n,self.H)*c_w[j]

        return 0.25*result
```

    warning: C:\Users\localadmin\.ipython\cython\_cython_magic_70c3ca2f3ccc28cf7294028c7f4b6a13.pyx:15:27: Index should be typed for more efficient access

# Check that the contraint $y=RV(x)(1)$ is being satisfied

```python
n=5
H=0.1
alpha=H-0.5
x0=np.zeros(n)
y=1.5
a=RateFunction(alpha,n,eta,gamma)
a.compute(x0,y,True)
```

    e^y= 4.4816890703380645  IV= 4.4816890703380645

    1.9004108663280204

# Let us first analyse the Effect of H in the Rate Function and Implied volatility smiles

```python
n=5
Hs=[0.05,0.1,0.2,0.3,0.4,0.5]
ys=np.linspace(-2.0,2.0,24)
rateFunc=np.zeros([len(Hs),len(ys)])
eta=np.array([1.5],dtype=np.float)
gamma=np.array([1.0],dtype=np.float)
i=0
for H in Hs:
    a=RateFunction(H-0.5,n,eta,gamma)
    j=0
    for y in ys:
        I=minimize(a.compute,x0=x0,method='BFGS',args=(y),tol=1e-7)
        rateFunc[i,j]=(I.fun)
        j+=1
    i+=1
i=0
```

```python
plt.figure(figsize=(14,5))
plt.subplot(1,2,1)
i=0
for H in Hs:
    plt.plot(ys,rateFunc[i,:],label="$H=$%1.2f"%H)
    i+=1
#plt.legend(fontsize=13)
plt.title("$\hat{\Lambda}^v(y)$",fontsize=20)
plt.xlabel("Log-moneyness",fontsize=14)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.subplot(1,2,2)
i=0
for H in Hs:
    plt.plot(0,100*np.sqrt(2*H)*(np.sum(eta*gamma))/(H+0.5)/np.sqrt(2*H+2),'*k',label="Asymptotic by AGM" if i == 0 else "")
    plt.plot(ys,100*np.abs(ys)/np.sqrt(2*rateFunc[i,:]),label="$H=$%1.2f"%H)
    i+=1
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.,fontsize=13)
plt.title("$\\sigma_{BS}(y)$ with $\\nu=%1.2f$"%eta,fontsize=20)
plt.xlabel("Log-moneyness",fontsize=14)
plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter() )
#plt.gca().xaxis.set_major_formatter(mtick.PercentFormatter() )
plt.tick_params(axis='both', which='major', labelsize=15)
plt.tight_layout()
plt.savefig('roughBergomiSmiles.png', bbox_inches = "tight",dpi=300)
plt.show()
i=0
for H in Hs:
    #plt.plot(0,100*np.sqrt(2*H)*(np.sum(eta*gamma))/(H+0.5)/np.sqrt(2*H+2),'*k',label="Asymptotic by AGM" if i == 0 else "")
    plt.plot(100*np.sqrt(0.04*np.exp(ys)),100*np.abs(ys)/np.sqrt(2*rateFunc[i,:]),label="$H=$%1.2f"%H)
    i+=1
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.,fontsize=13)
plt.title("$\\sigma_{BS}(y)$",fontsize=20)
plt.xlabel("Strike in volatility",fontsize=14)
plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter() )
plt.gca().xaxis.set_major_formatter(mtick.PercentFormatter() )
#plt.gca().xaxis.set_major_formatter(mtick.PercentFormatter() )
plt.tick_params(axis='both', which='major', labelsize=15)

plt.show()
```

## We observe linear trends in the implied volatilities when considering the log space, this of course changes when one considers the effective strike

# Effect of $\sum\gamma\eta=constant$

```python
Hs=[0.05,0.1,0.2,0.3,0.4,0.5]
n=3
ys=np.linspace(np.log(0.5),np.log(1.5),16)
eta=np.zeros([5,2],dtype=np.float)
gamma=np.zeros([5,2],dtype=np.float)
constant=2
for i in range(5):
    eta[i,:]=2.0+np.array([0.5*i,-0.2*i])
    aux=(constant-eta[i,1])/(eta[i,0]-eta[i,1])
    if i==0:
        gamma[i,:]=np.array([1,0])
    else:
        gamma[i,:]=np.array([aux,1-aux])

rateFunc=np.zeros([5,len(ys)])
i=0
for i in range(5):
    a=RateFunction(H-0.5,n,eta[i,:],gamma[i,:])
    j=0
    for y in ys:
        I=minimize(a.compute,x0=x0,method='BFGS',args=(y),tol=1e-10)
        rateFunc[i,j]=(I.fun)
        j+=1
```

```python
plt.figure(figsize=(14,5))
plt.subplot(1,2,1)
i=0
for i in range(5):
    plt.plot(ys,rateFunc[i,:])
plt.legend(fontsize=13)
plt.title("$\hat{\Lambda}^v(y)$",fontsize=20)
plt.xlabel("Log-moneyness",fontsize=14)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.subplot(1,2,2)
i=0
for i in range(5):
    plt.plot(0,100*np.sqrt(2*H)*(constant)/(H+0.5)/np.sqrt(2*H+2),'*k',markersize=9,label="Asymptotic by AGM" if i == 0 else "")
    plt.plot(ys,100*np.abs(ys)/np.sqrt(2*rateFunc[i,:]),label="$(\\eta_1,\\eta_2,\\gamma_1,\\gamma_2)$=(%1.1f,%1.1f,%1.1f,%1.1f)"%(eta[i,0],eta[i,1],gamma[i,0],gamma[i,1]))
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.,fontsize=15)
plt.title("$\\sigma_{BS}(y)$",fontsize=20)
plt.xlabel("Log-moneyness",fontsize=14)
plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter() )
#plt.gca().xaxis.set_major_formatter(mtick.PercentFormatter() )
plt.tick_params(axis='both', which='major', labelsize=15)
plt.tight_layout()
plt.savefig('roughBergomiSmilesFixedVolOfVol.png', bbox_inches = "tight",dpi=300)
plt.show()
```

### Consistent with AGM result we may fix the ATMI and modify the skew


# Neural Receiver for OFDM SIMO Systems

In this notebook, you will learn how to train a neural receiver that implements OFDM detection.
The considered setup is shown in the figure below.
As one can see, the neural receiver substitutes channel estimation, equalization, and demapping.
It takes as input the post-DFT (discrete Fourier transform) received samples, which form the received resource grid, and computes log-likelihood ratios (LLRs) on the transmitted coded bits.
These LLRs are then fed to the outer decoder to reconstruct the transmitted information bits.

Two baselines are considered for benchmarking, which are shown in the figure above.
Both baselines use linear minimum mean square error (LMMSE) equalization and demapping assuming additive white Gaussian noise (AWGN).
They differ by how channel estimation is performed:

- **Pefect CSI**: Perfect channel state information (CSI) knowledge is assumed.
- **LS estimation**: Uses the transmitted pilots to perform least squares (LS) estimation of the channel with nearest-neighbor interpolation.

All the considered end-to-end systems use an LDPC outer code from the 5G NR specification, QPSK modulation, and a 3GPP CDL channel model simulated in the frequency domain.

* [GPU Configuration and Imports](#GPU-Configuration-and-Imports)
* [Simulation Parameters](#Simulation-Parameters)
* [Neural Receiver](#Neural-Receiver)
* [End-to-end System](#End-to-end-System)
* [End-to-end System as a Keras Model](#End-to-end-System-as-a-Keras-Model)
* [Evaluation of the Baselines](#Evaluation-of-the-Baselines)
* [Training the Neural Receiver](#Training-the-Neural-Receiver)
* [Evaluation of the Neural Receiver](#Evaluation-of-the-Neural-Receiver)
* [Pre-computed Results](#Pre-computed-Results)
* [References](#References)

## GPU Configuration and Imports

```python
# Configure the notebook to use only a single GPU and allocate only as much memory as needed
# For more details, see https://www.tensorflow.org/guide/gpu
import tensorflow as tf
gpus = tf.config.list_physical_devices('GPU')
print('Number of GPUs available :', len(gpus))
if gpus:
    gpu_num = 0 # Index of the GPU to use
    try:
        tf.config.set_visible_devices(gpus[gpu_num], 'GPU')
        print('Only GPU number', gpu_num, 'used.')
        tf.config.experimental.set_memory_growth(gpus[gpu_num], True)
    except RuntimeError as e:
        print(e)
```

    Number of GPUs available : 2
    Only GPU number 0 used.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import pickle

from tensorflow.keras import Model
from tensorflow.keras.layers import Layer, Conv2D, LayerNormalization
from tensorflow.nn import relu

# Import Sionna
try:
    import sionna
except ImportError as e:
    # Install Sionna if package is not already installed
    import os
    os.system("pip install sionna")
    import sionna

from sionna.channel.tr38901 import Antenna, AntennaArray, CDL
from sionna.channel import OFDMChannel
from sionna.mimo import StreamManagement
from sionna.ofdm import ResourceGrid, ResourceGridMapper, LSChannelEstimator, LMMSEEqualizer, RemoveNulledSubcarriers, ResourceGridDemapper
from sionna.utils import BinarySource, ebnodb2no, insert_dims, flatten_last_dims, log10, expand_to_rank
from sionna.fec.ldpc.encoding import LDPC5GEncoder
from sionna.fec.ldpc.decoding import LDPC5GDecoder
from sionna.mapping import Mapper, Demapper
from sionna.utils.metrics import compute_ber
from sionna.utils import sim_ber
```

## Simulation Parameters

```python
############################################
## Channel configuration
carrier_frequency = 3.5e9 # Hz
delay_spread = 100e-9 # s
cdl_model = "C" # CDL model to use
speed = 10.0 # Speed for evaluation and training [m/s]
# SNR range for evaluation and training [dB]
ebno_db_min = -5.0
ebno_db_max = 10.0

############################################
## OFDM waveform configuration
subcarrier_spacing = 30e3 # Hz
fft_size = 128 # Number of subcarriers forming the resource grid, including the null-subcarrier and the guard bands
num_ofdm_symbols = 14 # Number of OFDM symbols forming the resource grid
dc_null = True # Null the DC subcarrier
num_guard_carriers = [5, 6] # Number of guard carriers on each side
pilot_pattern = "kronecker" # Pilot pattern
pilot_ofdm_symbol_indices = [2, 11] # Index of OFDM symbols carrying pilots
cyclic_prefix_length = 0 # Simulation in frequency domain. This is useless

############################################
## Modulation and coding configuration
num_bits_per_symbol = 2 # QPSK
coderate = 0.5 # Coderate for LDPC code

############################################
## Neural receiver configuration
num_conv_channels = 128 # Number of convolutional channels for the convolutional layers forming the neural receiver

############################################
## Training configuration
num_training_iterations = 30000 # Number of training iterations
training_batch_size = 128 # Training batch size
model_weights_path = "neural_receiver_weights" # Location to save the neural receiver weights once training is done

############################################
## Evaluation configuration
results_filename = "neural_receiver_results" # Location to save the results
```

The `StreamManagement` class is used to configure the receiver-transmitter association and the number of streams per transmitter.
A SIMO system is considered, with a single transmitter equipped with a single non-polarized antenna.
Therefore, there is only a single stream, and the receiver-transmitter association matrix is $[1]$.
The receiver is equipped with an antenna array.

```python
stream_manager = StreamManagement(np.array([[1]]), # Receiver-transmitter association matrix
                                  1)               # One stream per transmitter
```

The `ResourceGrid` class is used to configure the OFDM resource grid. It is initialized with the parameters defined above.

```python
resource_grid = ResourceGrid(num_ofdm_symbols = num_ofdm_symbols,
                             fft_size = fft_size,
                             subcarrier_spacing = subcarrier_spacing,
                             num_tx = 1,
                             num_streams_per_tx = 1,
                             cyclic_prefix_length = cyclic_prefix_length,
                             dc_null = dc_null,
                             pilot_pattern = pilot_pattern,
                             pilot_ofdm_symbol_indices = pilot_ofdm_symbol_indices,
                             num_guard_carriers = num_guard_carriers)
```

Outer coding is performed such that all the databits carried by the resource grid with size `fft_size`x`num_ofdm_symbols` form a single codeword.

```python
# Codeword length. It is calculated from the total number of databits carried by the resource grid, and the number of bits transmitted per resource element
n = int(resource_grid.num_data_symbols*num_bits_per_symbol)
# Number of information bits per codeword
k = int(n*coderate)
```

The SIMO link is setup by considering an uplink transmission with one user terminal (UT) equipped with a single non-polarized antenna, and a base station (BS) equipped with an antenna array.
One can try other configurations for the BS antenna array.

```python
ut_antenna = Antenna(polarization="single",
                     polarization_type="V",
                     antenna_pattern="38.901",
                     carrier_frequency=carrier_frequency)

bs_array = AntennaArray(num_rows=1,
                        num_cols=1,
                        polarization="dual",
                        polarization_type="VH",
                        antenna_pattern="38.901",
                        carrier_frequency=carrier_frequency)
```

## Neural Receiver

The next cell defines the Keras layers that implement the neural receiver.
As in [1] and [2], a neural receiver using residual convolutional layers is implemented. Convolutional layers are leveraged to efficienly process the 2D resource grid, that is fed as an input to the neural receiver.
Residual (skip) connections are used to avoid gradient vanishing [3].

For convinience, a Keras layer that implements a *residual block* is first defined. The Keras layer that implements the neural receiver is built by stacking such blocks. The following figure shows the architecture of the neural receiver.

```python
class ResidualBlock(Layer):
    r"""
    This Keras layer implements a convolutional residual block made of two convolutional layers with ReLU activation, layer normalization, and a skip connection.
    The number of convolutional channels of the input must match the number of kernel of the convolutional layers ``num_conv_channel`` for the skip connection to work.

    Input
    ------
    : [batch size, num time samples, num subcarriers, num_conv_channel], tf.float
        Input of the layer

    Output
    -------
    : [batch size, num time samples, num subcarriers, num_conv_channel], tf.float
        Output of the layer
    """

    def build(self, input_shape):

        # Layer normalization is done over the last three dimensions: time, frequency, conv 'channels'
        self._layer_norm_1 = LayerNormalization(axis=(-1, -2, -3))
        self._conv_1 = Conv2D(filters=num_conv_channels,
                              kernel_size=[3,3],
                              padding='same',
                              activation=None)
        # Layer normalization is done over the last three dimensions: time, frequency, conv 'channels'
        self._layer_norm_2 = LayerNormalization(axis=(-1, -2, -3))
        self._conv_2 = Conv2D(filters=num_conv_channels,
                              kernel_size=[3,3],
                              padding='same',
                              activation=None)

    def call(self, inputs):
        z = self._layer_norm_1(inputs)
        z = relu(z)
        z = self._conv_1(z)
        z = self._layer_norm_2(z)
        z = relu(z)
        z = self._conv_2(z) # [batch size, num time samples, num subcarriers, num_channels]
        # Skip connection
        z = z + inputs

        return z

class NeuralReceiver(Layer):
    r"""
    Keras layer implementing a residual convolutional neural receiver.

    This neural receiver is fed with the post-DFT received samples, forming a resource grid of size num_of_symbols x fft_size, and computes LLRs on the transmitted coded bits.
    These LLRs can then be fed to an outer decoder to reconstruct the information bits.

    As the neural receiver is fed with the entire resource grid, including the guard bands and pilots, it also computes LLRs for these resource elements.
    They must be discarded to only keep the LLRs corresponding to the data-carrying resource elements.

    Input
    ------
    y : [batch size, num rx antenna, num ofdm symbols, num subcarriers], tf.complex
        Received post-DFT samples.

    no : [batch size], tf.float32
        Noise variance. At training, a different noise variance value is sampled for each batch example.

    Output
    -------
    : [batch size, num ofdm symbols, num subcarriers, num_bits_per_symbol]
        LLRs on the transmitted bits.
        LLRs computed for resource elements not carrying data (pilots, guard bands...) must be discarded.
    """

    def build(self, input_shape):

        # Input convolution
        self._input_conv = Conv2D(filters=num_conv_channels,
                                  kernel_size=[3,3],
                                  padding='same',
                                  activation=None)
        # Residual blocks
        self._res_block_1 = ResidualBlock()
        self._res_block_2 = ResidualBlock()
        self._res_block_3 = ResidualBlock()
        self._res_block_4 = ResidualBlock()
        # Output conv
        self._output_conv = Conv2D(filters=num_bits_per_symbol,
                                   kernel_size=[3,3],
                                   padding='same',
                                   activation=None)

    def call(self, inputs):
        y, no = inputs

        # Feeding the noise power in log10 scale helps with the performance
        no = log10(no)

        # Stacking the real and imaginary components of the different antennas along the 'channel' dimension
        y = tf.transpose(y, [0, 2, 3, 1]) # Putting antenna dimension last
        no = insert_dims(no, 3, 1)
        no = tf.tile(no, [1, y.shape[1], y.shape[2], 1])
        # z : [batch size, num ofdm symbols, num subcarriers, 2*num rx antenna + 1]
        z = tf.concat([tf.math.real(y),
                       tf.math.imag(y),
                       no], axis=-1)
        # Input conv
        z = self._input_conv(z)
        # Residual blocks
        z = self._res_block_1(z)
        z = self._res_block_2(z)
        z = self._res_block_3(z)
        z = self._res_block_4(z)
        # Output conv
        z = self._output_conv(z)

        return z
```

## End-to-end System

The following cell defines the end-to-end system.

Training is done on the bit-metric decoding (BMD) rate which is computed from the transmitted bits and LLRs:

\begin{equation}
R = 1 - \frac{1}{SNMK} \sum_{s = 0}^{S-1} \sum_{n = 0}^{N-1} \sum_{m = 0}^{M-1} \sum_{k = 0}^{K-1} \texttt{BCE} \left( B_{s,n,m,k}, \texttt{LLR}_{s,n,m,k} \right)
\end{equation}

where

* $S$ is the batch size
* $N$ the number of subcarriers
* $M$ the number of OFDM symbols
* $K$ the number of bits per symbol
* $B_{s,n,m,k}$ the $k^{th}$ coded bit transmitted on the resource element $(n,m)$ and for the $s^{th}$ batch example
* $\texttt{LLR}_{s,n,m,k}$ the LLR (logit) computed by the neural receiver corresponding to the $k^{th}$ coded bit transmitted on the resource element $(n,m)$ and for the $s^{th}$ batch example
* $\texttt{BCE} \left( \cdot, \cdot \right)$ the binary cross-entropy in log base 2

Because no outer code is required at training, the outer encoder and decoder are not used at training to reduce computational complexity.

The BMD rate is known to be an achievable information rate for BICM systems, which motivates its used as objective function [4].

```python
## Transmitter
binary_source = BinarySource()
mapper = Mapper("qam", num_bits_per_symbol)
rg_mapper = ResourceGridMapper(resource_grid)

## Channel
cdl = CDL(cdl_model, delay_spread, carrier_frequency,
          ut_antenna, bs_array, "uplink", min_speed=speed)
channel = OFDMChannel(cdl, resource_grid, normalize_channel=True, return_channel=True)

## Receiver
neural_receiver = NeuralReceiver()
rg_demapper = ResourceGridDemapper(resource_grid, stream_manager) # Used to extract data-carrying resource elements
```

The following cell performs one forward step through the end-to-end system:

```python
batch_size = 64
ebno_db = tf.fill([batch_size], 5.0)
no = ebnodb2no(ebno_db, num_bits_per_symbol, coderate)

## Transmitter
# Generate codewords
c = binary_source([batch_size, 1, 1, n])
print("c shape: ", c.shape)
# Map bits to QAM symbols
x = mapper(c)
print("x shape: ", x.shape)
# Map the QAM symbols to a resource grid
x_rg = rg_mapper(x)
print("x_rg shape: ", x_rg.shape)

######################################
## Channel
# A batch of new channel realizations is sampled and applied at every inference
no_ = expand_to_rank(no, tf.rank(x_rg))
y,_ = channel([x_rg, no_])
print("y shape: ", y.shape)

######################################
## Receiver
# The neural receover computes LLRs from the frequency domain received symbols and N0
y = tf.squeeze(y, axis=1)
llr = neural_receiver([y, no])
print("llr shape: ", llr.shape)
# Reshape the input to fit what the resource grid demapper is expected
llr = insert_dims(llr, 2, 1)
# Extract data-carrying resource elements. The other LLRs are discarded
llr = rg_demapper(llr)
llr = tf.reshape(llr, [batch_size, 1, 1, n])
print("Post RG-demapper LLRs: ", llr.shape)
```

    c shape:  (64, 1, 1, 2784)
    x shape:  (64, 1, 1, 1392)
    x_rg shape:  (64, 1, 1, 14, 128)
    y shape:  (64, 1, 2, 14, 128)
    llr shape:  (64, 14, 128, 2)
    Post RG-demapper LLRs:  (64, 1, 1, 2784)

The BMD rate is computed from the LLRs and transmitted bits as follows:

```python
bce = tf.nn.sigmoid_cross_entropy_with_logits(c, llr)
bce = tf.reduce_mean(bce)
rate = tf.constant(1.0, tf.float32) - bce/tf.math.log(2.)
print(f"Rate: {rate:.2E} bit")
```

    Rate: -7.77E-01 bit

The rate is very poor (negative values means 0 bit) as the neural receiver is not trained.

## End-to-end System as a Keras Model

The following Keras *Model* implements the three considered end-to-end systems (perfect CSI baseline, LS estimation baseline, and neural receiver).

When instantiating the Keras model, the parameter ``system`` is used to specify the system to setup, and the parameter ``training`` is used to specified if the system is instantiated to be trained or to be evaluated. The ``training`` parameter is only relevant when the neural receiver is used.

At each call of this model:

* A batch of codewords is randomly sampled, modulated, and mapped to resource grids to form the channel inputs
* A batch of channel realizations is randomly sampled and applied to the channel inputs
* The receiver is executed on the post-DFT received samples to compute LLRs on the coded bits.
  Which receiver is executed (baseline with perfect CSI knowledge, baseline with LS estimation, or neural receiver) depends on the specified ``system`` parameter.
* If not training, the outer decoder is applied to reconstruct the information bits
* If training, the BMD rate is estimated over the batch from the LLRs and the transmitted bits

```python
class E2ESystem(Model):
    r"""
    Keras model that implements the end-to-end systems.

    As the three considered end-to-end systems (perfect CSI baseline, LS estimation baseline, and neural receiver) share most of
    the link components (transmitter, channel model, outer code...), they are implemented using the same Keras model.

    When instantiating the Keras model, the parameter ``system`` is used to specify the system to setup,
    and the parameter ``training`` is used to specified if the system is instantiated to be trained or to be evaluated.
    The ``training`` parameter is only relevant when the neural

    At each call of this model:
    * A batch of codewords is randomly sampled, modulated, and mapped to resource grids to form the channel inputs
    * A batch of channel realizations is randomly sampled and applied to the channel inputs
    * The receiver is executed on the post-DFT received samples to compute LLRs on the coded bits.
      Which receiver is executed (baseline with perfect CSI knowledge, baseline with LS estimation, or neural receiver) depends
      on the specified ``system`` parameter.
    * If not training, the outer decoder is applied to reconstruct the information bits
    * If training, the BMD rate is estimated over the batch from the LLRs and the transmitted bits

    Parameters
    -----------
    system : str
        Specify the receiver to use. Should be one of 'baseline-perfect-csi', 'baseline-ls-estimation' or 'neural-receiver'

    training : bool
        Set to `True` if the system is instantiated to be trained. Set to `False` otherwise. Defaults to `False`.
        If the system is instantiated to be trained, the outer encoder and decoder are not instantiated as they are not required for training.
        This significantly reduces the computational complexity of training.
        If training, the bit-metric decoding (BMD) rate is computed from the transmitted bits and the LLRs. The BMD rate is known to be
        an achievable information rate for BICM systems, and therefore training of the neural receiver aims at maximizing this rate.

    Input
    ------
    batch_size : int
        Batch size

    no : scalar or [batch_size], tf.float
        Noise variance.
        At training, a different noise variance should be sampled for each batch example.

    Output
    -------
    If ``training`` is set to `True`, then the output is a single scalar, which is an estimation of the BMD rate computed over the batch. It
    should be used as objective for training.
    If ``training`` is set to `False`, the transmitted information bits and their reconstruction on the receiver side are returned to
    compute the block/bit error rate.
    """

    def __init__(self, system, training=False):
        super().__init__()
        self._system = system
        self._training = training

        ######################################
        ## Transmitter
        self._binary_source = BinarySource()
        # To reduce the computational complexity of training, the outer code is not used when training,
        # as it is not required
        if not training:
            self._encoder = LDPC5GEncoder(k, n)
        self._mapper = Mapper("qam", num_bits_per_symbol)
        self._rg_mapper = ResourceGridMapper(resource_grid)

        ######################################
        ## Channel
        # A 3GPP CDL channel model is used
        cdl = CDL(cdl_model, delay_spread, carrier_frequency,
                  ut_antenna, bs_array, "uplink", min_speed=speed)
        self._channel = OFDMChannel(cdl, resource_grid, normalize_channel=True, return_channel=True)

        ######################################
        ## Receiver
        # Three options for the receiver depending on the value of `system`
        if "baseline" in system:
            if system == 'baseline-perfect-csi': # Perfect CSI
                self._removed_null_subc = RemoveNulledSubcarriers(resource_grid)
            elif system == 'baseline-ls-estimation': # LS estimation
                self._ls_est = LSChannelEstimator(resource_grid, interpolation_type="nn")
            # Components required by both baselines
            self._lmmse_equ = LMMSEEqualizer(resource_grid, stream_manager, )
            self._demapper = Demapper("app", "qam", num_bits_per_symbol)
        elif system == "neural-receiver": # Neural receiver
            self._neural_receiver = NeuralReceiver()
            self._rg_demapper = ResourceGridDemapper(resource_grid, stream_manager) # Used to extract data-carrying resource elements
        # To reduce the computational complexity of training, the outer code is not used when training,
        # as it is not required
        if not training:
            self._decoder = LDPC5GDecoder(self._encoder, hard_out=True)

    @tf.function
    def call(self, batch_size, ebno_db):

        # If `ebno_db` is a scalar, a tensor with shape [batch size] is created as it is what is expected by some layers
        if len(ebno_db.shape) == 0:
            ebno_db = tf.fill([batch_size], ebno_db)

        ######################################
        ## Transmitter
        no = ebnodb2no(ebno_db, num_bits_per_symbol, coderate)
        # Outer coding is only performed if not training
        if self._training:
            c = self._binary_source([batch_size, 1, 1, n])
        else:
            b = self._binary_source([batch_size, 1, 1, k])
            c = self._encoder(b)
        # Modulation
        x = self._mapper(c)
        x_rg = self._rg_mapper(x)

        ######################################
        ## Channel
        # A batch of new channel realizations is sampled and applied at every inference
        no_ = expand_to_rank(no, tf.rank(x_rg))
        y,h = self._channel([x_rg, no_])

        ######################################
        ## Receiver
        # Three options for the receiver depending on the value of ``system``
        if "baseline" in self._system:
            if self._system == 'baseline-perfect-csi':
                h_hat = self._removed_null_subc(h) # Extract non-null subcarriers
                err_var = 0.0 # No channel estimation error when perfect CSI knowledge is assumed
            elif self._system == 'baseline-ls-estimation':
                h_hat, err_var = self._ls_est([y, no]) # LS channel estimation with nearest-neighbor
            x_hat, no_eff = self._lmmse_equ([y, h_hat, err_var, no]) # LMMSE equalization
            no_eff_= expand_to_rank(no_eff, tf.rank(x_hat))
            llr = self._demapper([x_hat, no_eff_]) # Demapping
        elif self._system == "neural-receiver":
            # The neural receover computes LLRs from the frequency domain received symbols and N0
            y = tf.squeeze(y, axis=1)
            llr = self._neural_receiver([y, no])
            llr = insert_dims(llr, 2, 1) # Reshape the input to fit what the resource grid demapper is expected
            llr = self._rg_demapper(llr) # Extract data-carrying resource elements. The other LLrs are discarded
            llr = tf.reshape(llr, [batch_size, 1, 1, n]) # Reshape the LLRs to fit what the outer decoder is expected

        # Outer coding is not needed if the information rate is returned
        if self._training:
            # Compute and return BMD rate (in bit), which is known to be an achievable
            # information rate for BICM systems.
            # Training aims at maximizing the BMD rate
            bce = tf.nn.sigmoid_cross_entropy_with_logits(c, llr)
            bce = tf.reduce_mean(bce)
            rate = tf.constant(1.0, tf.float32) - bce/tf.math.log(2.)
            return rate
        else:
            # Outer decoding
            b_hat = self._decoder(llr)
            return b,b_hat # Ground truth and reconstructed information bits returned for BER/BLER computation
```

## Evaluation of the Baselines

We evaluate the BERs achieved by the baselines in the next cell.

**Note:** Evaluation of the two systems can take a while. Therefore, we provide pre-computed results at the end of this notebook.

```python
# Range of SNRs over which the systems are evaluated
ebno_dbs = np.arange(ebno_db_min, # Min SNR for evaluation
                     ebno_db_max, # Max SNR for evaluation
                     0.5) # Step
```

```python
# Dictionnary storing the evaluation results
BLER = {}

model = E2ESystem('baseline-perfect-csi')
_,bler = sim_ber(model, ebno_dbs, batch_size=128, num_target_block_errors=100, max_mc_iter=100)
BLER['baseline-perfect-csi'] = bler.numpy()

model = E2ESystem('baseline-ls-estimation')
_,bler = sim_ber(model, ebno_dbs, batch_size=128, num_target_block_errors=100, max_mc_iter=100)
BLER['baseline-ls-estimation'] = bler.numpy()
```

    EbNo [dB] |        BER |       BLER |  bit errors |    num bits | block errors |  num blocks | runtime [s] |    status
    ---------------------------------------------------------------------------------------------------------------------------------------
         -5.0 | 2.4973e-01 | 1.0000e+00 |       44496 |      178176 |          128 |         128 |         7.3 |reached target block errors
         -4.5 | 2.3573e-01 | 1.0000e+00 |       42001 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -4.0 | 2.1623e-01 | 1.0000e+00 |       38527 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -3.5 | 1.9510e-01 | 1.0000e+00 |       34762 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -3.0 | 1.6282e-01 | 1.0000e+00 |       29011 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -2.5 | 1.0123e-01 | 9.9219e-01 |       18037 |      178176 |          127 |         128 |         0.2 |reached target block errors
         -2.0 | 2.1967e-02 | 5.5078e-01 |        7828 |      356352 |          141 |         256 |         0.3 |reached target block errors
         -1.5 | 4.8697e-04 | 2.6042e-02 |        2603 |     5345280 |          100 |        3840 |         5.1 |reached target block errors
         -1.0 | 1.8212e-04 | 2.5000e-03 |        3245 |    17817600 |           32 |       12800 |        17.2 |reached max iter
         -0.5 | 2.4414e-05 | 5.4688e-04 |         435 |    17817600 |            7 |       12800 |        17.1 |reached max iter
          0.0 | 0.0000e+00 | 0.0000e+00 |           0 |    17817600 |            0 |       12800 |        17.2 |reached max iter

    Simulation stopped as no error occurred @ EbNo = 0.0 dB.

    WARNING:tensorflow:From /usr/local/lib/python3.8/dist-packages/tensorflow/python/util/dispatch.py:1082: calling gather (from tensorflow.python.ops.array_ops) with validate_indices is deprecated and will be removed in a future version.
    Instructions for updating:
    The `validate_indices` argument has no effect. Indices are always validated on CPU and never validated on GPU.
    EbNo [dB] |        BER |       BLER |  bit errors |    num bits | block errors |  num blocks | runtime [s] |    status
    ---------------------------------------------------------------------------------------------------------------------------------------
         -5.0 | 3.9198e-01 | 1.0000e+00 |       69842 |      178176 |          128 |         128 |         3.7 |reached target block errors
         -4.5 | 3.7972e-01 | 1.0000e+00 |       67657 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -4.0 | 3.6827e-01 | 1.0000e+00 |       65616 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -3.5 | 3.5388e-01 | 1.0000e+00 |       63053 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -3.0 | 3.3977e-01 | 1.0000e+00 |       60539 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -2.5 | 3.2656e-01 | 1.0000e+00 |       58185 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -2.0 | 3.1059e-01 | 1.0000e+00 |       55340 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -1.5 | 2.9594e-01 | 1.0000e+00 |       52729 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -1.0 | 2.7719e-01 | 1.0000e+00 |       49388 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -0.5 | 2.5766e-01 | 1.0000e+00 |       45909 |      178176 |          128 |         128 |         0.2 |reached target block errors
          0.0 | 2.3227e-01 | 1.0000e+00 |       41385 |      178176 |          128 |         128 |         0.2 |reached target block errors
          0.5 | 2.1057e-01 | 1.0000e+00 |       37518 |      178176 |          128 |         128 |         0.2 |reached target block errors
          1.0 | 1.6697e-01 | 1.0000e+00 |       29750 |      178176 |          128 |         128 |         0.2 |reached target block errors
          1.5 | 1.0083e-01 | 9.3750e-01 |       17966 |      178176 |          120 |         128 |         0.2 |reached target block errors
          2.0 | 1.0116e-02 | 2.3633e-01 |        7210 |      712704 |          121 |         512 |         0.7 |reached target block errors
          2.5 | 6.4064e-04 | 1.0521e-02 |        8561 |    13363200 |          101 |        9600 |        12.9 |reached target block errors
          3.0 | 1.4334e-04 | 1.4844e-03 |        2554 |    17817600 |           19 |       12800 |        17.2 |reached max iter
          3.5 | 1.1663e-04 | 7.8125e-04 |        2078 |    17817600 |           10 |       12800 |        17.2 |reached max iter
          4.0 | 4.2767e-05 | 3.9063e-04 |         762 |    17817600 |            5 |       12800 |        17.3 |reached max iter
          4.5 | 5.1185e-05 | 3.1250e-04 |         912 |    17817600 |            4 |       12800 |        17.3 |reached max iter
          5.0 | 2.6435e-05 | 2.3437e-04 |         471 |    17817600 |            3 |       12800 |        17.3 |reached max iter
          5.5 | 1.3694e-05 | 7.8125e-05 |         244 |    17817600 |            1 |       12800 |        17.3 |reached max iter
          6.0 | 0.0000e+00 | 0.0000e+00 |           0 |    17817600 |            0 |       12800 |        17.3 |reached max iter

    Simulation stopped as no error occurred @ EbNo = 6.0 dB.

## Training the Neural Receiver

In the next cell, one forward pass is performed within a *gradient tape*, which enables the computation of gradient and therefore the optimization of the neural network through stochastic gradient descent (SGD).

**Note:** For an introduction to the implementation of differentiable communication systems and their optimization through SGD and backpropagation with Sionna, please refer to [the Part 2 of the Sionna tutorial for Beginners](https://nvlabs.github.io/sionna/examples/Sionna_tutorial_part2.html).

```python
# The end-to-end system equipped with the neural receiver is instantiated for training.
# When called, it therefore returns the estimated BMD rate
model = E2ESystem('neural-receiver', training=True)

# Sampling a batch of SNRs
ebno_db = tf.random.uniform(shape=[], minval=ebno_db_min, maxval=ebno_db_max)
# Forward pass
with tf.GradientTape() as tape:
    rate = model(training_batch_size, ebno_db)
    # Tensorflow optimizers only know how to minimize loss function.
    # Therefore, a loss function is defined as the additive inverse of the BMD rate
    loss = -rate
```

Next, one can perform one step of stochastic gradient descent (SGD).
The Adam optimizer is used

```python
optimizer = tf.keras.optimizers.Adam()

# Computing and applying gradients
weights = model.trainable_weights
grads = tape.gradient(loss, weights)
optimizer.apply_gradients(zip(grads, weights))
```

    <tf.Variable 'UnreadVariable' shape=() dtype=int64, numpy=1>

Training consists in looping over SGD steps. The next cell implements a training loop.

At each iteration:
- A batch of SNRs $E_b/N_0$ is sampled
- A forward pass through the end-to-end system is performed within a gradient tape
- The gradients are computed using the gradient tape, and applied using the Adam optimizer
- The achieved BMD rate is periodically shown

After training, the weights of the models are saved in a file

**Note:** Training can take a while. Therefore, pre-trained weights are made available with this notebook. Do not execute the next cell if you don't want to train the model from scratch.

```python
model = E2ESystem('neural-receiver', training=True)

optimizer = tf.keras.optimizers.Adam()

for i in range(num_training_iterations):
    # Sampling a batch of SNRs
    ebno_db = tf.random.uniform(shape=[], minval=ebno_db_min, maxval=ebno_db_max)
    # Forward pass
    with tf.GradientTape() as tape:
        rate = model(training_batch_size, ebno_db)
        # Tensorflow optimizers only know how to minimize loss function.
        # Therefore, a loss function is defined as the additive inverse of the BMD rate
        loss = -rate
    # Computing and applying gradients
    weights = model.trainable_weights
    grads = tape.gradient(loss, weights)
    optimizer.apply_gradients(zip(grads, weights))
    # Periodically printing the progress
    if i % 100 == 0:
        print('Iteration {}/{}  Rate: {:.4f} bit'.format(i, num_training_iterations, rate.numpy()), end='\r')

# Save the weights in a file
weights = model.get_weights()
with open(model_weights_path, 'wb') as f:
    pickle.dump(weights, f)
```

## Evaluation of the Neural Receiver

The next cell evaluates the neural receiver.

**Note:** Evaluation of the system can take a while and requires having the trained weights of the neural receiver. Therefore, we provide pre-computed results at the end of this notebook.

```python
model = E2ESystem('neural-receiver')

# Run one inference to build the layers and loading the weights
model(1, tf.constant(10.0, tf.float32))
with open(model_weights_path, 'rb') as f:
    weights = pickle.load(f)
model.set_weights(weights)

# Evaluations
_,bler = sim_ber(model, ebno_dbs, batch_size=128, num_target_block_errors=100, max_mc_iter=100)
BLER['neural-receiver'] = bler.numpy()

```

    EbNo [dB] |        BER |       BLER |  bit errors |    num bits | block errors |  num blocks | runtime [s] |    status
    ---------------------------------------------------------------------------------------------------------------------------------------
         -5.0 | 2.5996e-01 | 1.0000e+00 |       46319 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -4.5 | 2.4409e-01 | 1.0000e+00 |       43491 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -4.0 | 2.2290e-01 | 1.0000e+00 |       39715 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -3.5 | 2.0002e-01 | 1.0000e+00 |       35638 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -3.0 | 1.7395e-01 | 1.0000e+00 |       30993 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -2.5 | 1.1934e-01 | 1.0000e+00 |       21263 |      178176 |          128 |         128 |         0.2 |reached target block errors
         -2.0 | 4.0123e-02 | 7.3438e-01 |       14298 |      356352 |          188 |         256 |         0.4 |reached target block errors
         -1.5 | 4.4114e-03 | 1.1161e-01 |        5502 |     1247232 |          100 |         896 |         1.4 |reached target block errors
         -1.0 | 6.7739e-04 | 8.2237e-03 |       11466 |    16926720 |          100 |       12160 |        18.2 |reached target block errors
         -0.5 | 2.3106e-04 | 2.8125e-03 |        4117 |    17817600 |           36 |       12800 |        19.1 |reached max iter
          0.0 | 1.2718e-04 | 1.0938e-03 |        2266 |    17817600 |           14 |       12800 |        19.1 |reached max iter
          0.5 | 1.6501e-05 | 3.1250e-04 |         294 |    17817600 |            4 |       12800 |        19.2 |reached max iter
          1.0 | 3.2384e-05 | 3.1250e-04 |         577 |    17817600 |            4 |       12800 |        19.0 |reached max iter
          1.5 | 0.0000e+00 | 0.0000e+00 |           0 |    17817600 |            0 |       12800 |        19.1 |reached max iter

    Simulation stopped as no error occurred @ EbNo = 1.5 dB.

Finally, we plots the BLERs

```python
plt.figure(figsize=(10,6))
# Baseline - Perfect CSI
plt.semilogy(ebno_dbs, BLER['baseline-perfect-csi'], 'o-', c=f'C0', label=f'Baseline - Perfect CSI')
# Baseline - LS Estimation
plt.semilogy(ebno_dbs, BLER['baseline-ls-estimation'], 'x--', c=f'C1', label=f'Baseline - LS Estimation')
# Neural receiver
plt.semilogy(ebno_dbs, BLER['neural-receiver'], 's-.', c=f'C2', label=f'Neural receiver')
#
plt.xlabel(r"$E_b/N_0$ (dB)")
plt.ylabel("BLER")
plt.grid(which="both")
plt.ylim((1e-4, 1.0))
plt.legend()
plt.tight_layout()
```

## Pre-computed Results

```python
pre_computed_results = "{'baseline-perfect-csi': [1.0, 1.0, 1.0, 1.0, 1.0, 0.9916930379746836, 0.5367080479452054, 0.0285078125, 0.0017890625, 0.0006171875, 0.0002265625, 9.375e-05, 2.34375e-05, 7.8125e-06, 1.5625e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'baseline-ls-estimation': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9998022151898734, 0.9199448529411764, 0.25374190938511326, 0.0110234375, 0.002078125, 0.0008359375, 0.0004375, 0.000171875, 9.375e-05, 4.6875e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'neural-receiver': [1.0, 1.0, 1.0, 1.0, 1.0, 0.9984177215189873, 0.7505952380952381, 0.10016025641025642, 0.00740625, 0.0021640625, 0.000984375, 0.0003671875, 0.000203125, 0.0001484375, 3.125e-05, 2.34375e-05, 7.8125e-06, 7.8125e-06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}"
BLER = eval(pre_computed_results)
```

## References

[1] M. Honkala, D. Korpi and J. M. J. Huttunen, "DeepRx: Fully Convolutional Deep Learning Receiver," in IEEE Transactions on Wireless Communications, vol. 20, no. 6, pp. 3925-3940, June 2021, doi: 10.1109/TWC.2021.3054520.

[2] F. Ait Aoudia and J. Hoydis, "End-to-end Learning for OFDM: From Neural Receivers to Pilotless Communication," in IEEE Transactions on Wireless Communications, doi: 10.1109/TWC.2021.3101364.

[3] Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun, "Deep Residual Learning for Image Recognition", Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2016, pp. 770-778

[4] G. Böcherer, "Achievable Rates for Probabilistic Shaping", arXiv:1707.01134, 2017.