# Wigner Functions
---
### Contributers
Russell P. Rundle$^{1,2}$, Todd Tilma$^{1,3}$, Vincent M. Dwyer$^{1,2}$, Mark J. Everitt (m.j.everitt@physics.org)$^{1}$

1 Quantum Systems Engineering Research Group, Physics Dpartment, Loughborough University, UK

2 Wolfson School, Loughborough University, UK

3 Tokyo Institute of Technology, Japan

## Introduction

In this notebook we demonstrate how to create Wigner functions by either using the full state or by measuring points in phase space. We will show the different methods which can be used by inputting an arbitrary state or just measuring the state on both the simulators and the IBM Quantum Experience.
The spin Wigner function presented here is based on work from [*T Tilma, MJ Everitt, JH Samson, WJ Munro, K Nemoto, Phys. Rev. Lett. 117, 180401*](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.117.180401) and can be calculated analytically as

$W(\boldsymbol{\Omega}) = \mathrm{Tr}\left[\hat{\rho} \; \hat{U}(\boldsymbol{\Omega}) \hat{\Pi} \hat{U}^\dagger(\boldsymbol{\Omega})\right]$

Where $\hat{U}$ is the rotation by the Euler angles and $\hat{\Pi}$ is the parity, such that

$\hat{\Pi} = \frac{1}{2}
\begin{pmatrix}
1+\sqrt{3} & 0\\
0 & 1-\sqrt{3}
\end{pmatrix}$

for one qubit. The Hilbert space for a single qubit is spanned by the Euler angles where $\theta$ (the elevation) and $\phi$ (the azimuth) thus the full Wigner function for $n$ qubits is given by $2n$ degrees of freedom.

When there is more than one qubit, we take the Kronecker tensor product of the $\hat{U}$s and $\hat{\Pi}$. In the following examples, various methods of extracting information from 2 entangled qubits from four degrees of freedom are shown.
These methods include taking an the equal angle slice, where we set the angles $\theta_0 = \theta_1 = \theta$ and $\phi_0 = \phi_1 = \phi$. We can also set some angles to be constant values as we plot the remaining angles against each other, an example of this can be seen when plotting the plaquettes where we set $\phi_0=\phi_1=0$ and we plot $\theta_0$ against $\theta_1$. Finally we show how we can simplify the Wigner function by just taking a 2 dimensional curve around the function, this is shown when we look at the equal angle equatorial slice where we take $\theta_i=\theta=\pi/2$ for all $\theta$s and $\phi_i=\phi$ for all values of $\phi$ where $0\leq\phi<2\pi$.
With this visualisation methods we show how these points in phase space can be measured directly by using the Wigner function tomography module and it works by using the u3 gates to rotate to points in phase space. Below is an example for measing points for a Bell-state, the first two gates create the Bell-state and the two u3 are example rotations to a point in phase space.

# Wigner Functions for Arbitrary States

First we will look at the Wigner function tomography module and show how we can create the spin Wigner function on a Bloch sphere for an arbitrary state. All that is necessary is to input a density matrix or state vector for a known state and then set the desired resolution for the plot (if no resolution is set, the mesh of the sphere will default to 100x100)

```python
# Checking the version of PYTHON
import sys
if sys.version_info < (3,0):
    raise Exception('Please use Python version 3 or greater.')
import numpy as np

# importing the QISKit
from qiskit import QuantumCircuit, QuantumProgram
import Qconfig

# import tomography library
import qiskit.tools.qcvv.tomography as tomo

#visualization packages
from qiskit.tools.visualization import plot_wigner_function, plot_wigner_data
```

## Wigner Function for an entangled Bell-state
We begin by creating the density matrix for a Bell-state (note that the matrices and state vectors need to be created in numpy matrix format), $\left(|00\rangle+|11\rangle\right)/\sqrt{2}$, which is given by

$
\frac{1}{2}
\begin{pmatrix}
1 & 0 & 0 & 1 \\
0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 \\
1 & 0 & 0 & 1
\end{pmatrix}
$

```python
density_matrix = np.matrix([[0.5, 0, 0, 0.5],[0, 0, 0, 0],[0, 0, 0, 0],[0.5, 0, 0, 0.5]])
print(density_matrix)
```

    [[0.5 0.  0.  0.5]
     [0.  0.  0.  0. ]
     [0.  0.  0.  0. ]
     [0.5 0.  0.  0.5]]

This density matrix can then be put into wigner_function which plots the Bloch sphere Wigner function for the equal angle slice, i.e. where $\theta_0 = \theta_1 = \theta$ and $\phi_0 = \phi_1 = \phi$

```python
plot_wigner_function(density_matrix, res=200)
```

If it is desired to calculate the analytic expression for the Wigner function, we can import the sympy package and run the Wigner function calculations symbolically. The below code is almost identical to the code in the Wigner function tomography module but edited to work with symbolics rather than run through the points in the mesh of the sphere.

```python
import sympy as sym
from sympy.physics.quantum import TensorProduct

num = int(np.log2(len(density_matrix)))
harr = sym.sqrt(3)
Delta_su2 = sym.zeros(2)
Delta = sym.ones(1)
for qubit in range(num):
    phi = sym.Indexed('phi', qubit)
    theta = sym.Indexed('theta', qubit)
    costheta  = harr*sym.cos(2*theta)
    sintheta  = harr*sym.sin(2*theta)
    Delta_su2[0,0] =  (1+costheta)/2
    Delta_su2[0,1] = -(sym.exp(2j*phi)*sintheta)/2
    Delta_su2[1,0] = -(sym.exp(-2j*phi)*sintheta)/2
    Delta_su2[1,1] =  (1-costheta)/2
    Delta = TensorProduct(Delta,Delta_su2)
W = sym.trace(density_matrix*Delta)
print(sym.latex(W))
```

    0.5 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.5 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.375 e^{2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + 0.375 e^{- 2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )}

pasting this result into a latex environment returns the Wigner function

$W = 0.5 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.5 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.375 e^{2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + 0.375 e^{- 2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )}$

### Wigner function using State tomography

We can create the density matrix for a given state by using the state tomography module, more information for the tomography module can be found in the [quantum state tomography tutorial](https://github.com/QISKit/qiskit-tutorial/blob/master/3_qcvv/state_tomography.ipynb). First creating an entangled Bell-state with the following set of gates and then taking the state tomography, we can create the spin Wigner function for the measured tomography

```python
Q_program = QuantumProgram()

number_of_qubits = 2
backend = 'local_qasm_simulator'
shots = 1024
bell_qubits = [0, 1]
qr = Q_program.create_quantum_register('qr',2)
cr = Q_program.create_classical_register('cr',2)
bell = Q_program.create_circuit('bell', [qr], [cr])
bell.h(qr[0])
bell.cx(qr[0],qr[1])
```

    <qiskit.extensions.standard.cx.CnotGate at 0x1165ec3c8>

The above code generates the circuits needed to create the Bell-state and below we show how to then build the density matrix for the state using the tomography module. The density matrix created can then be put into the Wigner fucntion visualisation module

```python
bell_tomo_set = tomo.state_tomography_set([0, 1])
bell_tomo_circuits = tomo.create_tomography_circuits(Q_program, 'bell', qr, cr, bell_tomo_set)
bell_tomo_result = Q_program.execute(bell_tomo_circuits, backend=backend, shots=shots)
bell_tomo_data = tomo.tomography_data(bell_tomo_result, 'bell', bell_tomo_set)
rho_fit_sim = tomo.fit_tomography_data(bell_tomo_data)
plot_wigner_function(np.matrix(rho_fit_sim),res=200)
```

Again, using the same code from before, we can calculate the analytic expression for the Wigner function from the generated density matrix, it can be seen that since the density matrix is less sparse, the resulting Wigner function has many more terms

```python
Delta_su2 = sym.zeros(2)
Delta = sym.ones(1)
for qubit in range(num):
    phi = sym.Indexed('phi', qubit)
    theta = sym.Indexed('theta', qubit)
    costheta  = harr*sym.cos(2*theta)
    sintheta  = harr*sym.sin(2*theta)
    Delta_su2[0,0] =  (1+costheta)/2
    Delta_su2[0,1] = -(sym.exp(2j*phi)*sintheta)/2
    Delta_su2[1,0] = -(sym.exp(-2j*phi)*sintheta)/2
    Delta_su2[1,1] =  (1-costheta)/2
    Delta = TensorProduct(Delta,Delta_su2)
W = sym.trace(np.matrix(rho_fit_sim)*Delta)
print(sym.latex(W))
```

    0.491915865423126 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.00304822758495185 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(0.00869493382510282 - 0.00783370809820619 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.00869493382510282 + 0.00783370809820619 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} + 0.00286997223350026 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.502165934758422 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(0.00635202430568175 + 0.00319297154682478 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.00635202430568175 - 0.00319297154682478 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.00466742644792856 + 0.00165960173857872 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.00466742644792856 - 0.00165960173857872 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0103070394398947 + 0.00329652116954638 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0103070394398947 - 0.00329652116954638 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} + \frac{3}{4} \left(0.492443570270737 + 0.00845863703582394 i\right) e^{2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(-0.00210405323397097 - 0.00166972608960388 i\right) e^{2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(-0.00210405323397097 + 0.00166972608960388 i\right) e^{- 2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.492443570270737 - 0.00845863703582394 i\right) e^{- 2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )}

$W = 0.493698558640376 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.00275286172575906 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(-0.00534735110770023 - 0.00770494561267218 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(-0.00534735110770023 + 0.00770494561267218 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} + 0.00266528247889166 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.500883297154973 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(-0.0015149834996068 + 0.000102164609935808 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(-0.0015149834996068 - 0.000102164609935808 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(-0.00628509857352323 - 0.000924492486476053 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(-0.00628509857352323 + 0.000924492486476053 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(-0.000462681165102337 + 0.00867331154973143 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(-0.000462681165102337 - 0.00867331154973143 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} + \frac{3}{4} \left(0.492697839219187 - 0.0085595434721166 i\right) e^{2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.00247818568149116 + 0.000867188901661959 i\right) e^{2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.00247818568149116 - 0.000867188901661959 i\right) e^{- 2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.492697839219187 + 0.0085595434721166 i\right) e^{- 2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )}$

# Experiment

```python
Q_program.set_api(Qconfig.APItoken, Qconfig.config['url'])
backend = 'ibmqx2'
max_credits = 8
shots = 1024
bell_qubits = [0, 1]
bell_tomo_set = tomo.state_tomography_set(bell_qubits)
bell_tomo_circuits = tomo.create_tomography_circuits(Q_program, 'bell', qr, cr, bell_tomo_set)
bell_tomo_result = Q_program.execute(bell_tomo_circuits, backend=backend, shots=shots,
                                     max_credits=max_credits, timeout=300)
bell_tomo_data = tomo.tomography_data(bell_tomo_result, 'bell', bell_tomo_set)
rho_fit_ibmqx = tomo.fit_tomography_data(bell_tomo_data)
plot_wigner_function(np.matrix(rho_fit_ibmqx), res=100)
```

```python
print(rho_fit_ibmqx)
```

    [[0.45501836+0.        j 0.02838654-0.0069407 j 0.03416704-0.00509147j
      0.39767563+0.01159956j]
     [0.02838654+0.0069407 j 0.04741073+0.        j 0.00425551+0.00329646j
      0.03233094-0.00878466j]
     [0.03416704+0.00509147j 0.00425551-0.00329646j 0.04684031+0.        j
      0.00867067+0.00760748j]
     [0.39767563-0.01159956j 0.03233094+0.00878466j 0.00867067-0.00760748j
      0.4507306 +0.        j]]

```python
Delta_su2 = sym.zeros(2)
Delta = sym.ones(1)
for qubit in range(num):
    phi = sym.Indexed('phi', qubit)
    theta = sym.Indexed('theta', qubit)
    costheta  = harr*sym.cos(2*theta)
    sintheta  = harr*sym.sin(2*theta)
    Delta_su2[0,0] =  (1+costheta)/2
    Delta_su2[0,1] = -(sym.exp(2j*phi)*sintheta)/2
    Delta_su2[1,0] = -(sym.exp(-2j*phi)*sintheta)/2
    Delta_su2[1,1] =  (1-costheta)/2
    Delta = TensorProduct(Delta,Delta_su2)
W = sym.trace(np.matrix(rho_fit_ibmqx)*Delta)
print(sym.latex(W))
```

    0.450730600133957 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.0468403077860062 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(0.00867067369882213 - 0.00760748089221235 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.00867067369882213 + 0.00760748089221235 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} + 0.047410729458634 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.455018362621403 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(0.0283865426373279 + 0.00694070351354548 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.0283865426373279 - 0.00694070351354548 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.0323309357962314 + 0.00878465966622207 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0323309357962314 - 0.00878465966622207 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0341670433513886 + 0.00509146712699025 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0341670433513886 - 0.00509146712699025 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} + \frac{3}{4} \left(0.397675625888841 - 0.0115995620461406 i\right) e^{2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.00425551338456106 - 0.00329645907683558 i\right) e^{2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.00425551338456106 + 0.00329645907683558 i\right) e^{- 2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.397675625888841 + 0.0115995620461406 i\right) e^{- 2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )}

$0.32699519833677 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.124306773174647 \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(0.0285310304913017 + 0.0238741788035984 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.0285310304913017 - 0.0238741788035984 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} + 0.124202902962916 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) + 0.424495125525667 \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) - \frac{\sqrt{3}}{2} \left(0.0490359773360699 + 0.0381005337507329 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.0490359773360699 - 0.0381005337507329 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{0} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{1} \right )} - \frac{\sqrt{3}}{2} \left(0.0512856636758563 + 0.0414367583959388 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0512856636758563 - 0.0414367583959388 i\right) \left(- \frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0408691303527403 + 0.0280408874045363 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} - \frac{\sqrt{3}}{2} \left(0.0408691303527403 - 0.0280408874045363 i\right) \left(\frac{\sqrt{3}}{2} \cos{\left (2 \theta_{1} \right )} + \frac{1}{2}\right) e^{- 2.0 i \phi_{0}} \sin{\left (2 \theta_{0} \right )} + \frac{3}{4} \left(0.285095302985003 - 0.00922257353994807 i\right) e^{2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(-0.035281529513114 - 0.00875077955598942 i\right) e^{2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(-0.035281529513114 + 0.00875077955598942 i\right) e^{- 2.0 i \phi_{0}} e^{2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )} + \frac{3}{4} \left(0.285095302985003 + 0.00922257353994807 i\right) e^{- 2.0 i \phi_{0}} e^{- 2.0 i \phi_{1}} \sin{\left (2 \theta_{0} \right )} \sin{\left (2 \theta_{1} \right )}$

# Measuring Points in Phase Space

We will now look at how to create the circuits to measure set points in phase space. The following results are based on the figures in [*RP Rundle, PW Mills, T Tilma, JH Samson, MJ Everitt, Phys. Rev. A 96, 022117*](r p rundle prl)

## Plaquette Visualisation
Staying with the bell state, we will look here at the slice where $\phi_0=\phi_1=0$ and we plot $\theta_0$ against $\theta_1$

```python
theta1_points = 8
theta2_points = 8
number_of_points = theta1_points*theta2_points
the1 = [0]*number_of_points
the2 = [0]*number_of_points #initialize theta values
phis = [[0]*number_of_points]*number_of_qubits #set phi values to 0

point = 0
for i in range(theta1_points):
    for k in range(theta2_points):

        the1[point] = 2*i*np.pi/theta1_points
        the2[point] = 2*k*np.pi/theta2_points #create the values of theta for all points on plot
        point += 1

thetas = np.vstack((the1,the2))
bell_circuits = tomo.build_wigner_circuits(Q_program, 'bell', phis, thetas,
                                      bell_qubits, qr, cr)

backend = 'local_qasm_simulator'
shots = 1024
bell_result = Q_program.execute(bell_circuits, backend=backend, shots=shots)
print(bell_result)
```

    COMPLETED

```python
wdata = tomo.wigner_data(bell_result, bell_qubits,
                        bell_circuits, shots=shots)
wdata = np.matrix(wdata)
wdata = wdata.reshape(theta1_points,theta2_points)
plot_wigner_data(wdata, method='plaquette')
```

## Curve on a Wigner fucntion
We can also take a line around phase space and plot a curve. A useful line to take is the equator of the equal angle slice, below we plot this slice also for the Bell-state. When we have an $n$-qubit maximally entangled state of the form $\left(|0...0\rangle + |1...1\rangle\right)/\sqrt{2}$, the equatorial slice will show sinusoidal behaviour with frequency $n$.

```python
equator_points = 64
theta = [np.pi/2]*equator_points
phi = [0]*equator_points

point = 0
for i in range(equator_points):
    phi[i] = 2*i*np.pi/equator_points

thetas = np.vstack((theta,theta))
phis = np.vstack((phi,phi))
bell_eq_circuits = tomo.build_wigner_circuits(Q_program, 'bell', phis, thetas,
                                      bell_qubits, qr, cr)
bell_eq_result = Q_program.execute(bell_eq_circuits, backend=backend, shots=shots)
wdata_eq = tomo.wigner_data(bell_eq_result,  bell_qubits,
                        bell_eq_circuits, shots=shots)
plot_wigner_data(wdata_eq, method='curve')
```

Recreating the equatorial slice of a five qubit GHZ state, $\left(|00000\rangle - |11111\rangle\right)/\sqrt{2}$ from the paper, the following gates are needed...

```python
Q_program = QuantumProgram()
number_of_qubits = 5
backend = 'local_qasm_simulator'
shots = 1024
ghz_qubits = [0, 1, 2, 3, 4]
qr = Q_program.create_quantum_register('qr',5)
cr = Q_program.create_classical_register('cr',5)
ghz = Q_program.create_circuit('ghz', [qr], [cr])
ghz.h(qr[0])
ghz.h(qr[1])
ghz.x(qr[2])
ghz.h(qr[3])
ghz.h(qr[4])
ghz.cx(qr[0],qr[2])
ghz.cx(qr[1],qr[2])
ghz.cx(qr[3],qr[2])
ghz.cx(qr[4],qr[2])
ghz.h(qr[0])
ghz.h(qr[1])
ghz.h(qr[2])
ghz.h(qr[3])
ghz.h(qr[4])

equator_points = 64
thetas = [[np.pi/2]*equator_points]*number_of_qubits
phi = [0]*equator_points

point = 0
for i in range(equator_points):
    phi[i] = 2*i*np.pi/equator_points
phis = np.vstack((phi,phi,phi,phi,phi))
ghz_eq_circuits = tomo.build_wigner_circuits(Q_program, 'ghz', phis, thetas,
                                      ghz_qubits, qr, cr)
ghz_eq_result = Q_program.execute(ghz_eq_circuits, backend=backend, shots=shots, timeout = 300)
wghzdata_eq = tomo.wigner_data(ghz_eq_result, ghz_qubits,
                        ghz_eq_circuits, shots=shots)
plot_wigner_data(wghzdata_eq, method='curve')
```

Since all the data for the plot is stored within `wghzdata_eq`, we can import matplotlib and go wild with different ways of plotting

```python
import matplotlib.pyplot as plt
plt.plot(phi, wghzdata_eq, 'o')
plt.axis([0, 2*np.pi, -0.6, 0.6])
plt.show()
```

```python
density_matrix = np.zeros((32,32))
density_matrix[0][0] = 0.5
density_matrix[0][31] = -0.5
density_matrix[31][0] = -0.5
density_matrix[31][31] = 0.5
plot_wigner_function(density_matrix, res=200)
```

```python
%run "../version.ipynb"
```

<h2>Version information</h2>
<p>Please note that this tutorial is targeted to the <b>stable</b> version of the QISKit SDK. The following versions of the packages are recommended:</p>
<table>
<tr><th>Package</th><th colspan="2">Version</th></tr>
<tr><td>QISKit</td><td> 0.4.8</td></tr>
<tr><td>IBMQuantumExperience</td><td>>= 1.8.26</td></tr>
<tr><td>numpy</td><td>>= 1.13, < 1.14</td></tr>
<tr><td>scipy</td><td>>= 0.19, < 0.20</td></tr>
<tr><td>matplotlib</td><td>>= 2.0, < 2.1</td></tr>
</table>


# Бэтмен: Полиномы Лежандра

Автор: [Илья Ощепков](https://thegeodesy.com/)

В поисках интересных примеров для разложения в ряд по полиномам Лежандра, мне вспомнилось, что я встречал на просторах сети уравнение, описывающее логотип Бэтмена. И действительно, быстрый поиск привел к нужному результату. Довольно страшненькому, надо сказать, то есть как раз к тому, на котором хорошо показывать, почему аппроксимация порой изящнее исходного уравнения или функции, а также какие трудности при этом возникают.

## Функция Бэтмена

Итак, нас интересует математическая функция, которая описывала бы каким-то образом вот этот логотип Бэтмена, известного всем Тёмного рыцаря и грозы всех преступников Готэма.

Автором уравнения является преподаватель математики J. Matthew Register, о чём он сам сообщает на [Quora](https://www.quora.com/Who-wrote-the-Batman-equation). Но особенно хорошее обсуждение и подробный разбор есть на [StackExchange](https://math.stackexchange.com/questions/54506/is-this-batman-equation-for-real). Всем заинтересованным в самой функции очень рекомендую туда заглянуть. В процессе раздумий, я наткнулся на очень похожий пост о разложении функции Бэтмена в ряд Фурье [здесь](https://www.enib.fr/~choqueuse/articles/signal_processing/batman_fourier.html), откуда многое пришлось позаимствовать. Наконец, сами формулы взяты [отсюда](https://www.desmos.com/calculator/dnzfajfpym).

Делаем сразу все необходимые импорты для наших изысканий.

```python
import numpy as np

import matplotlib.pyplot as plt

from matplotlib import rc
rc('text', usetex=True)

from scipy.special import lpn
from scipy.integrate import quad
```

Функция Бэтмена, или правильнее, наверное, бэтфункция, является [кусочно-заданной](https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%81%D0%BE%D1%87%D0%BD%D0%BE-%D0%B7%D0%B0%D0%B4%D0%B0%D0%BD%D0%BD%D0%B0%D1%8F_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F) и к сожалению для нас не является однозначной, то есть одному значению $x$ соответствует два значения $y$. Но её можно разбить на две функции: одну для верхней части, а вторую для нижней части.

### Верхняя часть

Верхняя часть описывается чётной функцией $f_u$ с областью определения $-7 \leq x \leq 7$:

\begin{equation}
f_u(x)=\begin{cases}
2.25, &\mbox{если } 0\le |x| <0.5,\\
3|x|+0.75,&\mbox{если } 0.5\le |x|<0.75,\\ 9-8|x|,&\mbox{если } 0.75\le |x|<1,\\
1.5-0.5|x|-\dfrac{3\sqrt{10}}{7}\left(\sqrt{3-x^2+2|x|}- 2\right),& \mbox{если } 1\le |x|<3,\\
3\sqrt{1-\left(\frac{x}{7}\right)^2},&\mbox{если } 3\le |x| \le 7.\\
\end{cases}.
\end{equation}

```python
def batman_upper(x):
    x = np.abs(x)
    if x > 7:
        return ValueError(
            'The Baxman equaxion is defined on -7 <= x <= 7')
    elif x < 0.5:
        return 2.25
    elif 0.5 <= x < 0.75:
        return 3 * x + 0.75
    elif 0.75 <= x < 1.0:
        return 9 - 8 * x
    elif 1 <= x < 3:
        return (1.5 - 0.5 * x - 3 * np.sqrt(10) / 7 *
             (np.sqrt(3 - x**2 + 2 * x) - 2))
    elif 3 <= x <= 7:
        return 3 * np.sqrt(-(x / 7)**2 + 1)
```

### Нижняя часть

Нижняя часть логотипа также может быть описана чётной функцией $f_l$ с областью определения $-7 \leq x \leq 7$:

$$
f_l(t)=
\begin{cases}
\left|\dfrac{x}{2}\right|-\left(\dfrac{3 \sqrt{33} - 7}{112}\right) x^2+
\sqrt{1-\left(\left|\left|x\right| - 2\right|-1\right)^2}-3, &\text{если } 0\le |x|<4,\\
-3\sqrt{-\left(\frac{x}{7}\right)^2+1},&\text{если } 4\le |x| \le 7.
\end{cases}.
$$

```python
def batman_lower(x):
    x = np.abs(x)
    if x > 7:
        return ValueError(
            'The Batman equation is defined on -7 <= t <= 7')
    elif 0 <= x < 4:
        return (np.abs(x / 2) - (3*np.sqrt(33) - 7) / 112 * x**2 +
             np.sqrt(1 - (np.abs(x - 2) - 1)**2) - 3)
    elif 4 <= x <= 7 :
        return -3 * np.sqrt(-(x / 7)**2 + 1)
```

### Полная функция и её график

Таким образом, мы получили довольно сложную кусочно-заданную бэтфункцию $f$, состоящую из двух частей:

$$
f \left( x \right) =
\left\{\begin{array}{l}
f_u \left( x \right) \\
f_l \left( x \right)
\end{array}\right\}.
$$

Она, очевидно, является чётной. Область определения и область значений легко установить исходя из того, что в качестве исходного уравнения брался эллипс

$$
\left( \dfrac{x}{7} \right)^{2} + \left( \dfrac{y}{3} \right)^{2} = 1
$$

с большой полуосью $a = 7$ и малой полуосью $b = 3$, то есть весь логотип лежит внутри этого эллипса, поэтому

$$x \in \left[ -7, 7 \right],\quad y \in \left[-3, 3\right].$$

Теперь рисуем полный график бэтфункции. Мы берем шаг $0.01$, при котором мы не потеряем информацию при отображении.

```python
x = np.linspace(-7, 7, 700*2)
y_upper = np.array([batman_upper(xi) for xi in x])
y_lower = np.array([batman_lower(xi) for xi in x])

plt.plot(x, y_upper, 'r', label="$f_u (x)$")
plt.plot(x, y_lower, 'b', label="$f_l (x)$")

plt.legend(loc='upper right', fontsize=14)
```

Выглядит очень правдоподобно. Нам понадобилось $2\times 1400$ точек.

Мы замечаем определённые сложности. Уши летучей мыши, а также её нижняя часть имеют острые углы, а потому сглаживание любой аппроксимирующей функцией потребует намного больше членов, которые понадобятся именно на эти места.

## Разложение бэтфункции в ряд Лежандра

В этом разделе мы попробуем разложить бэтфункцию в ряд Лежандра, а точнее в два ряда, поскольку результирующая функция не является однозначной. Разложение в общем виде можно записать так

$$
f \left( x \right) =
\left\{\begin{array}{l}
f_u \left( x \right) \\
f_l \left( x \right)
\end{array}\right\} =
\sum\limits_{n=0}^{\infty}
\left\{\begin{array}{l}
u_n \\ l_n
\end{array}\right\}
P_n \left( x \right) \approx
\sum\limits_{n=0}^{N}
\left\{\begin{array}{l}
u_n \\ l_n
\end{array}\right\}
P_n \left( x \right),
$$

где $u_n$ и $l_n$ являются коэффициентами при полиноме Лежандра $P_n$ стпени $n$ в разложении верхней и нижней части бэтфункции соответственно, $N$ - максимальная степень разложения, обшая для верхней и нижней части бэтфункции. Очевидно, что число коэффициентов будет равно $2 \times \left(N + 1\right)$.

Коэффициенты разложения можно найти по известному правилу

$$
\left\{\begin{array}{l}
u_n \\ l_n
\end{array}\right\} =
\dfrac{2n + 1}{2} \int\limits_{-1}^{+1} \left\{\begin{array}{l}
f_u \left( x \right) \\
f_l \left( x \right)
\end{array}\right\} P_n \left( x \right) dx,
$$

однако поскольку функции $f_u$ и $f_l$ чётные, то выражение можно немного упростить

$$
\left\{\begin{array}{l}
u_n \\ l_n
\end{array}\right\} =
\left({2n + 1}\right) \int\limits_{0}^{1} \left\{\begin{array}{l}
f_u \left( x \right) \\
f_l \left( x \right)
\end{array}\right\} P_n \left( x \right) dx.
$$

Полиномы Лежандра ортогональны на отрезке $\left[ -1, 1 \right]$, а бэтфункция имеет область определения $\left[ -7, 7 \right]$. Следовательно, для аппроксимации всей кривой, нам нужно нормализовать $x$ в пределах $\left[ -1, 1 \right]$, что можно сделать так

$$
x' = 2 \dfrac{x - \min(x)}{\max(x) - \min(x)} - 1 = \dfrac{x}{7},
$$

где $x'$ теперь лежит в пределах $\left[ -1, 1 \right]$, поэтому

$$
\left\{\begin{array}{l}
u_n \\ l_n
\end{array}\right\} =
\left({2n + 1}\right) \int\limits_{0}^{1} \left\{\begin{array}{l}
f_u \left( 7x' \right) \\
f_l \left( 7x' \right)
\end{array}\right\} P_n \left( x' \right) dx'.
$$

Таким образом, мы будем искать коэффициенты от $\frac{1}{7} x$, что потом нужно будет учесть.

Находить интеграл мы будем численно с помощью функции `scipy.integrate.quad`, а входящие в интеграл полиномы Лежандра получим из функции `scipy.special.lpn`.

```python
def batman2legendre(n_max):
    """Expand Batman function in Legendre series.

    """
    ul = np.zeros((2, n_max + 1))

    func = [batman_upper, batman_lower]

    args = {'a' : 0, 'b' : 1, 'epsabs' : 1e-6,
           'epsrel' : 1e-4, 'limit' : int(1e4)}

    for n in np.arange(0, n_max + 1, 2):
        for i, fi in enumerate(func):
            ul[i, n] = quad(lambda x: fi(7*x) * lpn(n, x)[0][-1], **args)[0]
        ul[:,n] *= (2*n + 1)
    return ul
```

Посмотрим, насколько хорошо работает наша аппроксимация. Для этого нарисуем графики для разных степеней $N$.

```python
fig, axes = plt.subplots(3, 3, figsize=(15, 9),
                         sharey=True, sharex=True)
axes = axes.flatten()

degrees = [5, 10, 15, 20, 30, 40, 50, 100, 150]
for i, n in enumerate(degrees):
    batcoef = np.around(batman2legendre(n),3)
    batleg = np.polynomial.legendre.legval(x / 7, batcoef.T)
    axes[i].plot(x, y_upper, 'b')
    axes[i].plot(x, y_lower, 'b')

    axes[i].plot(x, batleg[0], 'r')
    axes[i].plot(x, batleg[1], 'r')

    axes[i].set_title('N = {}'.format(n), fontsize=14)

plt.tight_layout()
```

Мы наблюдаем, что при степенях $n > 100$ аппроксимация практически совпадает с исходной функцией. Однако, если присмотреться, то всё еще остаются проблемные места, в основном на границах кусков, что легко объяснимо.

Экспериментальным путём можно найти, что при $N = 800$ аппроксимация становится практически безупречной. Это конечно большое число, однако информации становится всё равно меньше. Вместо $2 \times 1400 = 2800$ исходных точек мы получили $\frac{1}{2}(2 \times 802) = 802$ коэффициента. Дробь  $\frac{1}{2}$ возникает из-за того, что у нас только чётные коэффициенты. Надо отметить, что разложение в ряд Фурье, ссылка на который была дана в самом начале, сходится быстрее и можно получить аппроксимацию с меньшим числом коэффициентов.

В целом же, последний абзац является отличным подтверждением выражения: лучшее - враг хорошего. Стоило ли в погоне за совершенством раскладывать функцию до столь высоких степененй или же нам было бы достаточно остановиться на степени 50, 100 или 150? Ответ, скорее всего будет, что не стоило. При $N=150$ получился вполне приемлемый результат.

Полученные коэффициенты можно сохранить, например в текстовый файл и далее использовать повторно, когда нужно будет в очередной раз позвать Бэтмена на помощь.

Что же в итоге мы получили? Нарисуем наш результат по разложению бэтфункции в два ряда по полиномам Лежандра до $N = 800$.

```python
fig = plt.figure(figsize=(10, 6))

n_max = 800

batcoef = np.around(batman2legendre(n_max), 3)
batleg  = np.polynomial.legendre.legval(x / 7, batcoef.T)

plt.fill_between(x, batleg[0], batleg[1], color='k')
plt.axis('off')
```

Можно звать Бэтмена!

batman2legendere/Batman and Legendre plynomials.ipynb


# Diseño del controlador

Como se indicó en el apartado anterior, las ecuaciones de nuestro sistema tienen la siguiente forma:

$$
\begin{align}
    \dot{\mathbf x} &= \mathbf A\mathbf x + \mathbf Bu \\
    \mathbf y &= \mathbf C\mathbf x + \mathbf Du
\end{align}
$$

En la bibliografía sobre Control es frecuente referirse al sistema que queremos controlar, de modo general, como *planta*. Cuando no hay realimentación, el sistema se conoce como *planta en lazo abierto*. Si consideramos el sistema libre, sin introducir ningún *input*, resultará una ecuación diferencial de la forma:

$$
    \dot{\mathbf x} = \mathbf A\mathbf x
$$

Se dice que un sistema es *asintóticamente estable* cuando, para unas condiciones iniciales cualesquiera, el vector de estados $\mathbf x$ tiende a cero cuando el tiempo tiende a infinito. Se puede demostrar que la solución de un sistema de este tipo es estable cuando todos los autovalores de $\mathbf A$ tienen parte real negativa. Como se puede ver, nuestro sistema no es estable, ya que tiene un autovalor positivo:

```python
import numpy as np

# Cargamos las matrices A, B, C, y D del paso anterior
# Aprovechamos para cargar las dimensiones del modelo, para las animaciones
with np.load('./Data/Model.npz') as m:
    A, B, C, D = (m['A'], m['B'], m['C'], m['D'])
    L, R = (5*m['r'], m['r'])

# Comprobamos los autovalores de 'A'
display(np.real(np.linalg.eig(A)[0]))
```

## Estabilización mediante realimentación de estados

La realimentación de estados se basa en definir la entrada del sistema $u$ como una realimentación negativa del estado, de la siguiente forma:

$$
    u = -\mathbf K\mathbf x
$$

Con este tipo de realimentación, se puede determinar de forma analítica una matriz de ganancias $\mathbf K$ que estabilice el sistema, es decir, que haga que todos los autovalores sean negativos. Para verlo más claramente, se puede introducir la realimentación en las ecuaciones del sistema:

$$
    \dot{\mathbf x} = \mathbf{Ax} - \mathbf{BKx} = \left(\mathbf A - \mathbf{BK}\right)\mathbf x
$$

Se observa que, ahora, la estabilidad del sistema la determinan los autovalores de $\mathbf A - \mathbf{BK}$. Esto significa que se puede alterar la dinámica del sistema a conveniencia, ajustando la matriz $\mathbf K$ para colocar los autovalores donde nos interese. Este método de control se puede esquematizar de la forma siguiente:

En la figura se muestra un sistema con una entrada y una salida, es decir, un sistema SISO (*Single Input, Single Output*), pero todo lo aquí desarrollado se puede generalizar fácilemente a sistemas con varias entradas y salidas. Por ahora no vamos a considerar la entrada de referencia $r$.

## Regulador cuadrático lineal (LQR)

El esquema de control de realimentación de estados deja abierta una decisión complicada al diseñador: ¿dónde colocar los autovalores del sistema? Existen diversos métodos descritos en la bibliografía, dedicados a determinar dónde se deben colocar los autovalores para conseguir que la respuesta del sistema presente unas características determinadas. Estas técnicas se denominan *asignación de polos* o *pole placement*.

En nuestro proyecto, vamos a utilizar un método un poco más elaborado, pero a la vez sencillo de utilizar: el LQR (*Linear Quadratic Regulator*). El LQR no es más que otro método para calcular la matriz $\mathbf K$, pero en lugar de tener que decidir dónde colocamos los polos del sistema, lo que hacemos es buscar el valor único de $\mathbf K$ que minimiza la siguiente función:

$$
    J = \int_0^\infty\left(\mathbf x^T\mathbf Q\mathbf x + \mathbf u^T\mathbf R\mathbf u\right)dt
$$

Esta función (expresada en forma general, para un sistema con múltiples *inupts*) combina los valores cuadráticos, integrados a lo largo del tiempo, de las dos magnitudes que queremos minimizar: el error en los estados y la magnitud de las entradas de control. Cada término va ponderado por una matriz de pesos, $\mathbf Q$ para los estados y $\mathbf R$ para los *inputs*, de modo que podemos decidir qué variables queremos penalizar más en el controlador. Por ejemplo, asignar un valor elevado al elemento $Q_{ii}$ de la diagonal de $\mathbf Q$ hará que los errores en el estado $x_i$ sean fuertemente penalizados en la función objetivo $J$, así que el controlador se esforzará en manternerlos bajos. Por el contrario, si se hace lo mismo con el elemento $R_{jj}$ de $\mathbf R$, se indica al controlador que debe utilizar lo menos posible el actuador correspondiente al *input* $u_j$.

Modificando los elementos de las matrices $\mathbf Q$ y $\mathbf R$ se puede ajustar el comportamiento del controlador a nuestros requerimientos, dando mayor relevancia al error en los estados o al esfuerzo de control. Lo primero puede ser importante si en algún estado es crítico mantener errores bajos, y lo segundo, por ejemplo, si es prioritario obtener una alta eficiencia energética, aunque sea al coste de menor precisión en el control.

Una vez determinadas las matrices de pesos, la función `lqr` del módulo de control de Python resuelve el problema de optimización y nos devuelve directamente la matriz de ganancias $\mathbf K$ que minimiza la función $J$:

```python
from control import ss, lqr, ss2tf, minreal, forced_response

# Construimos un sistema state-space con las matrices A, B, C y D
sys = ss(A, B, C, D)

Qc = np.eye(3) # Aproximación incial para 'Q'
Qc[2, 2] = 200 # Ajustamos peso de la velociad 'xp'
Rc = 1         # Peso del voltaje 'V'

K = lqr(sys, Qc, Rc)[0]
```

Para evaluar el comportamiento del regulador, se simula el comportamiento del sistema cuando partimos de una posición fuera de equilibrio, con un ángulo incial $\theta_0$ de 30º. La entrada de referencia $r$ que se ve en la figura se dejará a cero (se explicará más adelante para qué sirve esta entrada). Como ejercicio, es interesante comprobar cómo varía la repuesta al modificar los pesos del LQR:

```python
%%capture
from matplotlib.animation import FuncAnimation
import plotfunctions as pf
```

```python
# Ensamblamos el sistema en lazo cerrado
clsys = ss(A - B*K, B, C, D)

tf = 3.00         # Duración de la simulación en segundos
dt = 0.01         # Paso de tiempo de simulación en segundos

# Simulamos respuesta para ángulo incial de 30 grados
t = np.arange(0, tf, dt)
r = np.zeros(t.shape)

T, Y, X = forced_response(clsys, t, r, X0=[np.radians(30), 0, 0], return_x=True)

pf.plot_response(T, X)
```

Como se ve, el sistema es estable, convergiendo rápidamente tanto $\theta$ como $\dot x$ a cero.

Podemos verificar que, al aplicar la realimentación, todos los autovalores del sistema tienen ahora parte real negativa:

```python
display(np.real(np.linalg.eig(A - B*K)[0]))
```

A continuación se muestra una animación con el movimiento resultante:

```python
FuncAnimation(pf.fg1, pf.anim, init_func=pf.init, frames=len(T), fargs=(X[[0, 2]], dt, R, L), interval=dt*1000, blit=True)
```

# Control de la velocidad

Cuando se aplica realimentación de estados, el controlador resultante siempre tratará de llevarlos todos al equilibrio, y eso es lo que se conoce como *regulador*. Hasta ahora, esto nos ha servido para estabilizar el sistema, manteniéndolo equilibrado en vertical y con velocidad cero. Si lo que queremos es asignar a algún *output* un valor arbitrario, hay que convertir nuestro regulador en un *servomecanismo*. El objetivo ahora es conseguir que alguno de los *outputs* (en nuestro caso la velocidad $\dot x$) siga a un valor de referencia $r$, en lugar de converger hacia cero.

Antes de explicar cómo se consigue esto, es importante introducir un nuevo concepto: el *tipo* de sistema o planta. Por definición, una planta es de tipo $N$ cuando, al representarla como un sistema con realimentación unitaria (como el que se muestra en la figura), el término de grado más bajo del denominador de la función de transferencia $G$ es de grado $N$.

Es decir, si la función de transferencia en lazo abierto $G$ se escribe en la forma:

$$
    G(s) = \frac{K\left(T_as + 1\right)\left(T_bs + 1\right)\cdots\left(T_ms + 1\right)}
    {s^N\left(T_1s + 1\right)\left(T_2s + 1\right)\cdots\left(T_ps + 1\right)}
$$

el tipo de sistema corresponderá al exponente $N$ que aparece en el denominador. El tipo de un sistema determina su error de seguimiento $e$ en régimen permanente para distintos tipos de entrada. Por ejemplo, en un sistema tipo 1, el error tenderá a cero para una entrada escalón, a un valor finito para una entrada rampa, y a infinito para una entrada cuadrática o de orden superior. En cambio, un sistema tipo 0 ya mostrará un error finito en régimen permanente para una entrada escalón, y será inestable frente a una entrada rampa o superior.

Para saber el tipo de nuestro sistema, primero tenemos que representarlo según la estructura de la figura. La función de transferencia $H$ del sistema completo, entre la entrada $r$ y la salida $y$, se puede obtener sustituyendo el error de seguimiento $e$ por su valor $r - y$:

$$
    y = G\left(r - y\right) \implies H(s) = \frac{y}{r} = \frac{G}{1 + G}
$$

Entonces, se puede hacer el cambio inverso para expresar cualquier sistema con función de transferencia $H$ como sistema con realimentación unitaria:

$$
    G(s) = \frac{H}{1 - H}
$$

Comprobamos el tipo de nuestro sistema, incluyendo la realimentación, cuando consideramos que nuestro único *output* es la velocidad. La función `ss2tf` devuelve la función de transferencia $H$ de un sistema *state-space*, y `minreal` (*minimal realization*) cancela los factores comunes del numerador y el denominador:

```python
# Matrices de salida si el output es sólo la velocidad
Cc = C[1:]
Dc = D[1:]

# Función de transferencia del sistema con realimentación
H = ss2tf(ss(A - B*K, B, Cc, Dc))

# Calculamos G para transformar en sistema con realimentación unitaria
minreal(H/(1 - H))
```

Mirando el denominador, se ve claramente que nuestro sistema realimentado es de tipo 0. Eso significa que, para una entrada escalón, la salida $\dot x$ nunca alcanzará el valor de referencia $r$ en régimen permanente. Si queremos controlar la velocidad correctamente, tendremos que convertir el sistema en uno de tipo 1.

## Servomecanismo tipo 1 para controlar la velocidad

Existen varios métodos para convertir un sistema de tipo 0 en uno de tipo 1. Lo más sencillo es escalar la entrada de referencia $r$ mediante una ganancia de precompensación $\bar N$, que haga que el *output* que queremos controlar (en nuestro caso la velocidad $\dot x$) alcance el valor solicitado en régimen permanente. En la figura se muestra un diagrama del sistema original, con la precompensación aplicada en la entrada:

Al añadir la precompensación, la entrada del sistema en lazo abierto pasa a ser:

$$
    u = -\mathbf{Kx} + \bar Nr
$$

Vamos a estudiar lo que ocurre con $\mathbf x$ y $u$ cuando tenemos una entrada de referencia $r$ de tipo escalón unitario. Supondremos que, en régimen permanente, la salida $y$ alcanza efectivamente el valor de referencia (es decir, la unidad). Como en régimen estacionario las derivadas de los estados $\dot{\mathbf x}$ serán nulas, podemos escribir:

$$
    \begin{bmatrix} \mathbf 0 \\ 1 \end{bmatrix} =
    \begin{bmatrix} \mathbf A && \mathbf B \\ \mathbf C && D \end{bmatrix}
    \begin{bmatrix} \mathbf{x_\infty} \\ u_\infty \end{bmatrix}
$$

Si la matriz de este sistema lineal es invertible, resolviéndolo obtendremos el valor de $\mathbf x_\infty$ y $u_\infty$. Como la referencia $r$ es un escalón unitario, la entrada del sistema en lazo abierto alcanzará el siguiente valor:

$$
    u_\infty = -\mathbf{Kx}_\infty + \bar N
$$

de donde obtenemos $\bar N$ como:

$$
    \bar N = \mathbf{Kx}_\infty + u_\infty
$$

```python
# Precompensación para salida 'xp' en régimen permanente
xuinf = np.linalg.solve(np.block([[A, B], [Cc, Dc]]), [0, 0, 0, 1])
Nb = K@xuinf[:-1] + xuinf[-1]
```

Antes de simular la respuesta, podemos montar el sistema en lazo cerrado con precompensación en la entrada, y comprobar su tipo:

```python
# Sistema en lazo cerrado, incluyendo la precompensación
clsys = ss(A - B*K, B*Nb, Cc, Dc)

# Función de transferencia del sistema con realimentación
H = ss2tf(clsys)

# Calculamos G(s) para sistema con realimentación unitaria
minreal(H/(1 - H))
```

Vemos que el sistema se puede considerar prácticamente de tipo 1, ya que el término de grado 0 del denominador es muy pequeño.

Para evaluar el comportamiento del controlador, se simulará su respuesta a una entrada escalón de 0.3 m/s seguida de una parada:

```python
# Simulamos respuesta a escalón de 0.3 m/s con parada
t = np.arange(0, tf, dt)
r = np.zeros(len(t))
r[t < 1.5] = 0.3

T, Y, X = forced_response(clsys, t, r, return_x=True)

pf.plot_response(T, X)
```

Se observa que el sistema alcanza efectivamente la velocidad objetivo y luego se para, manteniendo siempre la barra en equilibrio.

Si este controlador se va a implementar en el robot real, hay que verificar que el voltaje no supere los 6V:

```python
pf.plot_voltage(T, (Nb*r - K@X).T)
```

Vemos que el sistema puede realizar la maniobra solicitada sin saturar el motor. En caso contrario, habría que variar los pesos del LQR, o realizar una maniobra más suave. A continuación se muestra una animación del movimiento obtenido:

```python
FuncAnimation(pf.fg1, pf.anim, init_func=pf.init, frames=len(T), fargs=(X[[0, 2]], dt, R, L), interval=dt*1000, blit=True)
```

Los parámetros obtenidos ya sirven para controlar el robot en velocidad. Los valores mostrados más abajo se pueden copiar y pegar directamente en un *sketch* de Arduino, de forma que, para implementar el controlador, sólo habría que calcular el voltaje como $V = -\mathbf{Kx} + \bar Nr$.

```python
print('const float K1 =%10.6f;  // Ganancia theta'    % K[0, 0])
print('const float K2 =%10.6f;  // Ganancia thetap'   % K[0, 1])
print('const float K3 =%10.6f;  // Ganancia xp'       % K[0, 2])
print('const float Nb =%10.6f;  // Precompensación'   % Nb)
```

Una de las principales desventajas de los métodos de realimentación de estados es que, para calcular $u$, necesitamos conocer el valor de todos los estados. Esto es un problema, porque normalmente no todos los estados se pueden medir fácilmente con precisión. En estos casos, lo que se hace es utilizar un *observador*, que consiste en un modelo del sistema que se va integrando en el tiempo, corrigiendo los errores de integración a partir de los datos de los sensores.

En este proyecto hay dos estados, $\dot\theta$ y $\dot x$, que son muy fáciles de medir, ya que se obtienen a partir del giróscopo y los *encoders*, pero el ángulo $\theta$ no se puede medir directamente con los sensores que tenemos. Como el uso de observadores embebidos en el controlador queda fuera del ámbito de este proyecto, estimaremos $\theta$ utilizando un filtro de Kalman simplificado, como se explica en el [documento correspondiente](./Kalman.ipynb).

## Servomecanismo tipo 1 con integrador en la entrada

El problema de usar precompensación es que la respuesta es muy sensible a errores en los parámetros del sistema. Cualquier desviación en el valor de $\bar N$ hará que el sistema vuelva a ser de tipo 0, y por lo tanto tenga error en régimen permanente. La manera de resolver esto de forma más robusta es añadir a la entrada un integrador, en lugar de una simple ganancia:

Para utilizar este esquema de control, hay que aumentar el sistema añadiéndole un estado adicional $\xi$, que será la integral del error de seguimiento $r - y$. Por lo tanto, su derivada es:

$$
    \dot\xi = r - y = r - \left(\mathbf{Cx} + Du\right)
$$

y la entrada $u$ pasa a ser ahora:

$$
    u = -\mathbf K \mathbf x + k_I\xi
$$

La ecuación completa de la dinámica del sistema aumentado, sin incluir la realimentación, se puede escribir como:

$$
    \begin{bmatrix} \dot{\mathbf x} \\ \dot\xi \end{bmatrix} =
    \begin{bmatrix} \mathbf A && \mathbf 0 \\ -\mathbf C && 0 \end{bmatrix}
    \begin{bmatrix} \mathbf{x} \\ \xi\end{bmatrix} +
    \begin{bmatrix} \mathbf B \\ -D \end{bmatrix}u +
    \begin{bmatrix} \mathbf 0 \\ 1 \end{bmatrix}r
$$

Estudiaremos la respuesta de este sistema a una entrada escalón, cuando el tiempo tiende a infinito. Para ello, definiremos las siguientes magnitudes:

$$
    \mathbf e = \begin{bmatrix} \mathbf{x} - \mathbf{x}_\infty \\ \xi - \xi_\infty \end{bmatrix} \qquad
    u_e = u - u_\infty
$$

que representan las desviaciones del estado y la entrada respecto sus propios valores en régimen permanente. Como $r$ es un escalón, su valor es siempre el mismo para $t>0$, de forma que podemos escribir la dinámica del error como:

$$
    \dot{\mathbf e} =
    \begin{bmatrix} \mathbf A && \mathbf 0 \\ -\mathbf C && 0 \end{bmatrix}
    \mathbf e +
    \begin{bmatrix} \mathbf B \\ -D \end{bmatrix}u_e = \hat{\mathbf A}\mathbf e + \hat{\mathbf B}u_e
$$

Se puede observar que, de forma análoga a lo que ocurría al principio, tenemos un sistema en forma *state-space* cuyos estados tienen que converger asintóticamente a cero. Eso quiere decir que existirán unas ganancias de realimentación $\hat{\mathbf K}$ que estabilicen el sistema:

$$
    \hat{\mathbf K} = \begin{bmatrix} \mathbf K && -k_I \end{bmatrix}
$$

Estas ganancias se pueden calcular, como antes, utilizando el algoritmo LQR, sabiendo que ahora tenemos cuatro estados, ya que el último corresponde a la integral del error de seguimiento en la velocidad:

```python
# Ecuaciones del sistema con los errores como estados
# Se añade la integral del error en velocidad como cuarto estado
Ah = np.block([[A, np.zeros((3, 1))], [-Cc, 0]])
Bh = np.block([[B], [-Dc]])

# Controlador LQR para la dinámica del error
Qc = np.eye(4)   # Aproximación inicial para 'Q'
Qc[0, 0] = 0.1   # Ajustamos peso del ángulo 'th'
Qc[1, 1] = 0.01  # Ajustamos peso de la derivada del ángulo 'thp'
Qc[2, 2] = 2     # Ajustamos peso de la velocidad 'xp'
Qc[3, 3] = 500   # Peso de la integral del error en velocidad 'xi'
Rc = 1           # Peso del voltaje 'V'

# La función 'LQR' también admite usar sólo las matrices A y B
Kh = lqr(Ah, Bh, Qc, Rc)[0]
Kn = Kh[0, :-1]
Ki = -Kh[0, -1]
```

Para simular el sistema, utilizaremos un modelo aumentado de la siguiente forma, que se obtiene al sustituir en la ecuación del sistema el valor de $u$:

$$
    \begin{bmatrix} \dot{\mathbf x} \\ \dot\xi \end{bmatrix} =
    \begin{bmatrix} \mathbf A - \mathbf{BK} && \mathbf Bk_I \\ D\mathbf K - \mathbf C && -Dk_I \end{bmatrix}
    \begin{bmatrix} \mathbf{x} \\ \xi\end{bmatrix} +
    \begin{bmatrix} \mathbf 0 \\ 1 \end{bmatrix}r
$$

Antes de simular, verificamos que este sistema es de tipo 1:

```python
# Sistema en lazo cerrado con las ganancias obtenidas
An = np.block([[A - B*Kn, B*Ki], [Dc*Kn - Cc, -Dc*Ki]])
Bn = [[0], [0], [0], [1]]
Cn = [0, 0, 1, 0]
Dn = 0

clisys = ss(An, Bn, Cn, Dn)

# Función de transferencia del sistema con realimentación
H = ss2tf(clisys)

# Calculamos G(s) para sistema con realimentación unitaria
minreal(H/(1 - H))
```

Para comprobar que este método es mucho más robusto que la precompensación, se puede hacer una prueba sencilla. En el modelo con precompensación, si antes de ensamblar el sistema *state-space* se simula un error en los datos del modelo multiplicando $\bar N$ por un factor, por ejemplo 0.9, se verá que el sistema pasa a ser tipo 0, incluso con valores del factor muy cercanos a la unidad. En cambio, si se hace lo mismo con $k_I$, se observa que el término de grado 0 del denominador se mantiene cercano a cero para cualquier valor de $k_I$.

A continuación se muestra gráficamente la respuesta, comparándola con la obtenida usando precompensación (en línea de puntos). Igual que antes, es interesante probar diferentes pesos en el LQR, para ver cómo varía el comportamiento del sistema.

```python
# Respuesta con misma entrada que en el modelo con precompensación
T, Y, Xn = forced_response(clisys, t, r, return_x=True)

pf.plot_response(T, Xn, X)
```

Antes de implementar el controlador, hay que asegurarse de que no estamos saturando el actuador:

```python
pf.plot_voltage(T, (Ki*Xn[3, :] - Kn@Xn[:3, :]).T)
```

Vemos que los voltajes se mantienen por debajo de los 6V. Por último, visualizamos el movimiento:

```python
FuncAnimation(pf.fg1, pf.anim, init_func=pf.init, frames=len(T), fargs=(Xn[[0, 2]], dt, R, L), interval=dt*1000, blit=True)
```

Como todo es correcto, ya podemos utilizar las ganancias para la implementación física del controlador. Se pueden copiar y pegar directamente en un sketch de Arduino, siendo ahora el voltaje $V= -\mathbf{Kx} + k_I\xi$. Esto significa que, además de los estados anteriores, habrá que ir calculando la integral del error en velocidad.

```python
print('const float K1 =%10.6f;  // Ganancia theta'    % Kn[0])
print('const float K2 =%10.6f;  // Ganancia thetap'   % Kn[1])
print('const float K3 =%10.6f;  // Ganancia xp'       % Kn[2])
print('const float Ki =%10.6f;  // Ganancia integral' % Ki)
```


# Normalizing Flows - Introduction (Part 1)

This tutorial introduces Pyro's normalizing flow library. It is independent of much of Pyro, but users may want to read about distribution shapes in the [Tensor Shapes Tutorial](http://pyro.ai/examples/tensor_shapes.html).

## Introduction

In standard probabilistic modeling practice, we represent our beliefs over unknown continuous quantities with simple parametric distributions like the normal, exponential, and Laplacian distributions. However, using such simple forms, which are commonly symmetric and unimodal (or have a fixed number of modes when we take a mixture of them), restricts the performance and flexibility of our methods. For instance, standard variational inference in the Variational Autoencoder uses independent univariate normal distributions to represent the variational family. The true posterior is neither independent nor normally distributed, which results in suboptimal inference and simplifies the model that is learnt. In other scenarios, we are likewise restricted by not being able to model multimodal distributions and heavy or light tails.

Normalizing Flows \[1-4\] are a family of methods for constructing flexible learnable probability distributions, often with neural networks, which allow us to surpass the limitations of simple parametric forms. Pyro contains state-of-the-art normalizing flow implementations, and this tutorial explains how you can use this library for learning complex models and performing flexible variational inference. We introduce the main idea of Normalizing Flows (NFs) and demonstrate learning simple univariate distributions with element-wise, multivariate, and conditional flows.

## Univariate Distributions

### Background

Normalizing Flows are a family of methods for constructing flexible distributions. Let's first restrict our attention to representing univariate distributions. The basic idea is that a simple source of noise, for example a variable with a standard normal distribution, $X\sim\mathcal{N}(0,1)$, is passed through a bijective (i.e. invertible) function, $g(\cdot)$ to produce a more complex transformed variable $Y=g(X)$.

For a given random variable, we typically want to perform two operations: sampling and scoring. Sampling $Y$ is trivial. First, we sample $X=x$, then calculate $y=g(x)$. Scoring $Y$, or rather, evaluating the log-density $\log(p_Y(y))$, is more involved. How does the density of $Y$ relate to the density of $X$? We can use the substitution rule of integral calculus to answer this. Suppose we want to evaluate the expectation of some function of $X$. Then,

\begin{align}
\mathbb{E}_{p_X(\cdot)}\left[f(X)\right] &= \int_{\text{supp}(X)}f(x)p_X(x)dx\\
&= \int_{\text{supp}(Y)}f(g^{-1}(y))p_X(g^{-1}(y))\left|\frac{dx}{dy}\right|dy\\
&= \mathbb{E}_{p_Y(\cdot)}\left[f(g^{-1}(Y))\right],
\end{align}

where $\text{supp}(X)$ denotes the support of $X$, which in this case is $(-\infty,\infty)$. Crucially, we used the fact that $g$ is bijective to apply the substitution rule in going from the first to the second line. Equating the last two lines we get,

\begin{align}
\log(p_Y(y)) &= \log(p_X(g^{-1}(y)))+\log\left(\left|\frac{dx}{dy}\right|\right)\\
&= \log(p_X(g^{-1}(y)))-\log\left(\left|\frac{dy}{dx}\right|\right).
\end{align}

Inituitively, this equation says that the density of $Y$ is equal to the density at the corresponding point in $X$ plus a term that corrects for the warp in volume around an infinitesimally small length around $Y$ caused by the transformation.

If $g$ is cleverly constructed (and we will see several examples shortly), we can produce distributions that are more complex than standard normal noise and yet have easy sampling and computationally tractable scoring. Moreover, we can compose such bijective transformations to produce even more complex distributions. By an inductive argument, if we have $L$ transforms $g_{(0)}, g_{(1)},\ldots,g_{(L-1)}$, then the log-density of the transformed variable $Y=(g_{(0)}\circ g_{(1)}\circ\cdots\circ g_{(L-1)})(X)$ is

\begin{align}
\log(p_Y(y)) &= \log\left(p_X\left(\left(g_{(L-1)}^{-1}\circ\cdots\circ g_{(0)}^{-1}\right)\left(y\right)\right)\right)+\sum^{L-1}_{l=0}\log\left(\left|\frac{dg^{-1}_{(l)}(y_{(l)})}{dy'}\right|\right),
%\left( g^{(l)}(y^{(l)})
%\right).
\end{align}

where we've defined $y_{(0)}=x$, $y_{(L-1)}=y$ for convenience of notation.

In a latter section, we will see how to generalize this method to multivariate $X$. The field of Normalizing Flows aims to construct such $g$ for multivariate $X$ to transform simple i.i.d. standard normal noise into complex, learnable, high-dimensional distributions. The methods have been applied to such diverse applications as image modeling, text-to-speech, unsupervised language induction, data compression, and modeling molecular structures. As probability distributions are the most fundamental component of probabilistic modeling we will likely see many more exciting state-of-the-art applications in the near future.

### Fixed Univariate Transforms in Pyro

PyTorch contains classes for representing *fixed* univariate bijective transformations, and sampling/scoring from transformed distributions derived from these. Pyro extends this with a comprehensive library of *learnable* univariate and multivariate transformations using the latest developments in the field. As Pyro imports all of PyTorch's distributions and transformations, we will work solely with Pyro. We also note that the NF components in Pyro can be used independently of the probabilistic programming functionality of Pyro, which is what we will be doing in the first two tutorials.

Let us begin by showing how to represent and manipulate a simple transformed distribution,

\begin{align}
X &\sim \mathcal{N}(0,1)\\
Y &= \text{exp}(X).
\end{align}

You may have recognized that this is by definition, $Y\sim\text{LogNormal}(0,1)$.

We begin by importing the relevant libraries:

```python
import torch
import pyro
import pyro.distributions as dist
import pyro.distributions.transforms as T
import matplotlib.pyplot as plt
import seaborn as sns
import os
smoke_test = ('CI' in os.environ)
```

A variety of bijective transformations live in the [pyro.distributions.transforms](http://docs.pyro.ai/en/stable/distributions.html#transforms) module, and the classes to define transformed distributions live in [pyro.distributions](http://docs.pyro.ai/en/stable/distributions.html). We first create the base distribution of $X$ and the class encapsulating the transform $\text{exp}(\cdot)$:

```python
dist_x = dist.Normal(torch.zeros(1), torch.ones(1))
exp_transform = T.ExpTransform()
```

The class [ExpTransform](https://pytorch.org/docs/master/distributions.html#torch.distributions.transforms.ExpTransform) derives from [Transform](https://pytorch.org/docs/master/distributions.html#torch.distributions.transforms.Transform) and defines the forward, inverse, and log-absolute-derivative operations for this transform,

\begin{align}
g(x) &= \text{exp(x)}\\
g^{-1}(y) &= \log(y)\\
\log\left(\left|\frac{dg}{dx}\right|\right) &= y.
\end{align}

In general, a transform class defines these three operations, from which it is sufficient to perform sampling and scoring.

The class [TransformedDistribution](https://pytorch.org/docs/master/distributions.html#torch.distributions.transformed_distribution.TransformedDistribution) takes a base distribution of simple noise and a list of transforms, and encapsulates the distribution formed by applying these transformations in sequence. We use it as:

```python
dist_y = dist.TransformedDistribution(dist_x, [exp_transform])
```

Now, plotting samples from both to verify that we that have produced the log-normal distribution:

```python
plt.subplot(1, 2, 1)
plt.hist(dist_x.sample([1000]).numpy(), bins=50)
plt.title('Standard Normal')
plt.subplot(1, 2, 2)
plt.hist(dist_y.sample([1000]).numpy(), bins=50)
plt.title('Standard Log-Normal')
plt.show()
```

Our example uses a single transform. However, we can compose transforms to produce more expressive distributions. For instance, if we apply an affine transformation we can produce the general log-normal distribution,

\begin{align}
X &\sim \mathcal{N}(0,1)\\
Y &= \text{exp}(\mu+\sigma X).
\end{align}

or rather, $Y\sim\text{LogNormal}(\mu,\sigma^2)$. In Pyro this is accomplished, e.g. for $\mu=3, \sigma=0.5$, as follows:

```python
dist_x = dist.Normal(torch.zeros(1), torch.ones(1))
affine_transform = T.AffineTransform(loc=3, scale=0.5)
exp_transform = T.ExpTransform()
dist_y = dist.TransformedDistribution(dist_x, [affine_transform, exp_transform])

plt.subplot(1, 2, 1)
plt.hist(dist_x.sample([1000]).numpy(), bins=50)
plt.title('Standard Normal')
plt.subplot(1, 2, 2)
plt.hist(dist_y.sample([1000]).numpy(), bins=50)
plt.title('Log-Normal')
plt.show()
```

For the forward operation, transformations are applied in the order of the list that is the second argument to [TransformedDistribution](https://pytorch.org/docs/master/distributions.html#torch.distributions.transformed_distribution.TransformedDistribution). In this case, first [AffineTransform](https://pytorch.org/docs/master/distributions.html#torch.distributions.transforms.AffineTransform) is applied to the base distribution and then [ExpTransform](https://pytorch.org/docs/master/distributions.html#torch.distributions.transforms.ExpTransform).

### Learnable Univariate Distributions in Pyro

Having introduced the interface for invertible transforms and transformed distributions, we now show how to represent *learnable* transforms and use them for density estimation. Our dataset in this section and the next will comprise samples along two concentric circles. Examining the joint and marginal distributions:

```python
import numpy as np
from sklearn import datasets
from sklearn.preprocessing import StandardScaler

n_samples = 1000
X, y = datasets.make_circles(n_samples=n_samples, factor=0.5, noise=0.05)
X = StandardScaler().fit_transform(X)

plt.title(r'Samples from $p(x_1,x_2)$')
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$')
plt.scatter(X[:,0], X[:,1], alpha=0.5)
plt.show()

plt.subplot(1, 2, 1)
sns.distplot(X[:,0], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2})
plt.title(r'$p(x_1)$')
plt.subplot(1, 2, 2)
sns.distplot(X[:,1], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2})
plt.title(r'$p(x_2)$')
plt.show()
```

Standard transforms derive from the [Transform](https://pytorch.org/docs/master/distributions.html#torch.distributions.transforms.ExpTransform) class and are not designed to contain learnable parameters. Learnable transforms, on the other hand, derive from [TransformModule](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.TransformModule), which is a [torch.nn.Module](https://pytorch.org/docs/master/generated/torch.nn.Module.html#torch.nn.Module) and registers parameters with the object.

We will learn the marginals of the above distribution using such a transform, [Spline](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.transforms.Spline) \[5,6\], defined on a two-dimensional input:

```python
base_dist = dist.Normal(torch.zeros(2), torch.ones(2))
spline_transform = T.Spline(2, count_bins=16)
flow_dist = dist.TransformedDistribution(base_dist, [spline_transform])
```

This transform passes each dimension of its input through a *separate* monotonically increasing function known as a spline. From a high-level, a spline is a complex parametrizable curve for which we can define specific points known as knots that it passes through and the derivatives at the knots. The knots and their derivatives are parameters that can be learnt, e.g., through stochastic gradient descent on a maximum likelihood objective, as we now demonstrate:

```python
%%time
steps = 1 if smoke_test else 1001
dataset = torch.tensor(X, dtype=torch.float)
optimizer = torch.optim.Adam(spline_transform.parameters(), lr=1e-2)
for step in range(steps):
    optimizer.zero_grad()
    loss = -flow_dist.log_prob(dataset).mean()
    loss.backward()
    optimizer.step()
    flow_dist.clear_cache()

    if step % 200 == 0:
        print('step: {}, loss: {}'.format(step, loss.item()))
```

    step: 0, loss: 2.682476758956909
    step: 200, loss: 1.278384804725647
    step: 400, loss: 1.2647961378097534
    step: 600, loss: 1.2601449489593506
    step: 800, loss: 1.2561875581741333
    step: 1000, loss: 1.2545257806777954
    CPU times: user 4.92 s, sys: 69.3 ms, total: 4.99 s
    Wall time: 5.01 s

Note that we call `flow_dist.clear_cache()` after each optimization step to clear the transform's forward-inverse cache. This is required because `flow_dist`'s `spline_transform` is a stateful [TransformModule](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.TransformModule) rather than a purely stateless [Transform](https://pytorch.org/docs/stable/distributions.html#torch.distributions.transforms.Transform) object. Purely functional Pyro code typically creates `Transform` objects each model execution, then discards them after `.backward()`, effectively clearing the transform caches. By contrast in this tutorial we create stateful module objects and need to manually clear their cache after update.

Plotting samples drawn from the transformed distribution after learning:

```python
X_flow = flow_dist.sample(torch.Size([1000,])).detach().numpy()
plt.title(r'Joint Distribution')
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$')
plt.scatter(X[:,0], X[:,1], label='data', alpha=0.5)
plt.scatter(X_flow[:,0], X_flow[:,1], color='firebrick', label='flow', alpha=0.5)
plt.legend()
plt.show()

plt.subplot(1, 2, 1)
sns.distplot(X[:,0], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='data')
sns.distplot(X_flow[:,0], hist=False, kde=True,
             bins=None, color='firebrick',
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='flow')
plt.title(r'$p(x_1)$')
plt.subplot(1, 2, 2)
sns.distplot(X[:,1], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='data')
sns.distplot(X_flow[:,1], hist=False, kde=True,
             bins=None, color='firebrick',
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='flow')
plt.title(r'$p(x_2)$')
plt.show()
```

As we can see, we have learnt close approximations to the marginal distributions, $p(x_1),p(x_2)$. It would have been challenging to fit the irregularly shaped marginals with standard methods, e.g., a mixture of normal distributions. As expected, since there is a dependency between the two dimensions, we do not learn a good representation of the joint, $p(x_1,x_2)$. In the next section, we explain how to learn multivariate distributions whose dimensions are not independent.

## Multivariate Distributions

### Background

The fundamental idea of normalizing flows also applies to multivariate random variables, and this is where its value is clearly seen - *representing complex high-dimensional distributions*. In this case, a simple multivariate source of noise, for example a standard i.i.d. normal distribution, $X\sim\mathcal{N}(\mathbf{0},I_{D\times D})$, is passed through a vector-valued bijection, $g:\mathbb{R}^D\rightarrow\mathbb{R}^D$, to produce the more complex transformed variable $Y=g(X)$.

Sampling $Y$ is again trivial and involves evaluation of the forward pass of $g$. We can score $Y$ using the multivariate substitution rule of integral calculus,

\begin{align}
\mathbb{E}_{p_X(\cdot)}\left[f(X)\right] &= \int_{\text{supp}(X)}f(\mathbf{x})p_X(\mathbf{x})d\mathbf{x}\\
&= \int_{\text{supp}(Y)}f(g^{-1}(\mathbf{y}))p_X(g^{-1}(\mathbf{y}))\det\left|\frac{d\mathbf{x}}{d\mathbf{y}}\right|d\mathbf{y}\\
&= \mathbb{E}_{p_Y(\cdot)}\left[f(g^{-1}(Y))\right],
\end{align}

where $d\mathbf{x}/d\mathbf{y}$ denotes the Jacobian matrix of $g^{-1}(\mathbf{y})$. Equating the last two lines we get,

\begin{align}
\log(p_Y(y)) &= \log(p_X(g^{-1}(y)))+\log\left(\det\left|\frac{d\mathbf{x}}{d\mathbf{y}}\right|\right)\\
&= \log(p_X(g^{-1}(y)))-\log\left(\det\left|\frac{d\mathbf{y}}{d\mathbf{x}}\right|\right).
\end{align}

Inituitively, this equation says that the density of $Y$ is equal to the density at the corresponding point in $X$ plus a term that corrects for the warp in volume around an infinitesimally small volume around $Y$ caused by the transformation. For instance, in $2$-dimensions, the geometric interpretation of the absolute value of the determinant of a Jacobian is that it represents the area of a parallelogram with edges defined by the columns of the Jacobian. In $n$-dimensions, the geometric interpretation of the absolute value of the determinant Jacobian is that is represents the hyper-volume of a parallelepiped with $n$ edges defined by the columns of the Jacobian (see a calculus reference such as \[7\] for more details).

Similar to the univariate case, we can compose such bijective transformations to produce even more complex distributions. By an inductive argument, if we have $L$ transforms $g_{(0)}, g_{(1)},\ldots,g_{(L-1)}$, then the log-density of the transformed variable $Y=(g_{(0)}\circ g_{(1)}\circ\cdots\circ g_{(L-1)})(X)$ is

\begin{align}
\log(p_Y(y)) &= \log\left(p_X\left(\left(g_{(L-1)}^{-1}\circ\cdots\circ g_{(0)}^{-1}\right)\left(y\right)\right)\right)+\sum^{L-1}_{l=0}\log\left(\left|\frac{dg^{-1}_{(l)}(y_{(l)})}{dy'}\right|\right),
%\left( g^{(l)}(y^{(l)})
%\right).
\end{align}

where we've defined $y_{(0)}=x$, $y_{(L-1)}=y$ for convenience of notation.

The main challenge is in designing parametrizable multivariate bijections that have closed form expressions for both $g$ and $g^{-1}$, a tractable Jacobian whose calculation scales with $O(D)$ rather than $O(D^3)$, and can express a flexible class of functions.

### Multivariate Transforms in Pyro

Up to this point we have used element-wise transforms in Pyro. These are indicated by having the property `transform.event_dim == 0` set on the transform object. Such element-wise transforms can only be used to represent univariate distributions and multivariate distributions whose dimensions are independent (known in variational inference as the mean-field approximation).

The power of Normalizing Flow, however, is most apparent in their ability to model complex high-dimensional distributions with neural networks and Pyro contains several such flows for accomplishing this. Transforms that operate on vectors have the property `transform.event_dim == 1`, transforms on matrices with `transform.event_dim == 2`, and so on. In general, the `event_dim` property of a transform indicates how many dependent dimensions there are in the output of a transform.

In this section, we show how to use [SplineCoupling](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.transforms.SplineCoupling) to learn the bivariate toy distribution from our running example. A coupling transform \[8, 9\] divides the input variable into two parts and applies an element-wise bijection to the section half whose parameters are a function of the first. Optionally, an element-wise bijection is also applied to the first half. Dividing the inputs at $d$, the transform is,

\begin{align}
\mathbf{y}_{1:d} &= g_\theta(\mathbf{x}_{1:d})\\
\mathbf{y}_{(d+1):D} &= h_\phi(\mathbf{x}_{(d+1):D};\mathbf{x}_{1:d}),
\end{align}

where $\mathbf{x}_{1:d}$ represents the first $d$ elements of the inputs, $g_\theta$ is either the identity function or an elementwise bijection parameters $\theta$, and $h_\phi$ is an element-wise bijection whose parameters are a function of $\mathbf{x}_{1:d}$.

This type of transform is easily invertible. We invert the first half, $\mathbf{y}_{1:d}$, then use the resulting $\mathbf{x}_{1:d}$ to evaluate $\phi$ and invert the second half,

\begin{align}
\mathbf{x}_{1:d} &= g_\theta^{-1}(\mathbf{y}_{1:d})\\
\mathbf{x}_{(d+1):D} &= h_\phi^{-1}(\mathbf{y}_{(d+1):D};\mathbf{x}_{1:d}).
\end{align}

Difference choices for $g$ and $h$ form different types of coupling transforms. When both are monotonic rational splines, the transform is the spline coupling layer of Neural Spline Flow \[5,6\], which is represented in Pyro by the [SplineCoupling](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.transforms.SplineCoupling) class. As shown in the references, when we combine a sequence of coupling layers sandwiched between random permutations so we introduce dependencies between all dimensions, we can model complex multivariate distributions.

Most of the learnable transforms in Pyro have a corresponding helper function that takes care of constructing a neural network for the transform with the correct output shape. This neural network outputs the parameters of the transform and is known as a [hypernetwork](https://arxiv.org/abs/1609.09106) \[10\]. The helper functions are represented by lower-case versions of the corresponding class name, and usually input at the very least the input-dimension or shape of the distribution to model. For instance, the helper function corresponding to [SplineCoupling](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.transforms.SplineCoupling) is [spline_coupling](http://docs.pyro.ai/en/stable/distributions.html#spline-coupling).  We create a bivariate flow with a single spline coupling layer as follows:

```python
base_dist = dist.Normal(torch.zeros(2), torch.ones(2))
spline_transform = T.spline_coupling(2, count_bins=16)
flow_dist = dist.TransformedDistribution(base_dist, [spline_transform])
```

Similarly to before, we train this distribution on the toy dataset and plot the results:

```python
%%time
steps = 1 if smoke_test else 5001
dataset = torch.tensor(X, dtype=torch.float)
optimizer = torch.optim.Adam(spline_transform.parameters(), lr=5e-3)
for step in range(steps+1):
    optimizer.zero_grad()
    loss = -flow_dist.log_prob(dataset).mean()
    loss.backward()
    optimizer.step()
    flow_dist.clear_cache()

    if step % 500 == 0:
        print('step: {}, loss: {}'.format(step, loss.item()))
```

    step: 0, loss: 8.446191787719727
    step: 500, loss: 2.0197808742523193
    step: 1000, loss: 1.794958472251892
    step: 1500, loss: 1.73616361618042
    step: 2000, loss: 1.7254879474639893
    step: 2500, loss: 1.691617488861084
    step: 3000, loss: 1.679549217224121
    step: 3500, loss: 1.6967085599899292
    step: 4000, loss: 1.6723777055740356
    step: 4500, loss: 1.6505967378616333
    step: 5000, loss: 1.8024061918258667
    CPU times: user 10min 41s, sys: 14.7 s, total: 10min 56s
    Wall time: 1min 39s

```python
X_flow = flow_dist.sample(torch.Size([1000,])).detach().numpy()
plt.title(r'Joint Distribution')
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$')
plt.scatter(X[:,0], X[:,1], label='data', alpha=0.5)
plt.scatter(X_flow[:,0], X_flow[:,1], color='firebrick', label='flow', alpha=0.5)
plt.legend()
plt.show()

plt.subplot(1, 2, 1)
sns.distplot(X[:,0], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='data')
sns.distplot(X_flow[:,0], hist=False, kde=True,
             bins=None, color='firebrick',
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='flow')
plt.title(r'$p(x_1)$')
plt.subplot(1, 2, 2)
sns.distplot(X[:,1], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='data')
sns.distplot(X_flow[:,1], hist=False, kde=True,
             bins=None, color='firebrick',
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='flow')
plt.title(r'$p(x_2)$')
plt.show()
```

We see from the output that this normalizing flow has successfully learnt both the univariate marginals *and* the bivariate distribution.

## Conditional versus Joint Distributions

### Background

In many cases, we wish to represent conditional rather than joint distributions. For instance, in performing variational inference, the variational family is a class of conditional distributions,

$$
\begin{align}
\{q_\psi(\mathbf{z}\mid\mathbf{x})\mid\theta\in\Theta\},
\end{align}
$$

where $\mathbf{z}$ is the latent variable and $\mathbf{x}$ the observed one, that hopefully contains a member close to the true posterior of the model, $p(\mathbf{z}\mid\mathbf{x})$. In other cases, we may wish to learn to generate an object $\mathbf{x}$ conditioned on some context $\mathbf{c}$ using $p_\theta(\mathbf{x}\mid\mathbf{c})$ and observations $\{(\mathbf{x}_n,\mathbf{c}_n)\}^N_{n=1}$. For instance, $\mathbf{x}$ may be a spoken sentence and $\mathbf{c}$ a number of speech features.

The theory of Normalizing Flows is easily generalized to conditional distributions. We denote the variable to condition on by $C=\mathbf{c}\in\mathbb{R}^M$. A simple multivariate source of noise, for example a standard i.i.d. normal distribution, $X\sim\mathcal{N}(\mathbf{0},I_{D\times D})$, is passed through a vector-valued bijection that also conditions on C, $g:\mathbb{R}^D\times\mathbb{R}^M\rightarrow\mathbb{R}^D$, to produce the more complex transformed variable $Y=g(X;C=\mathbf{c})$. In practice, this is usually accomplished by making the parameters for a known normalizing flow bijection $g$ the output of a hypernet neural network that inputs $\mathbf{c}$.

Sampling of conditional transforms simply involves evaluating $Y=g(X; C=\mathbf{c})$. Conditioning the bijections on $\mathbf{c}$, the same formula holds for scoring as for the joint multivariate case.

### Conditional Transforms in Pyro

In Pyro, most learnable transforms have a corresponding conditional version that derives from [ConditionalTransformModule](http://docs.pyro.ai/en/stable/distributions.html#conditionaltransformmodule). For instance, the conditional version of the spline transform is [ConditionalSpline](http://docs.pyro.ai/en/stable/distributions.html#conditionalspline) with helper function [conditional_spline](http://docs.pyro.ai/en/stable/distributions.html#conditional-spline).

In this section, we will show how we can learn our toy dataset as the decomposition of the product of a conditional and a univariate distribution,

$$
\begin{align}
p(x_1,x_2) &= p(x_2\mid x_1)p(x_1).
\end{align}
$$

First, we create the univariate distribution for $x_1$ as shown previously,

```python
dist_base = dist.Normal(torch.zeros(1), torch.ones(1))
x1_transform = T.spline(1)
dist_x1 = dist.TransformedDistribution(dist_base, [x1_transform])
```

A conditional transformed distribution is created by passing the base distribution and list of conditional and non-conditional transforms to the [ConditionalTransformedDistribution](http://docs.pyro.ai/en/stable/distributions.html#conditionaltransformeddistribution) class:

```python
x2_transform = T.conditional_spline(1, context_dim=1)
dist_x2_given_x1 = dist.ConditionalTransformedDistribution(dist_base, [x2_transform])
```

You will notice that we pass the dimension of the context variable, $M=1$, to the conditional spline helper function.

Until we condition on a value of $x_1$, the [ConditionalTransformedDistribution](http://docs.pyro.ai/en/stable/distributions.html#conditionaltransformeddistribution) object is merely a placeholder and cannot be used for sampling or scoring. By calling its [.condition(context)](http://docs.pyro.ai/en/stable/distributions.html#pyro.distributions.ConditionalDistribution.condition) method, we obtain a [TransformedDistribution](https://pytorch.org/docs/master/distributions.html#transformeddistribution) for which all its conditional transforms have been conditioned on `context`.

For example, to draw a sample from $x_2\mid x_1=1$:

```python
x1 = torch.ones(1)
print(dist_x2_given_x1.condition(x1).sample())
```

    tensor([-0.4529])

In general, the context variable may have batch dimensions and these dimensions must broadcast over the batch dimensions of the input variable.

Now, combining the two distributions and training it on the toy dataset:

```python
%%time
steps = 1 if smoke_test else 5001
modules = torch.nn.ModuleList([x1_transform, x2_transform])
optimizer = torch.optim.Adam(modules.parameters(), lr=3e-3)
x1 = dataset[:,0][:,None]
x2 = dataset[:,1][:,None]
for step in range(steps):
    optimizer.zero_grad()
    ln_p_x1 = dist_x1.log_prob(x1)
    ln_p_x2_given_x1 = dist_x2_given_x1.condition(x1.detach()).log_prob(x2.detach())
    loss = -(ln_p_x1 + ln_p_x2_given_x1).mean()
    loss.backward()
    optimizer.step()
    dist_x1.clear_cache()
    dist_x2_given_x1.clear_cache()

    if step % 500 == 0:
        print('step: {}, loss: {}'.format(step, loss.item()))
```

    step: 0, loss: 5.663331031799316
    step: 500, loss: 2.040316581726074
    step: 1000, loss: 1.9603266716003418
    step: 1500, loss: 1.8922736644744873
    step: 2000, loss: 1.8509924411773682
    step: 2500, loss: 1.8328033685684204
    step: 3000, loss: 1.9166260957717896
    step: 3500, loss: 1.877435326576233
    step: 4000, loss: 1.8416743278503418
    step: 4500, loss: 1.8379391431808472
    step: 5000, loss: 1.824593424797058
    CPU times: user 9min 24s, sys: 5.93 s, total: 9min 30s
    Wall time: 1min 23s

```python
X = torch.cat((x1, x2), dim=-1)
x1_flow = dist_x1.sample(torch.Size([1000,]))
x2_flow = dist_x2_given_x1.condition(x1_flow).sample(torch.Size([1000,]))
X_flow = torch.cat((x1_flow, x2_flow), dim=-1)

plt.title(r'Joint Distribution')
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$')
plt.scatter(X[:,0], X[:,1], label='data', alpha=0.5)
plt.scatter(X_flow[:,0], X_flow[:,1], color='firebrick', label='flow', alpha=0.5)
plt.legend()
plt.show()

plt.subplot(1, 2, 1)
sns.distplot(X[:,0], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='data')
sns.distplot(X_flow[:,0], hist=False, kde=True,
             bins=None, color='firebrick',
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='flow')
plt.title(r'$p(x_1)$')
plt.subplot(1, 2, 2)
sns.distplot(X[:,1], hist=False, kde=True,
             bins=None,
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='data')
sns.distplot(X_flow[:,1], hist=False, kde=True,
             bins=None, color='firebrick',
             hist_kws={'edgecolor':'black'},
             kde_kws={'linewidth': 2},
             label='flow')
plt.title(r'$p(x_2)$')
plt.show()
```

### Conclusions

In this tutorial, we have explained the basic idea behind normalizing flows and the Pyro interface to create flows to represent univariate, multivariate, and conditional distributions. It is useful to think of flows as a powerful general-purpose tool in your probabilistic modelling toolkit, and you can replace any existing distribution in your model with one to increase its flexibility and performance. We hope you have fun exploring the power of normalizing flows!

### References

1. E.G. Tabak, Christina Turner. [*A Family of Nonparametric Density Estimation Algorithms*](https://www.math.nyu.edu/faculty/tabak/publications/Tabak-Turner.pdf). Communications on Pure and Applied Mathematics, 66(2):145–164, 2013.
2. Danilo Jimenez Rezende, Shakir Mohamed. [*Variational Inference with Normalizing Flows*](http://proceedings.mlr.press/v37/rezende15.pdf). ICML 2015.
3. Ivan Kobyzev, Simon J.D. Prince, and Marcus A. Brubaker. [*Normalizing Flows: An Introduction and Review of Current Methods*](https://arxiv.org/abs/1908.09257). \[arXiv:1908.09257\] 2019.
4. George Papamakarios, Eric Nalisnick, Danilo Jimenez Rezende, Shakir Mohamed, Balaji Lakshminarayanan. [*Normalizing Flows for Probabilistic Modeling and Inference*](https://arxiv.org/abs/1912.02762). \[arXiv:1912.02762\] 2019.
5. Conor Durkan, Artur Bekasov, Iain Murray, George Papamakarios. [*Neural Spline Flows*](https://arxiv.org/abs/1906.04032). NeurIPS 2019.
6. Hadi M. Dolatabadi, Sarah Erfani, Christopher Leckie. [*Invertible Generative Modeling using Linear Rational Splines*](https://arxiv.org/abs/2001.05168). AISTATS 2020.
7. James Stewart. [*Calculus*](https://www.stewartcalculus.com/). Cengage Learning. 9th Edition 2020.
8. Laurent Dinh, David Krueger, Yoshua Bengio. [*NICE: Non-linear Independent Components Estimation*](https://arxiv.org/abs/1410.8516). Workshop contribution at ICLR 2015.
9. Laurent Dinh, Jascha Sohl-Dickstein, Samy Bengio. [*Density estimation using Real-NVP*](https://arxiv.org/abs/1605.08803). Conference paper at ICLR 2017.
10. David Ha, Andrew Dai, Quoc V. Le. [*HyperNetworks*](https://arxiv.org/abs/1609.09106). Workshop contribution at ICLR 2017.

```python

```


```
import sympy
ha = qubit('a', latex_label='\\alpha', dtype=sympy)
hb = qubit('b', latex_label='\\beta', dtype=sympy)
sympy.var('x,y')
```

$$\begin{pmatrix}x, & y\end{pmatrix}$$

```
U=x*ha.fourier()
U
```

<colgroup style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>

 	<nobr>$\scriptsize{\left< 0_{\alpha} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha} \right|}$</nobr>

<nobr>$\scriptsize{\left| 0_{\alpha} \right>}$</nobr>	<nobr><tt>$\frac{1}{2} \sqrt{2} x$</tt></nobr>	<nobr><tt>$\frac{1}{2} \sqrt{2} x$</tt></nobr>

<nobr>$\scriptsize{\left| 1_{\alpha} \right>}$</nobr>	<nobr><tt>$\frac{1}{2} \sqrt{2} x$</tt></nobr>	<nobr><tt>$- \frac{1}{2} \sqrt{2} x$</tt></nobr>

```
s = ha.array([1, x])
t = hb.array([1, y])
rho = (s*t).O
rho
```

<colgroup style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>

 	<nobr>$\scriptsize{\left< 0_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 0_{\alpha}1_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}1_{\beta} \right|}$</nobr>

<nobr>$\scriptsize{\left| 0_{\alpha}0_{\beta} \right>}$</nobr>	<nobr><tt>$1$</tt></nobr>	<nobr><tt>$\overline{y}$</tt></nobr>	<nobr><tt>$\overline{x}$</tt></nobr>	<nobr><tt>$\overline{x} \overline{y}$</tt></nobr>

<nobr>$\scriptsize{\left| 0_{\alpha}1_{\beta} \right>}$</nobr>	<nobr><tt>$y$</tt></nobr>	<nobr><tt>$y \overline{y}$</tt></nobr>	<nobr><tt>$y \overline{x}$</tt></nobr>	<nobr><tt>$y \overline{x} \overline{y}$</tt></nobr>

<nobr>$\scriptsize{\left| 1_{\alpha}0_{\beta} \right>}$</nobr>	<nobr><tt>$x$</tt></nobr>	<nobr><tt>$x \overline{y}$</tt></nobr>	<nobr><tt>$x \overline{x}$</tt></nobr>	<nobr><tt>$x \overline{x} \overline{y}$</tt></nobr>

<nobr>$\scriptsize{\left| 1_{\alpha}1_{\beta} \right>}$</nobr>	<nobr><tt>$x y$</tt></nobr>	<nobr><tt>$x y \overline{y}$</tt></nobr>	<nobr><tt>$x y \overline{x}$</tt></nobr>	<nobr><tt>$x y \overline{x} \overline{y}$</tt></nobr>

```
U = (ha * hb).eye()
# arrays can be indexed using dictionaries
U[{ ha: 0, ha.H: 0, hb: 0, hb.H: 0 }] = x
U[{ ha: 0, ha.H: 0, hb: 0, hb.H: 1 }] = y
U

```

<colgroup style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>

 	<nobr>$\scriptsize{\left< 0_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 0_{\alpha}1_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}1_{\beta} \right|}$</nobr>

<nobr>$\scriptsize{\left| 0_{\alpha}0_{\beta} \right>}$</nobr>	<nobr><tt>$x$</tt></nobr>	<nobr><tt>$y$</tt></nobr>	0	0

<nobr>$\scriptsize{\left| 0_{\alpha}1_{\beta} \right>}$</nobr>	0	<nobr><tt>$1$</tt></nobr>	0	0

<nobr>$\scriptsize{\left| 1_{\alpha}0_{\beta} \right>}$</nobr>	0	0	<nobr><tt>$1$</tt></nobr>	0

<nobr>$\scriptsize{\left| 1_{\alpha}1_{\beta} \right>}$</nobr>	0	0	0	<nobr><tt>$1$</tt></nobr>

```
U.I
```

<colgroup style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>

 	<nobr>$\scriptsize{\left< 0_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 0_{\alpha}1_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}1_{\beta} \right|}$</nobr>

<nobr>$\scriptsize{\left| 0_{\alpha}0_{\beta} \right>}$</nobr>	<nobr><tt>$\frac{1}{x}$</tt></nobr>	<nobr><tt>$- \frac{y}{x}$</tt></nobr>	0	0

<nobr>$\scriptsize{\left| 0_{\alpha}1_{\beta} \right>}$</nobr>	0	<nobr><tt>$1$</tt></nobr>	0	0

<nobr>$\scriptsize{\left| 1_{\alpha}0_{\beta} \right>}$</nobr>	0	0	<nobr><tt>$1$</tt></nobr>	0

<nobr>$\scriptsize{\left| 1_{\alpha}1_{\beta} \right>}$</nobr>	0	0	0	<nobr><tt>$1$</tt></nobr>

```
U * U.I
```

<colgroup style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>
<colgroup span=2 style='border: 2px solid black;'></colgroup>

 	<nobr>$\scriptsize{\left< 0_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 0_{\alpha}1_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}0_{\beta} \right|}$</nobr>	<nobr>$\scriptsize{\left< 1_{\alpha}1_{\beta} \right|}$</nobr>

<nobr>$\scriptsize{\left| 0_{\alpha}0_{\beta} \right>}$</nobr>	<nobr><tt>$1$</tt></nobr>	0	0	0

<nobr>$\scriptsize{\left| 0_{\alpha}1_{\beta} \right>}$</nobr>	0	<nobr><tt>$1$</tt></nobr>	0	0

<nobr>$\scriptsize{\left| 1_{\alpha}0_{\beta} \right>}$</nobr>	0	0	<nobr><tt>$1$</tt></nobr>	0

<nobr>$\scriptsize{\left| 1_{\alpha}1_{\beta} \right>}$</nobr>	0	0	0	<nobr><tt>$1$</tt></nobr>

```
ha
```

$\left| \alpha \right\rangle$

```

```