# モデル化

* Next >> [0_quickstart](https://github.com/yfur/basic-mechanics-python/blob/master/0_quickstart/0_quickstart.ipynb)
* Prev >> editing

シミュレーションを行う際に一番最初に行うのは **モデル化** である．シミュレーションの結果は，どのようにモデル化を行ったかによって大きく影響される．当然ではあるが．

例えば，単振り子のシミュレーションにおいて，`0_quickstart`では **摩擦** による運動の減衰を考えなかったが，これを考えてモデル化を行ってみる．
振子と天井の結点の部分で粘性摩擦を仮定し，角速度に比例した力$-c\dot{\theta}$がはたらくものとする．すると，運動方程式は
\begin{align}
ml\ddot{\theta} = -mg\sin\theta - c\dot{\theta}
\end{align}
となる．

```python
import numpy as np
from scipy.integrate import odeint
from math import sin

''' constants '''
m = 1 # mass of the pendulum [kg]
l = 1 # length of the pendulum [m]
g = 10 # Gravitational acceleration [m/s^2]
c = 0.3 # Damping constant [kg.m/(rad.s)]

''' time setting '''
t_end = 10 # simulation time [s]
t_fps = 50 # frame per second. This value means smoothness of produced graph and animation
t_step = 1/t_fps
t = np.arange(0, t_end, t_step)

''' initial value '''
theta_init = 0 # initial value of theta [rad]
dtheta_init = 1 # initial value of dot theta [rad/s]
s_init = np.array([theta_init, dtheta_init])

def odefunc(s, t):
    theta = s[0]
    dtheta = s[1]
    ddtheta = -g/l*sin(theta) - c*dtheta# <- Equation of motion. *** THIS CODE CHANGED ***
    return np.r_[dtheta, ddtheta]

s = odeint(odefunc, s_init, t)
print('ODE calculation finished.')
```

    ODE calculation finished.

振り子の角度の時間変化をグラフにすると，このようになっている．

```python
%matplotlib inline
import matplotlib.pyplot as plt
plt.figure()
plt.plot(t, s[:, 0])
plt.xlabel('t [s]')
plt.ylabel('theta [rad]')
plt.show()
```

**摩擦** という要素を考えることによって運動の様相が変化したことがわかる．

シミュレーションを行う上では，考えたい物理モデルをどのようにモデル化するかによって，得られる結果が大きく変わる．所望のシミュレーションを行うためには，十分な力学の知識が必要となる．

## Lagrange の運動方程式

`0_quickstart`では，単振り子の運動方程式をニュートンの運動方程式から求めたが，今度は **ラグランジュの運動方程式** から求める．

おもりの運動エネルギーは
\begin{align}
  T = \frac{1}{2}m(l\dot{\theta})^2
\end{align}
であり，ポテンシャルエネルギー（位置エネルギー）は
\begin{align}
  U = - m(-g)(l-l\cos\theta) = mgl(1 - \cos\theta)
\end{align}
である．したがって，系のラグランジアンは
\begin{align}
  L = T - U = \frac{1}{2}m(l\dot{\theta})^2 - mgl(1 - \cos\theta)
\end{align}
であり，ラグランジュの運動方程式は
\begin{align}
  \frac{d}{dt}\left( \frac{\partial L}{\partial \dot{\theta}} \right) - \frac{\partial L}{\partial \theta} = 0
\end{align}
である．項を一つ一つ丁寧に計算をすると，
\begin{align}
  \frac{\partial L}{\partial \dot{\theta}} = \frac{\partial }{\partial \dot{\theta}} \left( \frac{1}{2}m(l\dot{\theta})^2 - mgl(1 - \cos\theta) \right) = ml^2\dot{\theta}
\end{align}
\begin{align}
  \frac{d}{dt}\left( \frac{\partial L}{\partial \dot{\theta}} \right) = \frac{d}{dt} (ml^2\dot{\theta}) = ml^2\ddot{\theta}
\end{align}
\begin{align}
  \frac{\partial L}{\partial \theta} = \frac{\partial }{\partial \theta} \left( \frac{1}{2}m(l\dot{\theta})^2 - mgl(1 - \cos\theta) \right) = -mgl \sin\theta
\end{align}
より，
\begin{align}
  \frac{d}{dt}\left( \frac{\partial L}{\partial \dot{\theta}} \right) - \frac{\partial L}{\partial \theta} = ml^2\ddot{\theta} - (-mgl \sin\theta) = 0
\end{align}
よって，
\begin{align}
 ml^2\ddot{\theta} + mgl \sin\theta = 0
\end{align}
である．式を整理すると，
\begin{align}
\ddot{\theta} = -\frac{g}{l} \sin\theta
\end{align}
となっており，ニュートンの運動方程式から導出したものと同じ結果が得られたことがわかる．

## Lagrange の運動方程式を SymPy で計算する

Lagrange の運動方程式は運動の自由度についてのミニマムな運動方程式を記述することができる．しかし，ラグランジアンとその偏微分の計算は複雑になりがちである．単振り子の例については，運動の自由度は1であり，かつ非常にシンプルな状況であるため手で計算してもよいのだが，これが他リンク系となったり，運動を2次元から3次元に拡張したりしたときには，もはや手計算で求める気力が起こらない．

そこで， Python を使って Lagrange の運動方程式を導く． [SymPy](http://www.sympy.org/en/index.html) の [LagrangesMethod クラス](http://docs.sympy.org/latest/modules/physics/mechanics/api/kane_lagrange.html?highlight=lagrangesmethod#sympy.physics.mechanics.lagrange.LagrangesMethod) を用いる．

```python
import sympy as sym
import sympy.physics.mechanics as me
```

定数については，`m = sym.symbols('m')` のように定義する．
なお，時間$t＄については必ず `m = sym.symbols('m')` を定義する必要がある．

時間とともに変化する値（一般化座標）については， `theta = me.dynamicsymbols('theta')`　のように定義する．また，これの微分（一般加速度）については， `dtheta = me.dynamicsymbols('theta, 1')`　のようにして定義をする．

```python
''' Define constants and generalized coordinates '''
t = sym.symbols('t')
l, m, g = sym.symbols('l m g')
theta = me.dynamicsymbols('theta')
dtheta = me.dynamicsymbols('theta', 1)
```

物理モデルに必要な定数・変数を全て定義してから，力学的エネルギーをそれぞれ記述し，ラグランジアンについても計算する．

```python
''' Kinetic energy '''
T = m*(l*dtheta)**2/2
''' Potential energy '''
U = -m*(-g)*(l - l*sym.cos(theta))

''' Lagurangian '''
L = T - U
```

`LM = me.LagrangesMethod(ラグランジアン, [一般化座標の配列])` という関数で，ラグランジュの運動方程式を定義する．

`LM.form_lagranges_equations()` でラグランジュの運動方程式が出力される．

```python
''' Calculating the eom '''
LM = me.LagrangesMethod(L, [theta])

print(LM.form_lagranges_equations())
```

    Matrix([[g*l*m*sin(theta(t)) + l**2*m*Derivative(theta(t), t, t)]])

出力結果は `Matrix([[g*l*m*sin(theta(t)) + l**2*m*Derivative(theta(t), t, t)]])` となる． `Matrix([[ ... ]])` の中に記述されている部分に着目すると，手計算で求めた
\begin{align}
 ml^2\ddot{\theta} + mgl \sin\theta = 0
\end{align}
の左辺と一致していることがわかる．ただし， `Derivative(theta(t), t, t)` は $\theta$ の二階の微分 (derivative) である．

## 【参考】散逸項を含む形での Lagrange の運動方程式

先に記述した粘性摩擦のような外力については， **散逸関数** とその一般加速度微分項を考えることで運動方程式に加えることができる．

散逸関数は，
\begin{align}
D = \frac{1}{2}c\dot{\theta}^2
\end{align}
である．そして Lagrange の運動方程式は
\begin{align}
  \frac{d}{dt}\left( \frac{\partial L}{\partial \dot{\theta}} \right) - \frac{\partial L}{\partial \theta} + \frac{\partial D}{\partial \dot{\theta}} = 0
\end{align}
を計算することになる．

## 【参考】一般化座標への入力を含む形での Lagrange の運動方程式
一般化座標$\theta$について，この方向に加速度を加えるような入力が加えられるとする．つまり，振子の結点の部分にモータが取り付けられていて，このモータから直接トルクが加えられるような状況である．このとき，加えられるトルクを$\tau$とすると，
\begin{align}
  \frac{d}{dt}\left( \frac{\partial L}{\partial \dot{\theta}} \right) - \frac{\partial L}{\partial \theta} = \tau
\end{align}
となる．


```python
!pip install sympy
```

    Collecting sympy
      Downloading sympy-1.8-py3-none-any.whl (6.1 MB)
         |████████████████████████████████| 6.1 MB 3.1 MB/s eta 0:00:01
    Collecting mpmath>=0.19
      Downloading mpmath-1.2.1-py3-none-any.whl (532 kB)
         |████████████████████████████████| 532 kB 24.9 MB/s eta 0:00:01
    Installing collected packages: mpmath, sympy
    Successfully installed mpmath-1.2.1 sympy-1.8

```python
L=1;T=1;Nx=100;Nt=100;a=1
import numpy as np
x = np.linspace(0, L, Nx+1)    # mesh points in space
dx = x[1] - x[0]
t = np.linspace(0, T, Nt+1)    # mesh points in time
dt = t[1] - t[0]
F = a*dt/dx**2
u   = np.zeros(Nx+1)           # unknown u at new time level
u_n = np.zeros(Nx+1)           # u at the previous time level
u_n = np.ones(Nx+1)
# Set initial condition u(x,0) = I(x)
for i in range(0, Nx+1):
    u_n[i] = I(x[i])

for n in range(0, Nt):
    # Compute u at inner mesh points
    for i in range(1, Nx):
        u[i] = u_n[i] + F*(u_n[i-1] - 2*u_n[i] + u_n[i+1]) + \
           dt*f(x[i], t[n])

    # Insert boundary conditions
    u[0] = 0;  u[Nx] = 0

    # Update u_n before next step
    u_n[:]= u
```

```python
def diff_sol(x,t,n=100):
    n_list = np.arange(n)
    sol_n = 4/((2*n_list + 1)*np.pi)*np.sin((2*n_list)*np.pi/2*x)*np.exp(-((2*n_list + 1)*np.pi/2)**2*t)
    sol = np.sum(sol_n)
    return sol #,sol_n
```

```python
np.arange(10)
```

    array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

```python
t = 0.0000005
x = np.linspace(0,1,100)
```

```python
sol,sol_n=diff_sol(0.8,t)
```

```python
sol
```

    0.11461808626860323

```python
import matplotlib.pyplot as plt
import numpy as np
```

```python
d=[]
for xx in x:
    d.append(diff_sol(x,t))
```

```python
from sympy import Sum, Symbol
x = Symbol('x')
s = Sum( 1/(x**2-1), (x, 1, 00))
s.doit()
```

$\displaystyle 0$

```python
from sympy import Sum, factorial, oo, IndexedBase, Function, sin,exp
from sympy.abc import n
```

```python
def diff_sol(x,t,alpha=0.1):
    sol= Sum(4/((2*n + 1)*np.pi)*sin((2*n+1)*np.pi/2*x)*exp(-((2*n + 1)*np.pi/2)**2*t*alpha),(n,0,50))

    return sol #,sol_n
```

```python
sol=diff_sol(0.5,0.2)
```

```python
sol
```

$\displaystyle \sum_{n=0}^{20} \frac{4 e^{- 1.97392088021787 \left(n + 0.5\right)^{2}} \sin{\left(1.5707963267949 n + 0.785398163397448 \right)}}{6.28318530717959 n + 3.14159265358979}$

```python
sol.doit()
```

$\displaystyle 0.553175891850086$

```python
xxx=np.linspace(0,1,100)
```

```python
for t in np.linspace(0.1,1,11):
    d=[]
    for xx in xxx:
        d.append(diff_sol(xx,t,alpha=1).doit())
    plt.plot(d)
```

```python

```

```python
xxx
```

    array([0.        , 0.11111111, 0.22222222, 0.33333333, 0.44444444,
           0.55555556, 0.66666667, 0.77777778, 0.88888889, 1.        ])

```python

```


# Surfinpy

#### Tutorial 4 -Temperature vs Pressure Phase Diagram

In tutorial 2, we showed how experimental data could be used to determine the temperature dependent free energy term for gaseous species and then plot a phase diagram that represents 298 K.  This same method can be used in conjuction with a temperature range to produce a phase diagram of temperature as a function of pressure (or chemical potential).  This is an important step to producing relatable phase diagrams that can be compared to experimental findings.

##### Methodology

To reiterate, the free energy can be calculated using;

\begin{align}
\Delta G^{0}_{f} = \sum\Delta G_{f}^{0,\text{products}} - \sum\Delta G_{f}^{0,\text{reactants}}
\end{align}

Where for this tutorial the free energy (G) for solid phases  is equal to is equal to the calculated DFT energy (U0). For gaseous species, the standard free energy varies significantly with temperature, and as DFT simulations are designed for condensed phase systems, we use experimental data to determine the temperature dependent free energy term for gaseous species, where $S_{\text{expt}}(T)$ is specific entropy value for a given T and  $H-H^0(T)$ is the , both can be obtained from the NIST database and can be calculated as;

\begin{align}
G =  U_0 + (H-H^0(T) - T S_{\text{expt}}(T))
\end{align}

##### Code

```python
import matplotlib.pyplot as plt

from surfinpy import bulk_mu_vs_mu as bmvm
from surfinpy import utils as ut
from surfinpy import bulk_mu_vs_t as bmvt
from surfinpy import data
import numpy as np

colors = ['#5B9BD5', '#4472C4', '#A5A5A5', '#772C24', '#ED7D31', '#FFC000', '#70AD47']

```

temperature_range sets the temperature range which is calculated for the phase diagram and needs to be specified within the data ReferenceDataSet and DataSet.

```python
temperature_range = [200, 400]
```

```python
bulk = data.ReferenceDataSet(cation = 1, anion = 1, energy = -92.0, funits = 10, file = 'ref_files/bulk_vib.yaml', temp_range=temperature_range)
```

```python

Bulk = data.DataSet(cation = 10, x = 0, y = 0, energy = -92., color=colors[0],
                   label = "Bulk", file = 'ref_files/bulk_vib.yaml',
                   funits = 10, temp_range=temperature_range)

D = data.DataSet(cation = 10, x = 10, y = 0, energy = -310.,  color=colors[1],
                   label = "D", file = 'ref_files/D_vib.yaml',
                   funits =  10, temp_range=temperature_range)

B = data.DataSet(cation = 10, x = 0, y = 10, energy = -227.,  color=colors[2],
                   label = "B", file = 'ref_files/B_vib.yaml',
                   funits =  10, temp_range=temperature_range)

F = data.DataSet(cation = 10, x = 8, y = 10, energy = -398.,  color=colors[3],
                   label = "F", file = 'ref_files/F_vib.yaml',
                   funits =  2, temp_range=temperature_range)

A = data.DataSet(cation = 10, x = 5, y = 20, energy = -467.,  color=colors[4],
                   label = "A", file = 'ref_files/A_vib.yaml',
                   funits = 5, temp_range=temperature_range)

C = data.DataSet(cation = 10, x = 10, y = 30, energy = -705.,  color=colors[5],
                   label = "C", file = 'ref_files/C_vib.yaml',
                   funits = 10, temp_range=temperature_range)

E = data.DataSet(cation = 10, x = 10, y = 50, energy = -971.,  color=colors[6],
                   label = "E", file = 'ref_files/E_vib.yaml',
                   funits =  10, temp_range=temperature_range)

```

```python
data = [Bulk, A, B, C,  D, E, F]
```

deltaZ specifies the temperature range which is plotted (Note that this must be the same as temperature_range).  mu_y is the chemical potential (eV) of third component, in this example we use a chemical potential of water = 0 eV which is equivalent to 1 bar pressure.

```python
deltaX = {'Range': [ -1, 0.6],  'Label': 'CO_2'}
deltaZ = {'Range': [ 200, 400], 'Label': 'Temperature'}
x_energy=-20.53412969
y_energy=-12.83725889
mu_y = 0
```

```python
exp_x = ut.temperature_correction_range("ref_files/CO2.txt", deltaZ)
exp_y = ut.temperature_correction_range("ref_files/H2O.txt", deltaZ)
```

```python
system = bmvt.calculate(data, bulk, deltaX, deltaZ, x_energy, y_energy, mu_y, exp_x, exp_y)
ax = system.plot_mu_vs_t(figsize=(6, 4.5))
```

```python
system.plot_p_vs_t(temperature=273, set_style="seaborn-dark-palette", figsize=(6, 4.5))
```

```python
ax = system.plot_mu_vs_t_vs_p(temperature=273, set_style="seaborn-dark-palette", figsize=(5, 4.5))
```

```python

```


 ✅ Put your name here

#
 Pre-Class Assignment 23: Background for Quantum Computing

This notebook starts a unit introducing a different model of computation, a model that plays by the rules of quantum physics rather than classical physics.

I hope you're not intimidated! Unfortunately, quantum physics gets a bad rap of being inherently confusing. This is not at all the case! Quantum physics sounds strange due to some weird consequences that we'll see shortly, but it's actually really easy to do, involving some simple mathematics that you have probably seen before. And it doesn't require you to know any classical physics! (If you do, you'll see why quantum physics is a strange theory.)

Quantum computing is a relatively new field that started in the 1980s and 1990s. Due to recent advances in experimental physics and engineering, we have today some of the world's first quantum computers, and the field has received a lot of attention recently. At the end of this unit, you'll have the opportunity to program a quantum computer!

##
 Itinerary for Quantum Computing Unit

    Assignment
    Topic
    Description

    Pre Class 23
    Background for Quantum Computing
    How Computers Store Information

    In Class 23
    Classsical and Quantum Bits
    Information in Quantum States

      Pre Class 24
      Software for Quantum Computing
      High Level Software and the Circuit Model

      In Class 24
      Programming Quantum Computers
      Manipulating Quantum Bits to Perform Useful Computations

###
 Before you start...

Take ten seconds to answer these survey questions:

```python
from IPython.display import HTML
HTML(
"""

"""
)
```

##
 Learning Goals for Today's Pre-Class Assignment

By the end of today's pre-class assignment, you should be able to:

1. Describe how computers store information using binary digits.
1. State the fundamental difference between classical and quantum computers in terms of how they store information.
1. Review/learn complex numbers, probability distributions, and vectors to more deeply understand quantum binary digits.

#
 How Computers Store Information

Watch the following video to learn about binary digits, or bits, the fundamental unit of information for all data in a computer.

```python
"""How computers work: binary & data."""
from IPython.display import YouTubeVideo
YouTubeVideo("USCBCmwMCDA", width=640, height=360)
```

Question: What are the possible values of a bit?

✎ **Answer:** Erase the contents of this cell and put your answer here!

The video mentioned that 1001 in binary is equal to 9 in decimal. You should understand how to convert from binary to decimal ($1001$ means $1 \cdot 2^3 + 0 \cdot 2^2 + 0 \cdot 2^1 + 1 \cdot 2^0 = 9$). There's a cool trick for doing this in Python, shown below.

```python
"""Cool trick! Converting from binary to decimal."""
int("1001", 2)
```

Here, the first argument to `int` is what gets converted to a number. The second argument to `int` represents the base of the number system to use (binary = base 2). You can change the first argument to get different resulting numbers and test your understanding of binary.

Question: All data on a computer--including text, images, and sound--is stored in bits. Pick one of these (text, images, or sound) and explain how bits are used to represent this information.

✎ **Answer:** Erase the contents of this cell and put your answer here!

Question: What do we use to physically represent bits in computers?

✎ **Answer:** Erase the contents of this cell and put your answer here!

#
 How Quantum Computers Store Information

Recall the last statement from the above video on bits and data:

   "If you want to understand how computers work on the inside, it all comes down to these simple ones and zeros and the electrical signals in the circuits behind them."

In the same way, if you want to understand how quantum computers work, it all comes down to how information is stored.

   Quantum computers store information in quantum bits, or qubits (pronounced "CUE bits") for short.

Watch the following short video to get introduced to qubits.

```python
"""Introduction to qubits."""
from IPython.display import YouTubeVideo
YouTubeVideo("KBpYK3i3kDs",width=640,height=360)
```

#
 Understanding Qubits: Three Key Concepts

To understand a qubit, we only have to understand three concepts.

1. Complex numbers.
1. Probability.
1. Vectors.

Watch the next three videos to see each concept in turn, and complete the exercises to test your understanding.

The goal of these concepts is to understand a qubit at a deeper level. Each may seem unrelated, but everything will tie together at the end of the notebook.

```python
"""Imports for the notebook."""
import numpy as np
import matplotlib.pyplot as plt
```

##
 Concept #1: Complex Numbers

Watch the following video on complex numbers.

```python
"""Complex numbers."""
from IPython.display import YouTubeVideo
YouTubeVideo("3AmdT0CsLbk",width=640,height=360)
```

###
 Video Recap

* The imaginary unit, which we'll denote $i$, is defined by the property that $i^2 = -1$. (Note: In Python, `j` is used for the imaginary unit.)

* A complex number has the form

\begin{equation}
    \alpha = a + b i
\end{equation}

where $a$ and $b$ are real numbers. (The symbol $\alpha$ is the Greek letter alpha. We'll use Greek letters for complex numbers to not confuse them with real numbers.)

* The addition of two complex numbers is defined by

\begin{equation}
        \alpha + \beta = (a + b i) + (c + d i) := (a + c) + (b + d)i .
\end{equation}

* We define the complex conjugate of a complex number $\alpha = a + bi$ to be

\begin{equation}
    \alpha^* := a - bi .
\end{equation}

(That is, we flip the sign of the imaginary part.)

* The modulus squared of $\alpha$ is defined to be the product of itself with its complex conjugate:

\begin{equation}
    |\alpha|^2 := \alpha^* \alpha = a^2 + b^2
\end{equation}

As you might guess, the modulus is just the square root of the modulus squared.

###
 Exercise: Working with Complex Numbers

✎ **Do this:** Run the cell below to see how to perform some operations on complex numbers in Python.

```python
"""Working with complex numbers in Python."""
# define two complex numbers
alpha = 1 + 2j # note: j is used for the imaginary unit in Python
beta = 3 - 4j
print("alpha =", alpha)
print("beta =", beta)

# print out the type of alpha
print("\ntype(alpha) =", type(alpha))

# print out the real and imaginary part of alpha
print("\nThe real part of alpha is", alpha.real)
print("The imaginary part of alpha is", alpha.imag)

# print out the sum of alpha and beta
print("\nalpha + beta =", alpha + beta)

# print out the complex conjugate of alpha and beta
print("\nalpha* =", alpha.conjugate())
print("beta* =", beta.conjugate())
```

✎ **Do this:** Write a function called `modulus_squared` that inputs a complex number $\alpha$ and returns its modulus squared $|\alpha|^2 = \alpha^* \alpha$.

Important: Make sure your function returns a `float`, not a `complex` number. You can do this by using the `real` part of the modulus squared.

```python
"""Put code for implementing your function here!"""
def modulus_squared(alpha):
    pass
```

The next cell contains test cases for your function. If your function is correct, this cell will execute without error. (Note: `assert EXPRESSION` throws an error if the `EXPRESSION` is `False`. Otherwise, nothing happens. For this reason, it's often used to test code.)

```python
"""Test cases: run this cell to ensure your function is correct."""
assert np.isclose(modulus_squared(3+4j), 25.0)
assert np.isclose(modulus_squared(1), 1.0)
assert np.isclose(modulus_squared(1j), 1.0)
assert np.isclose(modulus_squared(-3 - 4j), 25.0)
```

##
 Concept #2: Probability

Watch the following video on probability distributions.

```python
"""Probability."""
from IPython.display import YouTubeVideo
YouTubeVideo("rfmmhXzi5lk",width=640,height=360)
```

###
 Video Recap

A probability distribution is a list of numbers $p_1, ..., p_n$ that satisfy the following conditions:

* Each probability is non-negative.

\begin{equation}
p_i \ge 0
\end{equation}

* The sum over all probabilites is equal to one.

\begin{equation}
\sum_{i = 1}^{n} p_i = 1 .
\end{equation}

###
 Exercise: Working with Probabilities

**Question:** Could the following list of numbers be a probability distribution? Why or why not?

```python
"""Potential probability distribution."""
distribution = np.array([0.1, 0.3, 0.2, 0.2, 0.1, 0.2])
```

✎ **Answer:** Erase the contents of this cell and put your answer here!

**Question:** Write a function, called `is_valid`, that inputs a numpy array and returns `True` if the list of numbers defines a valid probability distribution, else returns `False`.

```python
"""Put code for implementing your function here!"""
def is_valid(array):
    pass
```

Run the next cell to test your function. If your function is correct, no errors should be thrown.

```python
"""Run this cell to test your function."""
assert is_valid(np.array([0.5, 0.3, 0.2]))
assert not is_valid(np.array([0.2, 0.4, 0.2]))
assert not is_valid(np.array([1.0, -1.0, 1.0]))
```

##
 Concept #3: Linear Algebra & Vectors

Watch the following video on vectors.

```python
"""Linear algebra and vectors."""
from IPython.display import YouTubeVideo
YouTubeVideo("klDm1eC1gxg",width=640,height=360)
```

###
 Video Recap

* A vector is the formal mathematical term for a list of numbers. (You may understand vectors as objects with size and direction, which is an equally valid definition. For the purposes of quantum computing, it's more convenient to think of vectors as just lists of numbers.)

* An example of a vector is

\begin{equation}
    |0\rangle := \left[ \begin{matrix}
    1 \\
    0 \\
    \end{matrix} \right],
\end{equation}

and another example of a vector is

\begin{equation}
    |1\rangle := \left[ \begin{matrix}
    0 \\
    1 \\
    \end{matrix} \right]
\end{equation}

* The angled-bracket notation $|\rangle$ denotes that an object is a vector. The number inside of the angled brackets is a label for which vector it is. (You'll see why we label the vectors 0 and 1 in the next In Class Assignment. In principle, though, any symbol could be used to label the vector.)

* Vector addition is defined component-wise. For example,

\begin{equation}
    |0\rangle + |1\rangle = \left[ \begin{matrix}
    1 \\
    0 \\
    \end{matrix} \right] +
    \left[ \begin{matrix}
    0 \\
    1 \\
    \end{matrix} \right]
    =
    \left[ \begin{matrix}
    1 + 0 \\
    0 + 1 \\
    \end{matrix} \right]
    =
    \left[ \begin{matrix}
    1 \\
    1 \\
    \end{matrix} \right]
\end{equation}

* We can also take scalar multiples of vectors, for example

\begin{equation}
    \alpha |0\rangle = \alpha \left[ \begin{matrix}
    1 \\
    0 \\
    \end{matrix} \right]
    =
    \left[ \begin{matrix}
    \alpha \cdot 1 \\
    \alpha \cdot 0 \\
    \end{matrix} \right]
    =
    \left[ \begin{matrix}
    \alpha \\
    0 \\
    \end{matrix} \right].
\end{equation}

In general, we multiply each component of the vector by the number $\alpha$.

* This allows us to write superpositions, which are scalar multiples and sums of vectors. That is, equations of the form

\begin{equation}
    \alpha |0\rangle + \beta |1\rangle
\end{equation}

* In Python, Numpy arrays handle vector operations for us.

###
 Exercise: Working with Vectors

The following cell shows how we use Numpy arrays to work with vectors in Python.

```python
"""Using numpy to perform vector operations."""
# the |0> == zero vector and |1> == one vector from above
zero = np.array([1, 0], dtype=np.complex64)
one = np.array([0, 1], dtype=np.complex64)

# print out the vectors
print("|0> =", zero)
print("|1> =", one)

# some complex numbers
alpha = 0.5 + 0.5j
beta = 1 - 2j
```

✎ **Do this:** Run the following code cell to see how Numpy arrays handle vector operations for us. Complete the last portion, labeled `TODO`.

```python
"""Run this cell. Complete the last portion."""
# print out the sum of zero and one
print("|0> + |1> =", zero + one)

# compute and print out alpha * |0>
print("alpha |0> =", alpha * zero)

# compute and print out beta * |1>
print("beta |1>> =", beta * one)

# TODO: print out the superposition alpha |0> + beta |1>

```

Question: Is this output of the cell above what you expect based on the definition of vector addition and scalar multiples of vectors?

✎ **Answer:** Erase the contents of this cell and put your answer here!

#
 Tying Together the Concepts

When we introduced a qubit, we said it could be the state $|0\rangle$, $|1\rangle$, or superpositions of $|0\rangle$ and $|1\rangle$. We can now fully understand this statement.

A superposition is a sum of scalar multiples of vectors. So, the most general state of a qubit can be written

\begin{equation}
    |\psi\rangle = \alpha |0\rangle + \beta |1\rangle
    =
    \left[ \begin{matrix}
    \alpha \\
    \beta \\
    \end{matrix} \right]
\end{equation}

where $|\alpha|^2 + |\beta|^2 = 1$.

That is, a qubit is a vector of complex numbers. These complex numbers determine the probability of measuring a particular state, as we'll discuss in upcoming assignments.

Unlike bits, which are only 0 or 1, qubits can exist in superposition states. This is the first idea that there is "more" processing power with qubits (quantum computers) than with bits (classical computers).

However, this isn't the entire story. Teaser of what's to come: When we measure qubits, we record either 0 with a probability that depends on $\alpha$, the coefficient of $|0\rangle$. In particular,

\begin{equation}
    p(\text{measuring 0}) = |\alpha|^2
\end{equation}

Similarly for measuring 1:

\begin{equation}
    p(\text{measuring 1}) = |\beta|^2
\end{equation}

This is why we requre $|\alpha|^2 + |\beta|^2 = 1$ for qubits. The next in class assignment will explore measurements further and give you more practice working with qubits.

(Brief remark for those interested: A qubit is an example of a wavefunction in quantum physics. A wavefunction is a mathematical description of a quantum system. In the discrete case (like a qubit), it consists of a vector of complex numbers which determine the probability of measuring particular states.)

#
 Assignment Wrapup

##
 Survey

```python
from IPython.display import HTML
HTML(
"""

"""
)
```

##
 Congrats, You're Finished!

Now, you just need to submit this assignment by uploading it to the course Desire2Learn web page for today's submission folder. (Don't forget to add your name in the first cell.)

© Copyright 2019, Michigan State University Board of Trustees.


# Hands-on LH1: the $\mathrm{TE}_{10}$-$\mathrm{TE}_{30}$ Mode Converter

## Introduction

The Tore Supra Lower Hybrid Launchers are equiped by $\mathrm{TE}_{10}$-$\mathrm{TE}_{30}$ mode converters, a waveguide structure which convert the RF power from a propagation mode to another, in order to split the power by three in the poloidal direction. The electric field topology in this device is illustrated in the following figure.

During this hands-on, students are initiated to RF measurements and analysis. Before measuring the RF performances of a mode converter, the students are brought to calibrate the RF measurement apparatus, a (Vectorial) Network Analyser. This device measures the scattering parameters (or S-parameters) between two ports, generally referred as ports 1 and 2. The scattering parameters are defined in terms of incident and reflected waves at ports, and are widely used at microwave frequencies where it becomes difficult to measure voltages and currents directly.

Before starting, let's import the necessary Python libraries:

```python
# This line configures matplotlib to show figures embedded in the notebook,
# and also import the numpy library
%pylab
%matplotlib inline
```

    Using matplotlib backend: Qt5Agg
    Populating the interactive namespace from numpy and matplotlib

## Scattering parameters measurement

The following figure illustrates the measurement setup, and the adopted port indexing convention.

Below we import the measurements data generated by the network analyser. These data consist in an ASCII file (tab separated file), with the following usual header :
! Network Analyzer HP8720ES.07.62: Oct 06, 1999 Serial No. US39172126
! Hewlett-Packard
! 10 Feb 2011  10:37:32
! Frequency       S11                S21                S12                S22
3600000000  -8.5072   -37.511   -5.4372    105.59   -5.4692    105.56   -6.405    48.978
3601000000  -8.5247   -40.428   -5.4388    102.22   -5.4899    102.31   -6.4298    42.946
3602000000  -8.5488   -43.451   -5.4724    98.785   -5.4869    98.979   -6.4618    37.193
where we have, by column number:

1. The frequency in Hertz,
2. The amplitude of the $S_{11}$ parameter in decibel (dB),
3. The phase of the $S_{11}$ parameter in degree,
4. The amplitude of the $S_{21}$ parameter in decibel (dB),
5. The phase of the $S_{21}$ parameter in degree,

and etc for $S_{12}$ and $S_{22}$.

Below we import the first measurement data, performed between the port indexed "0" of the mode converter (the input, corresponding to the port 1 of the network analyser) and the port indexed "1" of the mode converter (corresponding to port 2 of the network analyser).

```python
%cd C:\Users\JH218595\Documents\Notebooks\TP Master Fusion
```

    [WinError 2] Le fichier spécifié est introuvable: 'C:\\Users\\JH218595\\Documents\\Notebooks\\TP Master Fusion'
    C:\Users\JH218595\Documents\Documents_GitHub\notebooks\TP Master Fusion

```python
CDM1 = np.loadtxt('./LH1_Mode-Converter_data/CDM01', skiprows=4)
f_1 = CDM1[:,0]
```

The measurement contains $N$ frequency points, where $N$ is:

```python
len(f_1)
```

    201

Let's have a look to these data, for example by plotting the amplitude of the $S_{10}$ parameter, that is the ratio of the power coming from port 0 to port 1 (which corresponds to $S_{21}$  in the network analyser file).

```python
S10_dB = CDM1[:,3]
plot(f_1/1e9,S10_dB, lw=2)
xlabel('f [GHz]')
ylabel('Amplitude [dB]')
grid('on')
title('$S_{10}$ amplitude vs frequency')
```

OK. Let's do the same for the second and third measurements performed, that is for the power transferred from port 0 to ports 2 and 3.

```python
CDM2 = loadtxt('LH1_Mode-Converter_data/CDM02', skiprows=4)
CDM3 = loadtxt('LH1_Mode-Converter_data/CDM03', skiprows=4)

f_2 = CDM2[:,0]
f_3 = CDM3[:,0]

S20_dB = CDM2[:,3]
S30_dB = CDM3[:,3]
S00_dB = CDM1[:,1]

plot(f_1/1e9, S10_dB, f_2/1e9, S20_dB, f_3/1e9, S30_dB, lw=2)
xlabel('f [GHz]')
ylabel('Amplitude [dB]')
grid('on')
title('$S_{i0}$, $i=1,2,3$: amplitude vs frequency')
legend(('$S_{10}$','$S_{20}$', '$S_{30}$'),loc='best')
```

```python
idx = np.argmin(np.abs(f_1/1e9 - 3.7))
print(f'Measured values at 3.7 GHz: {S10_dB[idx]}, {S20_dB[idx]} and {S30_dB[idx]} dB')
```

    Measured values at 3.7 GHz: -5.4158, -4.2252 and -5.2717 dB

Nice. Now, let's stop and think. The purpose of the mode converter is the transfer the power from the fundamental mode of rectangular waveguides, namely the $\mathrm{TE}_{10}$, into a higher order mode, the $\mathrm{TE}_{30}$. Once these mode conversion achieved, thin metallic wall septum are located in zero E-field regions, which allows to split the power into three independant waveguides.

Dividing the power by 3, is equivalent in decibels to:

```python
10*log10(1.0/3.0)
```

    -4.771212547196624

Thus, ideally, the three transmission scattering parameters should be equal to -4.77 dB at the operational frequency, 3.7 GHz in our case. Clearly from the previous figure we see that it is not the case. The power splitting is unbalanced, and more power is directed to port 2 than to ports 1 and 3 at 3.7 GHz. In conclusion of this first serie of measurements: this mode converter is not working properly. The big deal is: "why ?".

Before continuying, it may be usefull to define a function that convert a (dB,degree) number into a natural complex number:

```python
def dBdegree_2_natural(ampl_dB, phase_deg):
    amp = 10**(ampl_dB/20)
    phase_rad = np.pi/180*phase_deg
    return amp*np.exp(1j*phase_rad)
```

Thus, we can convert the (dB,degree) data into natural (real,imaginary) numbers

```python
S00 = dBdegree_2_natural(CDM1[:,1],CDM1[:,2])
S10 = dBdegree_2_natural(CDM1[:,3],CDM1[:,4])
S20 = dBdegree_2_natural(CDM2[:,3],CDM2[:,4])
S30 = dBdegree_2_natural(CDM3[:,3],CDM3[:,4])
```

Let's check the power conservation. If the calibration has been correctly performed and if the conduction losses are negligible, one has: $$\sum_{i=0\ldots 3} \left | S_{j0} \right|^2 = 1$$
Let's try:

```python
# Check the power conservation in dB : reflected+incident = all the power
plot(f_1/1e9, 10**(S00_dB/10)+10**(S10_dB/10)+10**(S20_dB/10)+10**(S30_dB/10))
ylim([0, 1])
```

```python
plot(f_1/1e9, sum([abs(S00)**2,abs(S10)**2,abs(S20)**2,abs(S30)**2],axis=0))
xlabel('f [GHz]')
grid('on')
ylim([0, 1])
```

We are close to 1. The difference is the conduction losses, but also the intrinsic measurement error. Let's see how much power is lost in the device in terms of percent :

```python
plot(f_1/1e9, 100*(1-sum([abs(S00)**2,abs(S10)**2,abs(S20)**2,abs(S30)**2],axis=0)))
xlabel('f [GHz]')
ylabel('Fraction of RF power lost in the device [%]')
grid('on')
```

## Electric field measurements

In the previous section we figured out that the mode converter was not working properly as a 3-way splitter; indeed the power splitting is unbalanced. It's now time to understand why, since the guy who performed the RF modeling of the stucture is very good and is sure that his design is correct. In order to dig into this problem, we propose to probe the electric field after the mode converter but before the thin septum splitter: the objective is to "see" what is the electric field topology after the mode conversion (is it as we expect it should be?). But by the way, how should it be?

If the mode converter has performed well, the totality of the electromagnetic field should behave as a $\mathrm{TE}_{30}$ mode.

```python
# Electric field measurement
# Measurement Hands-On Group #1
# Feb 2013
# columns correspond to hole rows 1,2,3
ampl_dB = -1.0*np.array([           # use a numpy array in order to avoid the caveat to multiply a list by a float
              [31.3, 32.0, 31.2],
              [30.6, 31.2, 30.2],
              [33.2, 33.3, 32.5],
              [42.8, 42.0, 41.7],
              [40.1, 41.3, 40.6],
              [32.7, 33.5, 32.7],
              [31.2, 31.4, 30.9],
              [32.5, 33.0, 32.8],
              [39.1, 39.9, 40.9],
              [46.6, 44.0, 42.0],
              [34.8, 33.2, 33.2],
              [32.4, 30.6, 30.6],
              [33.3, 30.9, 32.0]])

phase_deg = np.array(
            [[-111.7,  64.9, -119.2],
             [-111.9,  67.2, -119.9],
             [-110.7,  67.1, -119.8],
             [-99.1,   75.7, -119.7],
             [55.90, -127.4,   60.1],
             [60.4,  -123.3,   59.2],
             [64.3,  -117.4,   62.8],
             [61.3,  -119.7,   63.3],
             [50.9,  -118.3,   68.1],
             [-95.8,   70.5, -120.9],
             [-107.9,  74.1, -116.0],
             [-112.0,  71.2, -112.6],
             [-116.6,  72.4, -116.0]])
```

Let's check if we have the same number of points for amplitude and phase:

```python
shape(ampl_dB) == shape(phase_deg)
```

    True

What does it look like?

```python
f = 3.7e9 # fréquence de la mesure
a = 192e-3 # largeur grand coté du guide surdimensionné
b = 40e-3

## On ne prend que N mesures aux N abscisses de mesures
# définition des emplacements des N abscisses :
x_mes = 24e-3 + arange(0, 12*12e-3, step=12e-3) # Modele @3.7GHz d'après S.Berio
x = linspace(0, a, 100) # theorical values

## Mesures tirées de la thèse de S.Berio p.30
# definition des emplacements des 3 séries de "mesures"
z_mes = [0, 28*1e-3, 100*1e-3]

z_mes = [0, 53*1e-3, 106*1e-3]

```

```python
fig, (ax1,ax2) = plt.subplots(2,1,sharex=True)
ax1.plot(x_mes*1e3, ampl_dB, '-o')
ax1.set_ylabel('amplitude [dB]')
ax1.grid(True)
ax1.set_title('Amplitude and phase measurements (3 rows)')

ax2.plot(x_mes*1e3, phase_deg, '--.')
ax2.set_xlabel('Measurement location x [mm] #')
ax2.set_ylabel('(relative) phase [deg]')
ax2.grid(True)
ax2.set_ylim(-180, 180)

fig.savefig('mode-converter_measurements_dB.png', dpi=150)
```

In natural values

```python
measures = dBdegree_2_natural(ampl_dB, phase_deg)
```

```python
fig, (ax1,ax2) = plt.subplots(2,1,sharex=True)
ax1.plot(x_mes*1e3, real(measures),'-x')
ax1.set_ylabel('amplitude [a.u.]')
ax1.grid(True)
ax1.set_title('Amplitude and phase measurements (3 rows)')

ax2.plot(x_mes*1e3, phase_deg, '--.')
ax2.set_xlabel('Measurement location x [mm] #')
ax2.set_ylabel('(relative) phase [deg]')
ax2.grid(True)
ax2.set_ylim(-180, 180)

fig.savefig('mode-converter_measurements.png', dpi=150)

```

From the previous amplitude figures, one can remark that the measureed amplitude is not ideal: the maxima and the minima seem not have exactly the same values. Thus is seems that the mode after the mode converter is not a pure $\mathrm{TE}_{30}$, but probably a mixture of various modes. The question is : what is that mixture of modes? Our objective is to deduce that from the Efield probe data.

## Electromagnetic model

Let's define first some usefull functions:

The following function calculates the electric field at a point $(x,z)$ of the waveguide of shape $(a,b)$ for $N$ modes:
$$
E_y (x,z) = \sum_{n=1}^N a_n \sin\left(\frac{n\pi}{a}x \right) e^{-j \beta_n z}
$$
where
$$
\beta_n = \sqrt{k_0^2 - \left(\frac{n\pi}{a}\right)^2}
$$

```python
from scipy.constants import c

def Ey(a_n, x, z, wg_a=192e-3, f=3.7e9):
    '''
    Evaluates the electric field at the (x,z) location
    x and z should be scalars
    '''

    k0 = 2*pi*f/c

    sin_n = np.zeros_like(a_n, dtype='complex')
    beta_n = np.zeros_like(a_n, dtype='complex')
    exp_n = np.zeros_like(a_n, dtype='complex')
    exp_n2 = np.zeros_like(a_n, dtype='complex')

    N_modes = len(a_n)

    for n in np.arange(N_modes):
        # Guided wavenumber
        # use a negative imaginary part for the square root
        # in order to insure the convergence of the exponential
        if k0**2 - ((n+1)*pi/wg_a)**2 >= 0:
            beta_n[n] = np.sqrt(k0**2 - ((n+1)*pi/wg_a)**2)
        else:
            beta_n[n] = -1j*np.sqrt(((n+1)*pi/wg_a)**2 - k0**2)

        exp_n[n] = np.exp(-1j*beta_n[n]*z)

        sin_n[n] = np.sin((n+1)*pi/wg_a*x)

    # sum of the modes
    Ey = np.sum(a_n*sin_n*exp_n)

    return Ey
```

```python
u_n = np.array([0.5, 0, 1,])
x_test = linspace(0, 192e-3, 201)
z_test = zeros_like(x_test)
E_test = zeros_like(x_test, dtype='complex')
for idx in range(len(x_test)):
    E_test[idx] = Ey(u_n, x_test[idx], z_test[idx])

plot(x_test, real(E_test), lw=2)
xlabel('x [m]')
ylabel('|$E_y$| [a.u.]')
grid()
```

## Least Square solving with Python scipy routines

```python
print(x_mes)
```

    [0.024 0.036 0.048 0.06  0.072 0.084 0.096 0.108 0.12  0.132 0.144 0.156
     0.168]

```python
print(z_mes)
```

    [0, 0.053, 0.106]

```python
Emeas = dBdegree_2_natural(ampl_dB, phase_deg)
```

We prescribe additional information concerning the field at the edge : we know the field should be zero in x=0 and x=a.

```python
x_mes = hstack((0, x_mes, a))
Emeas = vstack((array([0,0,0]), Emeas, array([0,0,0]) ))
```

```python
# Let's reshape x_mes and z_mes vectors
# in order to get position vectors with the same length
# x -> [x1 ... x13 x1 ... x13 x1 ... x13]
# z -> [z1 ... z1  z2 ... z2  z3 ...  z3]
XX = tile(x_mes, len(z_mes))
ZZ = repeat(z_mes, len(x_mes))

# and the same for the measurements :
# Emes -> [E(x1,z1) ... E(x13,z1) E(x1,z2) ... E(x13,z2) E(x1,z3) ... E(x13,z3)]
EEmeas = reshape(Emeas, size(Emeas), order='F') # order='F' is important to get the correct ordering
```

```python
def optim_fun(a, x, z):
    Emodel = zeros_like(x, dtype='complex')

    for idx in range(len(x)):
        Emodel[idx] = Ey(a, x[idx], z[idx])

    y = EEmeas - Emodel
    return y.real**2 + y.imag**2
```

```python
from scipy.optimize import leastsq
a0 = np.array([1,0,1,0,0])
sol=leastsq(optim_fun, a0, args=(XX, ZZ))
a_sol = sol[0]
print(abs(a_sol)/norm(sol[0],1)*100)
```

    [13.76141653  3.66641546 76.34252298  3.26640217  2.96324286]

We deduce that there is 76% of TE30 mode and 13% of TE10 mode, 3% of TE40 and 50

```python
x_test = linspace(0, a, 201)

def subplote(x_test, z, a, Emes, ax):
    E_test = zeros_like(x_test, dtype='complex')

    for idx in range(len(x_test)):
        E_test[idx] = Ey(a, x_test[idx], z)

    ax.plot(x_test*1e3, real(E_test), lw=2)
    ax.plot(x_mes*1e3, real(Emes), 'x', ms=16, lw=2)
    ax.set_ylabel('|$E_y$| [a.u.]')
    ax.grid(True)

fig, axes = plt.subplots(3,1, sharex=True)
subplote(x_test, z_mes[0], a_sol, Emeas[:,0], ax=axes[0])
subplote(x_test, z_mes[1], a_sol, Emeas[:,1], ax=axes[1])
subplote(x_test, z_mes[2], a_sol, Emeas[:,2], ax=axes[2])

axes[-1].set_xlabel('x [mm]')
axes[1].legend(('Reconstruction', 'Measurement'), loc='best')
fig.tight_layout()

fig.savefig('moce_converter_reconstruction.png', dpi=150)
```

## Least Square Equation Solving (Manually)
We can also directly solve the problem, by defining
$$
\phi_{ij} = \sin\left(\frac{j \pi}{a} x_i \right) e^{-j \beta_j z_i}
$$
Then
$$
\vec{a} = \left( \phi^T \phi \right)^{-1} \phi^T \vec{E}_{meas}
$$

```python
def phi(n, x, z, wg_a=192e-3, f=3.7e9):
    k0 = 2*pi*f/c

    if k0**2 - (n*pi/wg_a)**2 >= 0:
        beta_n = np.sqrt(k0**2 - (n*pi/wg_a)**2)
    else:
        beta_n = -1j*np.sqrt((n*pi/wg_a)**2 - k0**2)

    return  np.sin(n*pi/wg_a*x) * np.exp(-1j*beta_n*z)
```

```python
MAT = np.array([phi(1, XX, ZZ), phi(2, XX, ZZ), phi(3, XX, ZZ), phi(4, XX, ZZ), phi(5, XX, ZZ)]).T
```

```python
a_sol = np.linalg.inv(np.dot(MAT.T, MAT)).dot(MAT.T).dot(EEmeas)
print(abs(a_sol)/norm(a_sol,1)*100)
```

    [ 9.0833406   3.65953167 71.65895146 13.93531265  1.66286363]

This evaluation gives 71% of TE30 mode and 9 % for TE10.

```python
x_test = linspace(0, a, 201)

def subplote(x_test, z, a, Emes):
    E_test = zeros_like(x_test, dtype='complex')

    for idx in range(len(x_test)):
        E_test[idx] = Ey(a, x_test[idx], z)

    plot(x_test, real(E_test), lw=2)
    plot(x_mes, real(Emes), 'x', ms=15, lw=2)
    xlabel('x [m]')
    ylabel('|$E_y$| [a.u.]')
    grid()

subplot(311)
subplote(x_test, z_mes[0], a_sol, Emeas[:,0])
title('z1')
subplot(312)
subplote(x_test, z_mes[1], a_sol, Emeas[:,1])
title('z2')
subplot(313)
subplote(x_test, z_mes[2], a_sol, Emeas[:,2])
title('z3')
```

## Least Square Equation Solving (with linalg.lstsq)

Same as in previous section, but using the Python library that do the job for you.

```python
from numpy.linalg import lstsq
```

```python
C, res, _, _ = lstsq(MAT, EEmeas, rcond=None)
print(abs(C)/norm(C,1)*100)
```

    [ 6.64476342  3.19279677 86.25392091  1.88080313  2.02771576]

This gives 86% as TE30 mode.

```python
x_test = linspace(0, a, 201)

def subplote(x_test, z, a, Emes):
    E_test = zeros_like(x_test, dtype='complex')

    for idx in range(len(x_test)):
        E_test[idx] = Ey(a, x_test[idx], z)

    plot(x_test, real(E_test), lw=2)
    plot(x_mes, real(Emes), 'x', ms=15, lw=2)
    xlabel('x [m]')
    ylabel('|$E_y$| [a.u.]')
    grid()

subplot(311)
subplote(x_test, z_mes[0], C, Emeas[:,0])
title('z1')
subplot(312)
subplote(x_test, z_mes[1], C, Emeas[:,1])
title('z2')
subplot(313)
subplote(x_test, z_mes[2], C, Emeas[:,2])
title('z3')
```

## 2D plot view

```python
x = linspace(0, a, 201)
z = linspace(-0.1e-2, 15e-2, 301)

XX, ZZ = meshgrid(x,z)
XX2 = reshape(XX, len(x)*len(z))
ZZ2 = reshape(ZZ, len(x)*len(z))
E_2D = zeros_like(XX2, dtype='complex')
for idx in range(len(XX2)):
    E_2D[idx] = Ey(a_sol, XX2[idx], ZZ2[idx])

E_2D = reshape(E_2D, (len(z), len(x)))
```

```python
pcolormesh(ZZ, XX, real(E_2D))
xlabel('z [m]', size=14)
ylabel('x [m]', size=14)
for idx in range(3):
    axvline(z_mes[idx], color='k', ls='--', lw=2)
xlim(z[0], z[-1])
ylim(x[0], x[-1])
```

# Finding the mode content using the Fast Fourier Transform

An other solution could be to deduce the mode content from a Fourier analysis of the electric field.

We recall that the total electric field measured on a row $\ell={1,3}$ is :
$$
E_{tot}(x,z_\ell) = \sum_{m=1}^M E_m \sin\left( \frac{m\pi}{a} x\right) e^{-j\beta_m z} \;\;, x \in [0,a]
$$
where $E_m$ is complex valued, ie. $E_m=A_m e^{j\phi_m}$. Our goal is to deduce these coefficients $E_m$, from the measurements $E_{tot}$.

The Fourier transform of the field is defined by:
$$
\tilde{E}_{tot} =
\frac{1}{2\pi}
\iint E_{tot}(x,z) e^{j k_x x + j k_z z} \,dx \, dz
$$
which leads to:
$$
\tilde{E}_{tot}(k_x,k_z) =
\frac{1}{2\pi}
\sum_{m=1}^M E_m
\int_z \int_{x=0}^a
\sin\left( \frac{m\pi}{a} x\right) e^{j k_x x + j (k_z-\beta_m)  z} \,dx \, dz
$$

$$
\tilde{E}_{tot}(k_x,k_z) =
\frac{1}{2\pi}
\sum_{m=1}^M E_m
\int_z  e^{j (k_z-\beta_m)  z} \, dz
\cdot
\int_{x=0}^a\sin\left( \frac{m\pi}{a} x\right) e^{j k_x x} \,dx
$$

So,

$$
\tilde{E}_{tot}(k_x,k_z) =
\sum_{m=1}^M E_m
\delta(k_z-\beta_m)
\frac{\frac{m\pi}{a}}{k_x^2 - \left(\frac{m\pi}{a}\right)^2 }
\left(
(-1)^m e^{j k_x a} -1
\right)
$$

where we used the following results:
$$
\int_{x=0}^a\sin\left( \frac{m\pi}{a} x\right) e^{j k_x x} \,dx
=
\frac{\frac{m\pi}{a}}{k_x^2 - \left(\frac{m\pi}{a}\right)^2 }
\left(
(-1)^m e^{j k_x a} -1
\right)
$$
and:
$$
\int_z  e^{j (k_z-\beta_m)  z} \, dz
=
2\pi \delta(k_z-\beta_m)
$$

Need a proof? Look at the end of this notebook.

The later formula can be simplified for even or odd $m$ values, but it can also be implemented directly:

```python
def Etilde_analytic(kx, Em, a):
    Etilde = np.zeros_like(kx, dtype='complex')

    for m, Ei in enumerate(Em, start=1):
        Etilde += Ei \
            * ((-1)**(m) * np.exp(1j*kx*a) - 1) \
            * (m*pi/a)/ (kx**2 - (m*pi/a)**2)

    return Etilde
```

```python
# Ideal spectrum
ky_ana = np.linspace(0, 7*pi/a, num=101)
Etilde_ideal = Etilde_analytic(ky_ana, array([0.2, 0, 0.5, 0, 0.1]), a)

fig, ax = plt.subplots()
ax.plot(ky_ana, np.abs(Etilde_ideal))

# shows where the modes 1,2,... are
for mode_index in range(8):
    axvline(mode_index*pi/a, color='#888888', linestyle='--')
xticks(arange(0,8)*pi/a, ('0', '$\pi/a$', '$2\pi/a$', '$3\pi/a$', '$4\pi/a$', '$5\pi/a$', '$6\pi/a$', '$7\pi/a$'), size=16)
ax.set_xlabel('$k_y$', size=16)
```

Clearly, there is something strange wite the analytical spectrum! This is in fact normal, since the initial field "wideband" (from $x\in [0,a]$) is not large enough, which leads to a reduced precision in spectral dimension. So the solution would be to consider that the field is not terminated at the boundaries $x=0$ and $x=a$ and instead to consider it as infinite (undefinite Fourier integral):

$$
\tilde{E}_{tot}(k_x,k_z) =
\frac{1}{2\pi}
\sum_{m=1}^M E_m
\int_z  e^{j (k_z-\beta_m)  z} \, dz
\cdot
\int_{x=-\infty}^{+\infty} \sin\left( \frac{m\pi}{a} x\right) e^{j k_x x} \,dx
$$

Now we calculates the spectrum of the field from the spatial fields, using the Fast Fourier Transform:

```python
# index of the measurement row (0, 1 or 2)
# TODO : find a way to use all the rows ?
index_row = 0

x_mes2 = x_mes
E_mes2 = Emeas[:,index_row]

# interpolating the measurements in order to have smoother initial values
from scipy.interpolate import InterpolatedUnivariateSpline
ius_re = InterpolatedUnivariateSpline(x_mes2, real(E_mes2))
ius_im = InterpolatedUnivariateSpline(x_mes2, imag(E_mes2))

x_mes3 = linspace(x_mes2[0], x_mes2[-1], 101)
E_mes3 = ius_re(x_mes3) + 1j*ius_im(x_mes3)

# Padding the measurements by replicating the data,
# in order to have a better fourier domain precision
# (the larger spatial wideband, the better the precision)
# The trick is to replicate correctly, taking into account the symmetry of the field
x_mes4 = np.pad(x_mes3, (2**13,), 'reflect', reflect_type='odd')
E_mes4 = np.pad(E_mes3, (2**13,), 'reflect', reflect_type='odd')
```

```python
fig, ax = plt.subplots(2,1)
ax[0].plot(x_mes4, real(E_mes4), lw=2) # replicated interpolated data
ax[0].plot(x_mes3, real(E_mes3), lw=2) # interpolated data
ax[0].plot(x_mes, real(Emeas[:,index_row]), 'x', ms=8) # initial data
ax[0].set_xlim(-3*a, 4*a)
ax[0].axvline(0, color='#999999')
ax[0].axvline(a, color='#999999')
ax[0].set_ylabel('real E')

ax[1].plot(x_mes4, imag(E_mes4), lw=2) # replicated interpolated data
ax[1].plot(x_mes3, imag(E_mes3), lw=2) # interpolated data
ax[1].plot(x_mes, imag(Emeas[:,index_row]), 'x', ms=8)
ax[1].set_xlim(-3*a, 4*a)
ax[1].axvline(0, color='#999999')
ax[1].axvline(a, color='#999999')
ax[1].set_xlabel('x [m]')
ax[1].set_ylabel('imag E')
```

```python
from numpy.fft import fft, fftshift, fftfreq
```

```python
# Calcul the numerical spectrum
ky_num = 2*pi*fftshift(fftfreq(len(x_mes4), d=x_mes4[1]-x_mes4[0]))
Etilde_num = fftshift(fft(E_mes4))

fig,ax=subplots()
ax.plot(ky_num, abs(Etilde_num)/max(abs(Etilde_num)), lw=2, color='r')
ax.set_xlim(0, 1.5)

# shows where the modes 1,2,... are
for mode_index in range(8):
    ax.axvline(mode_index*pi/a, color='#888888', linestyle='--')
ax.set_xticks(np.arange(0,8)*pi/a)
ax.set_xticklabels([0] + [f'${m}\pi/a$' for m in range(1,8)])
ax.set_xlabel('$k_y$', size=16)

# TODO : calculates the relative height of the peak to deduce the mode % content
```

Using this technique, we clearly see the TE30 mode is the dominant one.

# Using orthonormalization properties
In this section we use the fact that waveguide modes form a complete spectrum. The mode base is orthonormal, thus that:
$$
\int_0^a
\sin\left( \frac{m \pi}{a} x \right)
\sin\left( \frac{n \pi}{a} x \right)
dx =
\left\{
\begin{array}{lr}
a/2 & \mathrm{if} \; m=n \\
0 & \mathrm{if}  \; m\neq n
\end{array}
\right.
$$

For a given $z$, we such multiply by $\sin(m \pi/a)$ and integrate between $[0,a]$ the measured data:

```python
def interpolate_measurements(row_index, num_points=501):
    # Add two points, at x=0 and x=a, to the measurements.
    # They are the edges of the waveguides, thus the field is zero here.
    x = x_mes
    E = Emeas[:,row_index]

    # interpolating the measurements in order to have smoother initial values
    from scipy.interpolate import InterpolatedUnivariateSpline
    ius_re = InterpolatedUnivariateSpline(x, real(E))
    ius_im = InterpolatedUnivariateSpline(x, imag(E))

    x2 = linspace(x[0], x[-1], num_points)
    E2 = ius_re(x2) + 1j*ius_im(x2)

    return x2, E2

x2, E2 = interpolate_measurements(1)
plot(x2, real(E2), x2, imag(E2))
xlabel('x [m]')
title('interpolated measurements')
```

```python
def orthonorm(x, E, number_of_modes=5):
    Im = []
    for m in arange(1, number_of_modes+1):
        integrande = E * sin(m*pi/a*x)
        Im.append(2/a*trapz(x, integrande))
    return asarray(Im)
```

```python
colors = ['b','g','r']
for idx_row in [0,1,2]:
    x2, E2 = interpolate_measurements(idx_row)
    Im = orthonorm(x2, E2)
    scatter(arange(1,len(Im)+1), abs(Im), color=colors[idx_row-1])
    print(abs(Im)/norm(Im,1)*100)
grid(True)
xlabel('m', size=16)
ylim(0,0.04)

```

We can see on the picture above that the TE30 mode is dominant, but that there is some TE10 and other mode also present. Almost 81 to 85% of the TE30 modes, and between 5-8% for the TE10 mode.

# Solving a linear system

This approach is derived from A.G.Bailey et al. paper, _Experimental Determination of Higher Order Mode Conversion in a Multimode Waveguide_. Here, the electric field in the waveguide is supposed to be :

$$
E_y(x,z) =
\sum_{m=1}^4
a_m \sin\left(\frac{m\pi}{a}x\right) e^{- j\beta_m z}
+
b_m \sin\left(\frac{m\pi}{a}x\right) e^{+ j\beta_m z}
$$

Since measurements give both real and imaginary parts of the previous expression, in the form of magnitude $A(x,z)$ and phase $\gamma(x,z)$, one has:

$$
A(x,z) \cos\gamma(x,z) =
\sum_{m=1}^4
|a_m|
\sin\left(\frac{m\pi}{a}x\right)
\cos\left(\theta_m - \beta_m z\right)
+
|b_m|
\sin\left(\frac{m\pi}{a}x\right)
\cos\left(\theta_m - \beta_m z\right)
$$

and

$$
A(x,z) \sin\gamma(x,z) =
\sum_{m=1}^4
|a_m| \sin\left(\frac{m\pi}{a}x\right) \sin\left(\theta_m - \beta_m z\right)
+
|b_m| \sin\left(\frac{m\pi}{a}x\right) \sin\left(\theta_m - \beta_m z\right)
$$

# Appendix

## Fourier transform integral calculation

Here, using [SymPy](http://www.sympy.org) we want to calculate the integral:
$$
\int_{x=0}^a\sin\left( \frac{m\pi}{a} x\right) e^{j k_x x} \,dx
$$

```python
import sympy as s
s.init_printing() # render formula nicely
```

```python
x_, k_x_ = s.symbols('x k_x', real=True)
a_ = s.symbols('a', positive=True)
m_ = s.symbols('m', integer=True, positive=True)
I = s.integrate( s.sin(m_*s.pi/a_*x_) * s.exp(s.I*k_x_*x_), (x_, 0, a_))
I.simplify()
```

The latter can be expressed as:
$$
\frac{\frac{m\pi}{a}}{k_x^2 - \left(\frac{m\pi}{a}\right)^2 }
\left(
(-1)^m e^{j k_x a} -1
\right)
$$

In the case of an undefinite integral ($x\in\mathbb{R}$):

```python
# Unfortunately, SymPy does not know how to compute the FT of a sinus :
s.fourier_transform(s.sin(m_*pi/a_*x_),x_, k_x_, noconds=True)
```

And we recall the definition of the Dirac Delta function:

$$
\int_z  e^{j (k_z-\beta_m)  z} \, dz
=
2\pi \delta(k_z-\beta_m)
$$

## Numerical Fourier Transform - option 2

```python
# This function calculates the FFT of the field
# and the corresponding wavenumber axis.
# This function is not used in this notebook, and just given here for example.
# (The wavenumber axis can be constructed instead with the fftfreq function)
def calculate_spectrum(x, E, f=3.7e9):
    k0 = 2*pi*f/c
    lambda0 = c/f
    # fourier domain points
    B = 2**18
    Efft = np.fft.fftshift(np.fft.fft(E,B))

    # fourier domain bins
    dx = x[1] - x[0] # assumes spatial period is constant
    df = 1/(B*dx)
    K = arange(-B/2,+B/2)
    # spatial frequency bins
    Fz= K*df
    # spatial wavenumber kx
    kx= (2*pi)*Fz
    # "power density" spectrum
    p = (dx)**2/lambda0 * Efft;

    return(kx,p)
```

### CSS Styling

```python
from IPython.core.display import HTML
def css_styling():
    styles = open("../../styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

```python

```


This notebook collects algorithms for evolving or evaluating (in nonprecessing cases) the orbital evolution of the PN system.  The primary approximants are denoted TaylorT$n$ for $n \in [1,5]$.  "Taylor" means that they are Taylor series (as opposed to Padé), "T" means that they are in the time domain (as opposed to frequency), and $n$ just numbers the order in which they were established in the literature.  The other important approximant is TaylorF2, which is based on TaylorT2, but is in the frequency domain.

```python
# Always run this first
# NOTE: Do not define new basic variables in this notebook;
#       define them in Variables_Q.ipynb.  Use this notebook
#       to define new expressions built from those variables.

import sympy
from sympy import *
from __future__ import division # This needs to be here, even though it's in Variables_Q.ipynb
import sys
sys.path.insert(0, '..') # Look for modules in directory above this one
import pickle
execfile('../Utilities/ExecNotebook.ipy')
try:
    UseQuaternions
    VariablesNotebook = 'Variables_Q.ipynb' if UseQuaternions else 'Variables_Q.ipynb'
except:
    UseQuaternions = False
    VariablesNotebook = 'Variables_Q.ipynb'
execnotebook(VariablesNotebook)
from Utilities import CodeOutput
```

    /home/dongze/.local/lib/python3.8/site-packages/IPython/nbformat.py:12: ShimWarning: The `IPython.nbformat` package has been deprecated since IPython 4.0. You should import from nbformat instead.
      warn("The `IPython.nbformat` package has been deprecated since IPython 4.0. "
    /home/dongze/.local/lib/python3.8/site-packages/nbformat/current.py:15: UserWarning: nbformat.current is deprecated.

    - use nbformat for read/write/validate public API
    - use nbformat.vX directly to composing notebooks of a particular version

      warnings.warn("""nbformat.current is deprecated.

Here, we set the PN order, if it is not already set.  This will be used in numerous places below.  This is the exponent of the largest power of $x$, or half the exponent of the largest power of $v$ that will appear beyond leading orders in the various quantities.  Note that, because of python's convention that intervals are half-open at the end, most occurrences of `PNOrbitalEvolutionOrder` in the code will have 1 added to them; the actual value of `PNOrbitalEvolutionOrder` will be what we normally expect.

```python
if 'PNOrbitalEvolutionOrder' not in globals():
    PNOrbitalEvolutionOrder = frac(7,2)
```

# TaylorT1, TaylorT4, and TaylorT5*

These very similar approximants are the simplest in construction, and most widely applicable.  In particular, they can both be applied to precessing systems.  Each gives rise to the same system of ODEs that need to be integrated in time, except that the right-hand side for $dv/dt$ is expanded as a series in $v$ and truncated for TaylorT4.  TaylorT5 extends that simply.

***** The version of TaylorT5 output below is slightly different from the one introduced by Ajith.  He further solved analytically for the orbital phase $\Phi$ in terms of $v$.  This doesn't appear to be possible in the precessing case, since terms such as $\vec{\Sigma} \cdot \hat{L}_{\text{N}}$ now vary with $v$ in nontrivial ways; I believe uses of T5 for precessing systems assume that such terms are constant.  Besides, we have 11 variables to integrate in addition to $\Phi$, so it's not much extra burden to integrate it too.  For nonprecessing systems, similar code could be generated for the non-precessing case using Ajith's solution for $\Phi$; the value of `y[1]` would have to be reset explicitly at the top of the `TaylorT4RHS` function emitted below.

First, we collect the various expressions from other notebooks:

```python
execnotebook('BindingEnergy.ipynb')
execnotebook('EnergyAbsorption.ipynb')
execnotebook('Precession.ipynb')
```

Next, we calculate the expansions needed for TaylorT4 and T5.  These will be the right-hand sides in our evolution equations for $dv/dt$.  TaylorT1 simply numerically evaluates a ratio of the terms imported above

```python
# Read in the high-order series expansion of a ratio of polynomials
with open('PolynomialRatios/PolynomialRatioSeries_Order{0}.dat'.format(2*PNOrbitalEvolutionOrder+1), 'rb') as fff:
    p_Ratio = pickle.load(fff)
display(p_Ratio)
p_Ratio = (p_Ratio.removeO()).subs('PolynomialVariable',v)
display(p_Ratio)

#p_Ratio = pickle.load(file('PolynomialRatios/PolynomialRatioSeries_Order{0}.dat'.format(2*PNOrbitalEvolutionOrder+1)))
#p_Ratio = p_Ratio.removeO().subs('PolynomialVariable',v)

# Evaluate the flux, energy, and derivative of energy
FluxTerms = [Flux_NoSpin, Flux_Spin]
BindingEnergyTerms = [BindingEnergy_NoSpin, BindingEnergy_Spin]
for Term in FluxTerms:
    PNVariables.update(Term)
for Term in BindingEnergyTerms:
    PNVariables.update(Term)
Flux = FluxExpression(FluxTerms, PNOrbitalEvolutionOrder)
Energy = BindingEnergyExpression(BindingEnergyTerms, PNOrbitalEvolutionOrder)
dEdV = BindingEnergyDerivativeExpression(BindingEnergyTerms, PNOrbitalEvolutionOrder)

# Evaluate the energy absorption by the BHs, and make substitutions so that the Horner form is nice
AbsorptionTerms = [AlviTerms]
for Term in AbsorptionTerms:
    PNVariables.update(Term)
Absorption = AbsorptionExpression(AbsorptionTerms, PNOrbitalEvolutionOrder)

# Treat remaining log(v) terms as constants, for Taylor expansions and efficient numerical evaluation
Flux = Flux.subs(log(v), logv)
Energy = Energy.subs(log(v), logv)
dEdV = dEdV.subs(log(v), logv)
Absorption = Absorption.subs(log(v), logv)

# Get the series expansions for the numerators and denominators
FluxSeries = series(- (Flux + Absorption)/Fcal_coeff, x=v, x0=0, n=2*PNOrbitalEvolutionOrder+1).removeO()
dEdVSeries = series(dEdV/(-nu*v/2), x=v, x0=0, n=2*PNOrbitalEvolutionOrder+1).removeO()

# TaylorT4
T4Expressions = PNCollection()
NumTerms4 = {'Num{0}'.format(n): FluxSeries.coeff(v,n=n) for n in range(2*PNOrbitalEvolutionOrder+1)}
DenTerms4 = {'Den{0}'.format(n): dEdVSeries.coeff(v,n=n) for n in range(2*PNOrbitalEvolutionOrder+1)}
T4Expressions.AddDerivedConstant('dvdt_T4', (Fcal_coeff/(-nu*v/2))*\
    horner(sum([v**n*horner(N(p_Ratio.coeff(v,n=n).subs(dict(NumTerms4.items() | DenTerms4.items()))))
                for n in range(2*PNOrbitalEvolutionOrder+1)])))

# TaylorT5
T5Expressions = PNCollection()
NumTerms5 = {'Num{0}'.format(n): dEdVSeries.coeff(v,n=n) for n in range(2*PNOrbitalEvolutionOrder+1)}
DenTerms5 = {'Den{0}'.format(n): FluxSeries.coeff(v,n=n) for n in range(2*PNOrbitalEvolutionOrder+1)}
T5Expressions.AddDerivedConstant('dtdv', ((-nu*v/2)/Fcal_coeff)*\
    horner(sum([v**n*horner(N(p_Ratio.coeff(v,n=n).subs(dict(NumTerms5.items() | DenTerms5.items()))))
                for n in range(2*PNOrbitalEvolutionOrder+1)])))
T5Expressions.AddDerivedConstant('dvdt_T5', 1.0/dtdv)

# TaylorT1 just gets some substitutions for efficiency
T1Expressions = PNCollection()
T1Expressions.AddDerivedConstant('Flux', Flux.subs(Pow(nu,3), nu__3).subs(Pow(nu,2), nu__2))
T1Expressions.AddDerivedConstant('dEdV', dEdV.subs(Pow(nu,3), nu__3).subs(Pow(nu,2), nu__2))
T1Expressions.AddDerivedConstant('Absorption', Absorption.subs(Pow(nu,3), nu__3).subs(Pow(nu,2), nu__2))
T1Expressions.AddDerivedConstant('dvdt_T1', - (Flux + Absorption) / dEdV)
```

Now, the precession terms:

```python
PrecessionVelocities = PNCollection()

PrecessionVelocities.AddDerivedVariable('OmegaVec_chiVec_1',
                                        Precession_chiVec1Expression(PNOrbitalEvolutionOrder),
                                        datatype=ellHat.datatype)
PrecessionVelocities.AddDerivedVariable('OmegaVec_chiVec_2',
                                        Precession_chiVec2Expression(PNOrbitalEvolutionOrder),
                                        datatype=ellHat.datatype)
PrecessionVelocities.AddDerivedVariable(('OmegaVec' if UseQuaternions else 'OmegaVec_ellHat'),
                                        Precession_ellHatExpression(PNOrbitalEvolutionOrder)*nHat
                                        + ((v**3/M)*ellHat if UseQuaternions else 0),
                                        datatype=nHat.datatype)
```

```python
CodeConstructor = CodeOutput.CodeConstructor(PNVariables, T1Expressions)
for Terms in BindingEnergyTerms+FluxTerms+[AlviTerms]+[Precession_ellHat, Precession_chiVec1, Precession_chiVec2]:
    CodeConstructor.AddDependencies(Terms)
CodeConstructor.AddDependencies(PrecessionVelocities)
```

# TaylorT2 and TaylorT3

These two approximants are also closely related to each other.  I believe they are only possible for nonprecessing systems, without further assumptions about the constancy of certain projections of the spins.  I choose not to make those assumptions.

```python

```

# TaylorF2

Being based on the `TaylorT2` approximant, I think this approximant is only possible for nonprecessing systems without further assumptions.

```python

```


# The Semi-Supervised VAE

## Introduction

Most of the models we've covered in the tutorials are unsupervised:

- [Variational Autoencoder (VAE)](vae.ipynb)
- [DMM](dmm.ipynb)
- [Attend-Infer-Repeat](air.ipynb)

We've also covered a simple supervised model:

- [Bayesian Regression](bayesian_regression.ipynb)

The semi-supervised setting represents an interesting intermediate case where some of the data is labeled and some is not. It is also of great practical importance, since we often have very little labeled data and much more unlabeled data. We'd clearly like to leverage labeled data to improve our models of the unlabeled data.

The semi-supervised setting is also well suited to generative models, where missing data can be accounted for quite naturally—at least conceptually.
As we will see, in restricting our attention to semi-supervised generative models, there will be no shortage of different model variants and possible inference strategies.
Although we'll only be able to explore a few of these variants in detail, hopefully you will come away from the tutorial with a greater appreciation for the abstractions and modularity offered by probabilistic programming.

So let's go about building a generative model. We have a dataset
$\mathcal{D}$ with $N$ datapoints,

$$ \mathcal{D} = \{ ({\bf x}_i, {\bf y}_i) \} $$

where the $\{ {\bf x}_i \}$ are always observed and the labels $\{ {\bf y}_i \}$ are only observed for some subset of the data. Since we want  to be able to model complex variations in the data, we're going to make this a latent variable model with a local latent variable ${\bf z}_i$ private to each pair $({\bf x}_i, {\bf y}_i)$. Even with this set of choices, a number of model variants are possible: we're going to focus on the model variant depicted in Figure 1 (this is model M2 in reference [1]).

 Figure 1: our semi-supervised generative model (c.f. model M2 in reference [1])

For convenience—and since we're going to model MNIST in our experiments below—let's suppose the $\{ {\bf x}_i \}$ are images and the $\{ {\bf y}_i \}$ are digit labels. In this model setup, the latent random variable ${\bf z}_i$ and the (partially observed) digit label _jointly_ generate the observed image.
The ${\bf z}_i$ represents _everything but_ the digit label, possibly handwriting style or position.
Let's sidestep asking when we expect this particular factorization of $({\bf x}_i, {\bf y}_i, {\bf z}_i)$ to be appropriate, since the answer to that question will depend in large part on the dataset in question (among other things). Let's instead highlight some of the ways that inference in this model will be challenging as well as some of the solutions that we'll be exploring in the rest of the tutorial.

## The Challenges of Inference

For concreteness we're going to continue to assume that the partially-observed $\{ {\bf y}_i \}$ are discrete labels; we will also assume that the $\{ {\bf z}_i \}$ are continuous.

- If we apply the general recipe for stochastic variational inference to our model (see [SVI Part I](svi_part_i.ipynb)) we would be sampling the discrete (and thus non-reparameterizable) variable ${\bf y}_i$ whenever it's unobserved. As discussed in [SVI Part III](svi_part_iii.ipynb) this will generally lead to high-variance gradient estimates.
- A common way to ameliorate this problem—and one that we'll explore below—is to forego sampling and instead sum out all ten values of the class label ${\bf y}_i$ when we calculate the ELBO for an unlabeled datapoint ${\bf x}_i$. This is more expensive per step, but can help us reduce the variance of our gradient estimator and thereby take fewer steps.
- Recall that the role of the guide is to 'fill in' _latent_ random variables. Concretely, one component of our guide will be a digit classifier $q_\phi({\bf y} | {\bf x})$ that will randomly 'fill in' labels $\{ {\bf y}_i \}$ given an image $\{ {\bf x}_i \}$. Crucially, this means that the only term in the ELBO that will depend on $q_\phi(\cdot | {\bf x})$ is the term that involves a sum over _unlabeled_ datapoints. This means that our classifier $q_\phi(\cdot | {\bf x})$—which in many cases will be the primary object of interest—will not be learning from the labeled datapoints (at least not directly).
- This seems like a potential problem. Luckily, various fixes are possible. Below we'll follow the approach in reference [1], which involves introducing an additional objective function for the classifier to ensure that the classifier learns directly from the labeled data.

We have our work cut out for us so let's get started!

## First Variant: Standard objective function, naive estimator

As discussed in the introduction, we're considering the model depicted in Figure 1. In more detail, the model has the following structure:

- $p({\bf y}) = Cat({\bf y}~|~{\bf \pi})$: multinomial (or categorical) prior for the class label
- $p({\bf z}) = \mathcal{N}({\bf z}~|~{\bf 0,I})$: unit normal prior for the latent code $\bf z$
- $p_{\theta}({\bf x}~|~{\bf z,y}) = Bernoulli\left({\bf x}~|~\mu\left({\bf z,y}\right)\right)$: parameterized Bernoulli likelihood function; $\mu\left({\bf z,y}\right)$ corresponds to `decoder` in the code

We structure the components of our guide $q_{\phi}(.)$ as follows:

- $q_{\phi}({\bf y}~|~{\bf x}) = Cat({\bf y}~|~{\bf \alpha}_{\phi}\left({\bf x}\right))$: parameterized multinomial (or categorical) distribution; ${\bf \alpha}_{\phi}\left({\bf x}\right)$ corresponds to `encoder_y` in the code
- $q_{\phi}({\bf z}~|~{\bf x, y}) = \mathcal{N}({\bf z}~|~{\bf \mu}_{\phi}\left({\bf x, y}\right), {\bf \sigma^2_{\phi}\left(x, y\right)})$: parameterized normal distribution; ${\bf \mu}_{\phi}\left({\bf x, y}\right)$ and ${\bf \sigma^2_{\phi}\left(x, y\right)}$ correspond to the neural digit classifier `encoder_z` in the code

These choices reproduce the structure of model M2 and its corresponding inference network in reference [1].

We translate this model and guide pair into Pyro code below. Note that:

- The labels `ys`, which are represented with a one-hot encoding, are only partially observed (`None` denotes unobserved values).

- `model()` handles both the observed and unobserved case.

- The code assumes that `xs` and `ys` are mini-batches of images and labels, respectively, with the size of each batch denoted by `batch_size`.

```python
def model(self, xs, ys=None):
    # register this pytorch module and all of its sub-modules with pyro
    pyro.module("ss_vae", self)
    batch_size = xs.size(0)

    # inform Pyro that the variables in the batch of xs, ys are conditionally independent
    with pyro.plate("data"):

        # sample the handwriting style from the constant prior distribution
        prior_loc = xs.new_zeros([batch_size, self.z_dim])
        prior_scale = xs.new_ones([batch_size, self.z_dim])
        zs = pyro.sample("z", dist.Normal(prior_loc, prior_scale).to_event(1))

        # if the label y (which digit to write) is supervised, sample from the
        # constant prior, otherwise, observe the value (i.e. score it against the constant prior)
        alpha_prior = xs.new_ones([batch_size, self.output_size]) / (1.0 * self.output_size)
        ys = pyro.sample("y", dist.OneHotCategorical(alpha_prior), obs=ys)

        # finally, score the image (x) using the handwriting style (z) and
        # the class label y (which digit to write) against the
        # parametrized distribution p(x|y,z) = bernoulli(decoder(y,z))
        # where `decoder` is a neural network
        loc = self.decoder([zs, ys])
        pyro.sample("x", dist.Bernoulli(loc).to_event(1), obs=xs)

def guide(self, xs, ys=None):
    with pyro.plate("data"):
        # if the class label (the digit) is not supervised, sample
        # (and score) the digit with the variational distribution
        # q(y|x) = categorical(alpha(x))
        if ys is None:
            alpha = self.encoder_y(xs)
            ys = pyro.sample("y", dist.OneHotCategorical(alpha))

        # sample (and score) the latent handwriting-style with the variational
        # distribution q(z|x,y) = normal(loc(x,y),scale(x,y))
        loc, scale = self.encoder_z([xs, ys])
        pyro.sample("z", dist.Normal(loc, scale).to_event(1))
```

### Network Definitions

In our experiments we use the same network configurations as used in reference [1]. The encoder and decoder networks have one hidden layer with $500$ hidden units and softplus activation functions. We use softmax as the activation function for the output of `encoder_y`, sigmoid as the output activation function for `decoder` and exponentiation for the scale part of the output of `encoder_z`. The latent dimension is 50.

### MNIST Pre-Processing

We normalize the pixel values to the range $[0.0, 1.0]$. We use the [MNIST data loader](http://pytorch.org/docs/master/torchvision/datasets.html#torchvision.datasets.MNIST) from the `torchvision` library. The testing set consists of $10000$ examples. The default training set consists of $60000$ examples. We use the first $50000$ examples for training (divided into supervised and un-supervised parts) and the remaining $10000$ images for validation. For our experiments, we use $4$ configurations of supervision in the training set, i.e. we consider $3000$, $1000$, $600$ and $100$ supervised examples selected randomly (while ensuring that each class is balanced).

### The Objective Function

The objective function for this model has the two terms (c.f. Eqn. 8 in reference [1]):

$$\mathcal{J} = \!\!\sum_{({\bf x,y}) \in \mathcal{D}_{supervised} } \!\!\!\!\!\!\!\!\mathcal{L}\big({\bf x,y}\big) +\!\!\! \sum_{{\bf x} \in \mathcal{D}_{unsupervised}} \!\!\!\!\!\!\!\mathcal{U}\left({\bf x}\right)
$$

To implement this in Pyro, we setup a single instance of the `SVI` class. The two different terms in the objective functions will emerge automatically depending on whether we pass the `step` method labeled or unlabeled data. We will alternate taking steps with labeled and unlabeled mini-batches, with the number of steps taken for each type of mini-batch depending on the total fraction of data that is labeled. For example, if we have 1,000 labeled images and 49,000 unlabeled ones, then we'll take 49 steps with unlabeled mini-batches for each labeled mini-batch. (Note that there are different ways we could do this, but for simplicity we only consider this variant.) The code for this setup is given below:

```python
from pyro.infer import SVI, Trace_ELBO, TraceEnum_ELBO, config_enumerate
from pyro.optim import Adam

# setup the optimizer
adam_params = {"lr": 0.0003}
optimizer = Adam(adam_params)

# setup the inference algorithm
svi = SVI(model, guide, optimizer, loss=Trace_ELBO())
```

When we run this inference in Pyro, the performance seen during test time is degraded by the noise inherent in the sampling of the categorical variables (see Figure 2 and Table 1 at the end of this tutorial). To deal with this we're going to need a better ELBO gradient estimator.

        Figure 2: Variant 1 (Left) Training losses for the case with 3000 supervised examples.
        (Right) Test and validation accuracies.

## Interlude: Summing Out Discrete Latents

As highlighted in the introduction, when the discrete latent labels ${\bf y}$ are not observed, the ELBO gradient estimates rely on sampling from $q_\phi({\bf y}|{\bf x})$. These gradient estimates can be very high-variance, especially early in the learning process when the guessed labels are often incorrect. A common approach to reduce variance in this case is to sum out discrete latent variables, replacing the Monte Carlo expectation

$$\mathbb E_{{\bf y}\sim q_\phi(\cdot|{\bf x})}\nabla\operatorname{ELBO}$$

with an explicit sum

$$\sum_{\bf y} q_\phi({\bf y}|{\bf x})\nabla\operatorname{ELBO}$$

This sum is usually implemented by hand, as in [1], but Pyro can automate this in many cases. To automatically sum out all discrete latent variables (here only ${\bf y}$), we simply wrap the guide in `config_enumerate()`:
```python
svi = SVI(model, config_enumerate(guide), optimizer, loss=TraceEnum_ELBO(max_plate_nesting=1))
```
In this mode of operation, each `svi.step(...)` computes a gradient term for each of the ten latent states of $y$. Although each step is thus $10\times$ more expensive, we'll see that the lower-variance gradient estimate outweighs the additional cost.

Going beyond the particular model in this tutorial, Pyro supports summing over arbitrarily many discrete latent variables. Beware that the cost of summing is exponential in the number of discrete variables, but is cheap(er) if multiple independent discrete variables are packed into a single tensor (as in this tutorial, where the discrete labels for the entire mini-batch are packed into the single tensor ${\bf y}$). To use this parallel form of `config_enumerate()`, we must inform Pyro that the items in a minibatch are indeed independent by wrapping our vectorized code in a `with plate("name")` block.

## Second Variant: Standard Objective Function, Better Estimator

Now that we have the tools to sum out discrete latents, we can see if doing so helps our performance. First, as we can see from Figure 3, the test and validation accuracies now evolve much more smoothly over the course of training. More importantly, this single modification improved test accuracy from around `20%` to about `90%` for the case of $3000$ labeled examples. See Table 1 for the full results. This is great, but can we do better?

        Figure 3: Variant 2 (Left) Training losses for the case with 3000 supervised examples.
        (Right) Test and validation accuracies.

## Third Variant: Adding a Term to the Objective

For the two variants we've explored so far, the classifier $q_{\phi}({\bf y}~|~ {\bf x})$ doesn't learn directly from labeled data. As we discussed in the introduction, this seems like a potential problem. One approach to addressing this problem is to add an extra term to the objective so that the classifier learns directly from labeled data. Note that this is exactly the approach adopted in reference [1] (see their Eqn. 9). The modified objective function is given by:

$$
\begin{align}
    \mathcal{J}^{\alpha} &= \mathcal{J} + \alpha \mathop{\mathbb{E}}_{\tilde{p_l}({\bf x,y})} \big[-\log\big(q_{\phi}({\bf y}~|~ {\bf x})\big)\big] \\
    &= \mathcal{J} + \alpha' \sum_{({\bf x,y}) \in \mathcal{D}_{supervised}}  \big[-\log\big(q_{\phi}({\bf y}~|~ {\bf x})\big)\big]
\end{align}
$$

where $\tilde{p_l}({\bf x,y})$ is the empirical distribution over the labeled (or supervised) data and $\alpha' \equiv \frac{\alpha}{|\mathcal{D}_{supervised}|}$. Note that we've introduced an arbitrary hyperparameter $\alpha$ that modulates the importance of the new term.

To learn using this modified objective in Pyro we do the following:

- We use a new model and guide pair (see the code snippet below) that corresponds to scoring the observed label ${\bf y}$ for a given image ${\bf x}$ against the predictive distribution $q_{\phi}({\bf y}~|~ {\bf x})$

- We specify the scaling factor $\alpha'$ (`aux_loss_multiplier` in the code) in the `pyro.sample` call by making use of `poutine.scale`. Note that `poutine.scale` was used to similar effect in the [Deep Markov Model](dmm.ipynb) to implement KL annealing.

- We create a new `SVI` object and use it to take gradient steps on the new objective term

```python
def model_classify(self, xs, ys=None):
    pyro.module("ss_vae", self)
    with pyro.plate("data"):
        # this here is the extra term to yield an auxiliary loss
        # that we do gradient descent on
        if ys is not None:
            alpha = self.encoder_y(xs)
            with pyro.poutine.scale(scale=self.aux_loss_multiplier):
                pyro.sample("y_aux", dist.OneHotCategorical(alpha), obs=ys)

def guide_classify(xs, ys):
    # the guide is trivial, since there are no
    # latent random variables
    pass

svi_aux = SVI(model_classify, guide_classify, optimizer, loss=Trace_ELBO())
```

When we run inference in Pyro with the additional term in the objective, we outperform both previous inference setups. For example, the test accuracy for the case with $3000$ labeled examples improves from `90%` to `96%` (see Figure 4 below and Table 1 in the next section). Note that we used validation accuracy to select the hyperparameter $\alpha'$.

        Figure 4: Variant 3 (Left) Training losses for the case with 3000 supervised examples.
        (Right) Test and validation accuracies.

## Results

| Supervised data  | First variant  | Second variant | Third variant  | Baseline classifier |
|------------------|----------------|----------------|----------------|---------------------|
| 100              | 0.2007(0.0353) | 0.2254(0.0346) | 0.9319(0.0060) | 0.7712(0.0159)      |
| 600              | 0.1791(0.0244) | 0.6939(0.0345) | 0.9437(0.0070) | 0.8716(0.0064)      |
| 1000             | 0.2006(0.0295) | 0.7562(0.0235) | 0.9487(0.0038) | 0.8863(0.0025)      |
| 3000             | 0.1982(0.0522) | 0.8932(0.0159) | 0.9582(0.0012) | 0.9108(0.0015)      |

 Table 1: Result accuracies (with 95% confidence bounds) for different inference methods

Table 1 collects our results from the three variants explored in the tutorial. For comparison, we also show results from a simple classifier baseline, which only makes use of the supervised data (and no latent random variables). Reported are mean accuracies (with 95% confidence bounds in parentheses) across five random selections of supervised data.

We first note that the results for the third variant—where we summed out the discrete latent random variable $\bf y$ and made use of the additional term in the objective function—reproduce the results reported in reference [1]. This is encouraging, since it means that the abstractions in Pyro proved flexible enough to accomodate the required modeling and inference setup. Significantly, this flexibility was evidently necessary to outperform the baseline. It's also worth emphasizing that the gap between the baseline and third variant of our generative model setup increases as the number of labeled datapoints decreases (maxing out at about 15% for the case with only 100 labeled datapoints). This is a tantalizing result because it's precisely in the regime where we have few labeled data points that semi-supervised learning is particularly attractive.

### Latent Space Visualization

        Figure 5: Latent space embedding for variant 3 with 3000 supervised examples

We use  T-SNE to reduce the dimensionality of the latent $\bf z$ from $50$ to $2$ and visualize the 10 digit classes in Figure 5. Note that the structure of the embedding is quite different than that in the [VAE](vae.ipynb) case, where the digits are clearly separated from one another in the embedding. This make sense, since for the semi-supervised case the latent $\bf z$ is free to use its representational capacity to model, e.g., handwriting style, since the variation between digits is provided by the (partially observed) labels.

### Conditional image generation

        Figure 6: Conditional samples obtained by fixing the class label and varying z (for variant 3 with 3000 supervised examples)

We sampled $100$ images for each class label ($0$ to $9$) by sampling different values of the latent variable ${\bf z}$. The diversity of handwriting styles exhibited by each digit is consistent with what we saw in the T-SNE visualization, suggesting that the representation learned by $\bf z$ is disentangled from the class labels.

## Final thoughts

We've seen that generative models offer a natural approach to semi-supervised machine learning. One of the most attractive features of generative models is that we can explore a large variety of models in a single unified setting. In this tutorial we've only been able to explore a small fraction of the possible model and inference setups that are possible. There is no reason to expect that one variant is best; depending on the dataset and application, there will be reason to prefer one over another. And there are a lot of variants (see Figure 7)!

 Figure 7: A zoo of semi-supervised generative models

Some of these variants clearly make more sense than others, but a priori it's difficult to know which ones are worth trying out. This is especially true once we open the door to more complicated setups, like the two models at the bottom of the figure, which include an always latent random variable $\tilde{\bf y}$ in addition to the partially observed label ${\bf y}$. (Incidentally, this class of models—see reference [2] for similar variants—offers another potential solution to the 'no training' problem that we identified above.)

The reader probably doesn't need any convincing that a systematic exploration of even a fraction of these options would be incredibly time-consuming and error-prone if each model and each inference procedure were coded up by scratch. It's only with the modularity and abstraction made possible by a probabilistic programming system that we can hope to explore the landscape of generative models with any kind of nimbleness—and reap any awaiting rewards.

See the full code on [Github](https://github.com/pyro-ppl/pyro/blob/dev/examples/vae/ss_vae_M2.py).

## References

[1] `Semi-supervised Learning with Deep Generative Models`,

Diederik P. Kingma, Danilo J. Rezende, Shakir Mohamed, Max Welling

[2] `Learning Disentangled Representations with Semi-Supervised Deep Generative Models`,

N. Siddharth, Brooks Paige, Jan-Willem Van de Meent, Alban Desmaison, Frank Wood,

Noah D. Goodman, Pushmeet Kohli, Philip H.S. Torr