```python
#remove cell visibility
from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)
```

Toggle cell visibility here.

```python
%matplotlib inline
import control
import numpy
import sympy as sym
from IPython.display import display, Markdown
import ipywidgets as widgets
import matplotlib.pyplot as plt

#print a matrix latex-like
def bmatrix(a):
     """Returns a LaTeX bmatrix - by Damir Arbula (ICCT project)

     :a: numpy array
     :returns: LaTeX bmatrix as a string
     """
     if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
     lines = str(a).replace('[', '').replace(']', '').splitlines()
     rv = [r'\begin{bmatrix}']
     rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
     rv +=  [r'\end{bmatrix}']
     return '\n'.join(rv)

# Display formatted matrix:
def vmatrix(a):
    if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
    lines = str(a).replace('[', '').replace(']', '').splitlines()
    rv = [r'\begin{vmatrix}']
    rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
    rv +=  [r'\end{vmatrix}']
    return '\n'.join(rv)

#matrixWidget is a matrix looking widget built with a VBox of HBox(es) that returns a numPy array as value !
class matrixWidget(widgets.VBox):
    def updateM(self,change):
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.M_[irow,icol] = self.children[irow].children[icol].value
                #print(self.M_[irow,icol])
        self.value = self.M_

    def dummychangecallback(self,change):
        pass

    def __init__(self,n,m):
        self.n = n
        self.m = m
        self.M_ = numpy.matrix(numpy.zeros((self.n,self.m)))
        self.value = self.M_
        widgets.VBox.__init__(self,
                             children = [
                                 widgets.HBox(children =
                                              [widgets.FloatText(value=0.0, layout=widgets.Layout(width='90px')) for i in range(m)]
                                             )
                                 for j in range(n)
                             ])

        #fill in widgets and tell interact to call updateM each time a children changes value
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
                self.children[irow].children[icol].observe(self.updateM, names='value')
        #value = Unicode('example@example.com', help="The email value.").tag(sync=True)
        self.observe(self.updateM, names='value', type= 'All')

    def setM(self, newM):
        #disable callbacks, change values, and reenable
        self.unobserve(self.updateM, names='value', type= 'All')
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].unobserve(self.updateM, names='value')
        self.M_ = newM
        self.value = self.M_
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].observe(self.updateM, names='value')
        self.observe(self.updateM, names='value', type= 'All')

                #self.children[irow].children[icol].observe(self.updateM, names='value')

#overlaod class for state space systems that DO NOT remove "useless" states (what "professor" of automatic control would do this?)
class sss(control.StateSpace):
    def __init__(self,*args):
        #call base class init constructor
        control.StateSpace.__init__(self,*args)
    #disable function below in base class
    def _remove_useless_states(self):
        pass
```

## Missile attitude control

Missile guidance is usually achieved by means of acceleration commands. The dynamic model of the missile of interest is given by the following equations obtained by system identification:

\begin{cases}
\dot{\textbf{X}}=\begin{bmatrix}\dot{x_1} \\ \dot{x_2} \\ \dot{x_3}\end{bmatrix} =\begin{bmatrix}-1.364 & −92.82 & −128.46 \\ 1 & −4.68 & −0.087 \\ 0 & 0 & -190 \end{bmatrix} \begin{bmatrix} x_1 \\x_2 \\ x_3 \end{bmatrix}+\begin{bmatrix}0 \\0 \\190\end{bmatrix}u \\
a=\begin{bmatrix}1.36 & −184.26 & 76.43\end{bmatrix}\textbf{X},
\end{cases}

where $x_1$ is pitch rate in rad/s, $x_2$ is angle of attack in rad, $x_3$ is rudder angle in rad, $u$ is rudder command in rad/s, and the output is lateral acceleration $a$ in m/$\text{s}^2$. Aim is to design a flight control system that regulates missile acceleration to the desired value $a_d$. The maximum rudder deflection is $\pm$17 degrees.

The aim is to design a regulator that controls the missile acceleration by acting on rudder angle according to the following specifications:
- steady-state error (in response to a desired acceleration step input equal to $3 G \simeq 30$ $\text{m/s}^2$) is less than 1%,
- max overshoot: 5%,
- settling time for 5% tolerance band is less than 0.1 seconds.

### Regulator design
#### Controller design
The system's controllability matrix $\mathcal{C}$ is:

```python
A = numpy.matrix('-1.364 -92.82 -128.46; 1 -4.68 -0.087; 0 0 -190')
B = numpy.matrix('0; 0; 190')
C = numpy.matrix('1.36 -184.26 76.43')
D = numpy.matrix('0')
Ctrb = control.ctrb(A,B)
display(Markdown(bmatrix(Ctrb)))
# print(numpy.linalg.matrix_rank(Ctrb))
```

\begin{bmatrix}
  0.00000000e+00 & -2.44074000e+04 & 4.67223201e+06\\
  0.00000000e+00 & -1.65300000e+01 & -2.11893396e+04\\
  1.90000000e+02 & -3.61000000e+04 & 6.85900000e+06\\
\end{bmatrix}

that has rank equal to 3, so the system is controllable.

The transfer function of the system is

```python
sys = sss(A,B,C,0)
print(control.ss2tf(sys))
```

    1.452e+04 s^2 + 5.762e+04 s + 5.789e+06
    ---------------------------------------
      s^3 + 196 s^2 + 1248 s + 1.885e+04

and the poles and zeros are

```python
print('Poles: ', sys.pole())
print('Zeros: ', sys.zero())
```

    Poles:  [  -3.022+9.49057617j   -3.022-9.49057617j -190.   +0.j        ]
    Zeros:  [-1.98395879+19.86693768j -1.98395879-19.86693768j]

We find that placing 2 imaginary poles near the zeros results in a good response, so the poles that we choose are $-2+19.1i$, $-2-19.1i$ and $-45$.

#### Observer design
The system's observability matrix $\mathcal{O}$ is:

```python
Obsv = control.obsv(A,C)
display(Markdown(bmatrix(Obsv)))
# print(numpy.linalg.matrix_rank(Obsv))
```

\begin{bmatrix}
  1.36000000e+00 & -1.84260000e+02 & 7.64300000e+01\\
  -1.86115040e+02 & 7.36101600e+02 & -1.46803750e+04\\
  9.89962515e+02 & 1.38302425e+04 & 2.81311554e+06\\
\end{bmatrix}

that has rank equal to 3, so the system is observable.

The only requirement that we have for the observer is that the error dynamics converges in approximately less than 0.1 s. A good choice for the observer poles is $-50$, $-50$, and $-50$.

### How to use this notebook?
Try to achieve the performance requirements with other locations of the poles and with errors in the initial state of the observer.

```python
# Preparatory cell

X0 = numpy.matrix('0.0; 0.0; 0.0')
K = numpy.matrix([8/15,-4.4,-4])
L = numpy.matrix([[23],[66],[107/3]])

Aw = matrixWidget(3,3)
Aw.setM(A)
Bw = matrixWidget(3,1)
Bw.setM(B)
Cw = matrixWidget(1,3)
Cw.setM(C)
X0w = matrixWidget(3,1)
X0w.setM(X0)
Kw = matrixWidget(1,3)
Kw.setM(K)
Lw = matrixWidget(3,1)
Lw.setM(L)

eig1c = matrixWidget(1,1)
eig2c = matrixWidget(2,1)
eig3c = matrixWidget(1,1)
eig1c.setM(numpy.matrix([-60.]))
eig2c.setM(numpy.matrix([[-2.],[-19.1]]))
eig3c.setM(numpy.matrix([-45.]))

eig1o = matrixWidget(1,1)
eig2o = matrixWidget(2,1)
eig3o = matrixWidget(1,1)
eig1o.setM(numpy.matrix([-50.]))
eig2o.setM(numpy.matrix([[-50.],[0.]]))
eig3o.setM(numpy.matrix([-50.]))
```

```python
# Misc

#create dummy widget
DW = widgets.FloatText(layout=widgets.Layout(width='0px', height='0px'))

#create button widget
START = widgets.Button(
    description='Test',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Test',
    icon='check'
)

def on_start_button_clicked(b):
    #This is a workaround to have intreactive_output call the callback:
    #   force the value of the dummy widget to change
    if DW.value> 0 :
        DW.value = -1
    else:
        DW.value = 1
    pass
START.on_click(on_start_button_clicked)

# Define type of method
selm = widgets.Dropdown(
    options= ['Set K and L', 'Set the eigenvalues'],
    value= 'Set the eigenvalues',
    description='',
    disabled=False
)

# Define the number of complex eigenvalues
sele = widgets.Dropdown(
    options= ['0 complex eigenvalues', '2 complex eigenvalues'],
    value= '2 complex eigenvalues',
    description='Complex eigenvalues:',
    style = {'description_width': 'initial'},
    disabled=False
)

#define type of ipout
selu = widgets.Dropdown(
    options=['impulse', 'step', 'sinusoid', 'square wave'],
    value='step',
    description='Type of reference:',
    style = {'description_width': 'initial'},
    disabled=False
)
# Define the values of the input
u = widgets.FloatSlider(
    value=9.81*3,
    min=0,
    max=9.81*10,
    step=0.1,
    description='Reference:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
period = widgets.FloatSlider(
    value=0.5,
    min=0.001,
    max=10,
    step=0.001,
    description='Period: ',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

gain_w2 = widgets.FloatText(
    value=1.,
    description='',
    disabled=True
)

simTime = widgets.FloatText(
    value=1.5,
    description='',
    disabled=False
)
```

```python
# Support functions

def eigen_choice(sele):
    if sele == '0 complex eigenvalues':
        eig1c.children[0].children[0].disabled = False
        eig2c.children[1].children[0].disabled = True
        eig1o.children[0].children[0].disabled = False
        eig2o.children[1].children[0].disabled = True
        eig = 0
    if sele == '2 complex eigenvalues':
        eig1c.children[0].children[0].disabled = True
        eig2c.children[1].children[0].disabled = False
        eig1o.children[0].children[0].disabled = True
        eig2o.children[1].children[0].disabled = False
        eig = 2
    return eig

def method_choice(selm):
    if selm == 'Set K and L':
        method = 1
        sele.disabled = True
    if selm == 'Set the eigenvalues':
        method = 2
        sele.disabled = False
    return method
```

```python
def main_callback2(Aw, Bw, X0w, K, L, eig1c, eig2c, eig3c, eig1o, eig2o, eig3o, u, period, selm, sele, selu, simTime, DW):
    eige = eigen_choice(sele)
    method = method_choice(selm)

    if method == 1:
        solc = numpy.linalg.eig(A-B*K)
        solo = numpy.linalg.eig(A-L*C)
    if method == 2:
        if eige == 0:
            K = control.acker(A, B, [eig1c[0,0], eig2c[0,0], eig3c[0,0]])
            Kw.setM(K)
            L = control.acker(A.T, C.T, [eig1o[0,0], eig2o[0,0], eig3o[0,0]]).T
            Lw.setM(L)
        if eige == 2:
            K = control.acker(A, B, [eig3c[0,0],
                                     numpy.complex(eig2c[0,0],eig2c[1,0]),
                                     numpy.complex(eig2c[0,0],-eig2c[1,0])])
            Kw.setM(K)
            L = control.acker(A.T, C.T, [eig3o[0,0],
                                         numpy.complex(eig2o[0,0],eig2o[1,0]),
                                         numpy.complex(eig2o[0,0],-eig2o[1,0])]).T
            Lw.setM(L)

    sys = control.ss(A,B,numpy.vstack((C,numpy.zeros((B.shape[1],C.shape[1])))),numpy.vstack((D,numpy.eye(B.shape[1]))))
    sysC = control.ss(numpy.zeros((1,1)),
                      numpy.zeros((1,numpy.shape(A)[0])),
                      numpy.zeros((numpy.shape(B)[1],1)),
                      -K)

    sysE = control.ss(A-L*C,
                      numpy.hstack((L,B-L*D)),
                      numpy.eye(numpy.shape(A)[0]),
                      numpy.zeros((A.shape[0],C.shape[0]+B.shape[1])))

    sys_append = control.append(sys, sysE, sysC, control.ss(A,B,numpy.eye(A.shape[0]),numpy.zeros((A.shape[0],B.shape[1]))))
    Q = []
    # y in ingresso a sysE
    for i in range(C.shape[0]):
        Q.append([B.shape[1]+i+1, i+1])
    # u in ingresso a sysE
    for i in range(B.shape[1]):
        Q.append([B.shape[1]+C.shape[0]+i+1, C.shape[0]+i+1])
    # u in ingresso a sys
    for i in range(B.shape[1]):
        Q.append([i+1, C.shape[0]+B.shape[1]+A.shape[0]+i+1])
    # u in ingresso al sistema che ha come uscite gli stati reali
    for i in range(B.shape[1]):
        Q.append([2*B.shape[1]+C.shape[0]+A.shape[0]+i+1, C.shape[0]+i+1])
    # xe in ingresso a sysC
    for i in range(A.shape[0]):
        Q.append([2*B.shape[1]+C.shape[0]+i+1, C.shape[0]+B.shape[1]+i+1])

    inputv = [i+1 for i in range(B.shape[1])]
    outputv = [i+1 for i in range(numpy.shape(sys_append.C)[0])]
    sys_CL = control.connect(sys_append,
                             Q,
                             inputv,
                             outputv)

    dcgain = control.dcgain(sys_CL[0,0])
    gain_w2.value = dcgain
    if dcgain != 0:
        u1 = u/gain_w2.value
    else:
        print('The feedforward gain setted is 0 and it is changed to 1')
        u1 = u/1
    print('The static gain of the closed loop system (from the reference to the output) is: %.5f' %dcgain)

    X0w1 = numpy.zeros((A.shape[0],1))
    for j in range(A.shape[0]):
        X0w1 = numpy.vstack((X0w1,X0w[j]))
    X0w1 = numpy.vstack((X0w1,numpy.zeros((A.shape[0],1))))
    if simTime != 0:
        T = numpy.linspace(0, simTime, 10000)
    else:
        T = numpy.linspace(0, 1, 10000)

    if selu == 'impulse': #selu
        U = [0 for t in range(0,len(T))]
        U[0] = u
        U1 = [0 for t in range(0,len(T))]
        U1[0] = u1
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    if selu == 'step':
        U = [u for t in range(0,len(T))]
        U1 = [u1 for t in range(0,len(T))]
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    if selu == 'sinusoid':
        U = u*numpy.sin(2*numpy.pi/period*T)
        U1 = u1*numpy.sin(2*numpy.pi/period*T)
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    if selu == 'square wave':
        U = u*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        U1 = u1*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    # N.B. i primi 3 stati di xout sono quelli del sistema, mentre gli ultimi 3 sono quelli dell'osservatore

    step_info_dict = control.step_info(sys_CL[0,0],SettlingTimeThreshold=0.05,T=T)
    print('Step info: \n\tRise time =',step_info_dict['RiseTime'],'\n\tSettling time (5%) =',step_info_dict['SettlingTime'],'\n\tOvershoot (%)=',step_info_dict['Overshoot'])
    print('Max x3 value (%)=', max(abs(yout[C.shape[0]+2*B.shape[1]+A.shape[0]+2]))/(numpy.pi/180*17)*100)

    fig = plt.figure(num='Simulation1', figsize=(14,12))

    fig.add_subplot(221)
    plt.title('Output response')
    plt.ylabel('Output')
    plt.plot(T,yout[0],T,U,'r--')
    plt.xlabel('$t$ [s]')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.legend(['$y$','Reference'])
    plt.grid()

    fig.add_subplot(222)
    plt.title('Input')
    plt.ylabel('$u$')
    plt.plot(T,yout[C.shape[0]])
    plt.xlabel('$t$ [s]')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

    fig.add_subplot(223)
    plt.title('States response')
    plt.ylabel('States')
    plt.plot(T,yout[C.shape[0]+2*B.shape[1]+A.shape[0]],
             T,yout[C.shape[0]+2*B.shape[1]+A.shape[0]+1],
             T,yout[C.shape[0]+2*B.shape[1]+A.shape[0]+2],
             T,[numpy.pi/180*17 for i in range(len(T))],'r--',
             T,[-numpy.pi/180*17 for i in range(len(T))],'r--')
    plt.xlabel('$t$ [s]')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.legend(['$x_{1}$','$x_{2}$','$x_{3}$','limit +$x_{3}$','limit -$x_{3}$'])
    plt.grid()

    fig.add_subplot(224)
    plt.title('Estimation errors')
    plt.ylabel('Errors')
    plt.plot(T,yout[C.shape[0]+2*B.shape[1]+A.shape[0]]-yout[C.shape[0]+B.shape[1]],
             T,yout[C.shape[0]+2*B.shape[1]+A.shape[0]+1]-yout[C.shape[0]+B.shape[1]+1],
             T,yout[C.shape[0]+2*B.shape[1]+A.shape[0]+2]-yout[C.shape[0]+B.shape[1]+2])
    plt.xlabel('$t$ [s]')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.legend(['$e_{1}$','$e_{2}$','$e_{3}$'])
    plt.grid()
    #plt.tight_layout()

alltogether2 = widgets.VBox([widgets.HBox([selm,
                                          sele,
                                          selu]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('K:',border=3), Kw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Eigenvalues:',border=3),
                                          eig1c,
                                          eig2c,
                                          eig3c,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('X0 est.:',border=3), X0w]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('L:',border=3), Lw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Eigenvalues:',border=3),
                                          eig1o,
                                          eig2o,
                                          eig3o,
                                          widgets.Label(' ',border=3),
                                          widgets.VBox([widgets.Label('Inverse reference gain:',border=3),
                                                        widgets.Label('Simulation time [s]:',border=3)]),
                                          widgets.VBox([gain_w2,simTime])]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([u,
                                          period,
                                          START])])
out2 = widgets.interactive_output(main_callback2, {'Aw':Aw, 'Bw':Bw, 'X0w':X0w, 'K':Kw, 'L':Lw,
                                                 'eig1c':eig1c, 'eig2c':eig2c, 'eig3c':eig3c, 'eig1o':eig1o, 'eig2o':eig2o, 'eig3o':eig3o,
                                                 'u':u, 'period':period, 'selm':selm, 'sele':sele, 'selu':selu, 'simTime':simTime, 'DW':DW})
out2.layout.height = '860px'
display(out2, alltogether2)
```

    Output(layout=Layout(height='860px'))

    VBox(children=(HBox(children=(Dropdown(index=1, options=('Set K and L', 'Set the eigenvalues'), value='Set the…

```python

```


##### Copyright 2020 The Cirq Developers

```
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
```

# Operators and observables

    View on QuantumAI

    Run in Google Colab

    View source on GitHub

    Download notebook

```
try:
    import cirq
except ImportError:
    print("installing cirq...")
    !pip install --quiet cirq
    print("installed cirq.")
    import cirq

import numpy as np
import sympy.parsing.sympy_parser as sympy_parser
```

This guide is directed at those already familiar with quantum operations (operators) and observables who want to know how to use them in Cirq. The following table shows an overview of operators.

| Operator | Cirq representation | Guides | Examples |
|-----|-------|--------|-----|
| Unitary operators | Any class implementing the `_unitary_` protocol | [Protocols](protocols.ipynb), [Gates and operations](gates.ipynb), [Custom gates](custom_gates.ipynb) | `cirq.Gate`
 `cirq.X(qubit)`
 `cirq.CNOT(q0, q1)`
 `cirq.MatrixGate.on(qubit)`
 `cirq.Circuit` (if it only contains unitary operations)  |
| Measurements | `cirq.measure` and `cirq.MeasurementGate`  | [Gates and operations](gates.ipynb) | `cirq.measure(cirq.LineQubit(0))` |
| Quantum channels |

Kraus operators (any class implementing the `_kraus_` protocol)

Pauli basis (cirq.PauliSum)

 | [Protocols](protocols.ipynb) | `cirq.DepolarizingChannel(p=0.2)(q0)`
 `cirq.X.with_probability(0.5)`|

Cirq also supports observables on qubits that can be used to calculate expectation values on a given state.

* You can use `cirq.PauliString` to express them,
* Or you can use `cirq.PauliSum` with real coefficients.

## Operators

Quantum operations (or just *operators*) include unitary gates, measurements, and noisy channels. Operators that act on a given set of qubits implement `cirq.Operation` which supports the Kraus operator representation

$$
\rho \mapsto \sum_{k} A_k \rho A_k^\dagger .
$$

Here, $\sum_{k} A_k^\dagger A_k = I$ and $\rho$ is a quantum state. Operators are defined in the `cirq.ops` module.

### Unitary operators

Standard unitary operators used in quantum information can be found in `cirq.ops`, for example Pauli-$X$ as shown below.

```
qubit = cirq.LineQubit(0)
unitary_operation = cirq.ops.X.on(qubit)  # cirq.X can also be used for cirq.ops.X
print(unitary_operation)
```

Cirq makes a distinction between gates (independent of qubits) and operations (gates acting on qubits). Thus `cirq.X` is a gate where `cirq.X.on(qubit)` is an operation. See the [guide on gates](gates.ipynb) for more details and additional common unitaries defined in Cirq.

> **Note**: The method `cirq.X.on_each` is a utility to apply `cirq.X` to multiple qubits. Similarly for other operations.

Every `cirq.Operation` supports the `cirq.channel` protocol which returns its Kraus operators. (Read more about [protocols](protocols.ipynb) in Cirq.)

```
kraus_ops = cirq.kraus(unitary_operation)
print(f"Kraus operators of {unitary_operation.gate} are:", *kraus_ops, sep="\n")
```

Unitary operators also support the `cirq.unitary` protocol.

```
unitary = cirq.unitary(cirq.ops.X)
print(f"Unitary of {unitary_operation.gate} is:\n", unitary)
```

Unitary gates can be raised to powers, for example to implement a $\sqrt{X}$ operation.

```
sqrt_not = cirq.X ** (1 / 2)
print(cirq.unitary(sqrt_not))
```

Any gate can be controlled via `cirq.ControlledGate` as follows.

```
controlled_hadamard = cirq.ControlledGate(sub_gate=cirq.H, num_controls=1)
print(cirq.unitary(controlled_hadamard).round(3))
```

Custom gates can be defined as described in [this guide](custom_gates.ipynb). Some common subroutines which consist of several operations are pre-defined - e.g., `cirq.qft` returns the operations to implement the quantum Fourier transform.

### Measurements

Cirq supports measurements in the computational basis.

```
measurement = cirq.MeasurementGate(num_qubits=1, key="key")
print("Measurement:", measurement)
```

The `key` can be used to identify results of measurements when [simulating circuits](simulation.ipynb). A measurement gate acting on a qubit forms an operation.

```
measurement_operation = measurement.on(qubit)
print(measurement_operation)
```

> **Note**: The function `cirq.measure` is a utility to measure a single qubit, and the function `cirq.measure_each` is a utility to measure multiple qubits.

Again measurement operations implement `cirq.Operation` so the `cirq.channel` protocol can be used to get the Kraus operators.

```
kraus_ops = cirq.kraus(measurement)
print(f"Kraus operators of {measurement} are:", *kraus_ops, sep="\n\n")
```

The functions `cirq.measure_state_vector` and `cirq.measure_density_matrix` can be used to perform computational basis measurements on state vectors and density matrices, respectively, represented by NumPy arrays.

```
psi = np.ones(shape=(2,)) / np.sqrt(2)
print("Wavefunction:\n", psi.round(3))
```

```
results, psi_prime = cirq.measure_state_vector(psi, indices=[0])

print("Measured:", results[0])
print("Resultant state:\n", psi_prime)
```

```
rho = np.ones(shape=(2, 2)) / 2.0
print("State:\n", rho)
```

```
measurements, rho_prime = cirq.measure_density_matrix(rho, indices=[0])

print("Measured:", measurements[0])
print("Resultant state:\n", rho_prime)
```

These functions do not modify the input state (`psi` or `rho`) unless the optional argument `out` is provided as the input state.

### Noisy channels

Like common unitary gates, Cirq defines many common noisy channels, for example the depolarizing channel below.

```
depo_channel = cirq.DepolarizingChannel(p=0.01, n_qubits=1)
print(depo_channel)
```

Just like unitary gates and measurements, noisy channels implement `cirq.Operation`, and we can always use `cirq.channel` to get the Kraus operators.

```
kraus_ops = cirq.kraus(depo_channel)
print(f"Kraus operators of {depo_channel} are:", *[op.round(2) for op in kraus_ops], sep="\n\n")
```

Some channels can be written

$$
\rho \mapsto \sum_k p_k U_k \rho U_k ^\dagger
$$

where real numbers $p_k$ form a probability distribution and $U_k$ are unitary. Such a *probabilistic mixture* of unitaries supports the `cirq.mixture` protocol which returns $p_k$ and $U_k$. An example is shown below for the bit-flip channel $\rho \mapsto (1 - p) \rho + p X \rho X$.

```
bit_flip = cirq.bit_flip(p=0.05)
probs, unitaries = cirq.mixture(bit_flip)

for prob, unitary in cirq.mixture(bit_flip):
    print(f"With probability {prob}, apply \n{unitary}\n")
```

> **Note**: Any unitary gate/operation supports `cirq.mixture` because it can be interpreted as applying a single unitary with probability one.

Custom noisy channels can be defined as described in [this guide](noise.ipynb).

### In circuits

Any `cirq.Operation` (pre-defined or user-defined) can be placed in a `cirq.Circuit`. An example with a unitary, noisy channel, and measurement is shown below.

```
circuit = cirq.Circuit(
    cirq.H(qubit),
    cirq.depolarize(p=0.01).on(qubit),
    cirq.measure(qubit)
)
print(circuit)
```

The general input to the circuit constructor is a `cirq.OP_TREE`, i.e., an operation or nested collection of operations. Circuits can be manipulated as described in the [circuits guide](circuits.ipynb) and simulated as described in the [simulation guide](simulation.ipynb).

## Observables

Cirq supports observables which are Pauli strings or linear combinations of Pauli strings. Such objects can be used to compute expectation values.

### Pauli strings

Pauli products or strings are supported via `cirq.PauliString`. For example, the Pauli string $Z_0 Z_1$ (where subscripts denote qubit indices) can be represented as follows.

```
# Qubit register
qreg = cirq.NamedQubit.range(2, prefix="q")

# PauliString Z_0 Z_1
zz = cirq.PauliString(cirq.Z(q) for q in qreg)
print(zz)
```

The matrix of a Pauli string can be returned via:

```
zz.matrix()
```

Pauli strings can also have arbitrary coefficients.

```
new = (1.0 - 0.1j) * zz
print(new)
```

### Pauli sums

A `cirq.PauliSum` is a linear combination of `cirq.PauliString`s and represents a Hamiltonian (or general observable) in the Pauli basis. To represent the observable $O = 1.5 Z_0 Z_1 - 0.7 X_0 X_1$, we can first define the $X_0 X_1$ Pauli string:

```
xx = cirq.PauliString(cirq.X(q) for q in qreg)
print(xx)
```

Then form the linear combination.

```
psum = 1.5 * zz - 0.7 * xx
print(psum)
```

Like Pauli strings, we can get the matrix of a `cirq.PauliSum`:

```
psum.matrix()
```

A Pauli sum can also be constructed from a `Sympy` Boolean expression. This is based on "On the representation of Boolean and real functions as Hamiltonians for quantum computing" by Stuart Hadfield (https://arxiv.org/abs/1804.09130)

```
psum = cirq.PauliSum.from_boolean_expression(
    sympy_parser.parse_expr('x0 ^ x1'),
    {'x0': cirq.NamedQubit('q0'), 'x1': cirq.NamedQubit('q1')})
```

### Expectation values

Given a state $\rho$ that is prepared by a circuit, expectation values $\text{Tr} [ \rho O ]$ where $O$ is an observable can be computed as follows. First, an example circuit:

```
circuit = cirq.Circuit(cirq.ops.H.on_each(qreg))
print(circuit)
```

The pattern for computing $\text{Tr} [ \rho O ]$ is shown below.

```
!pip install nest-asyncio
# for running collector under jupyter environment
import nest_asyncio
nest_asyncio.apply()
```

**Warning: If you run this notebook in a colab environment with "Run all", the cell below may hang forever or you may encounter crash. Please manually run this cell either by pressing the play button to the left of the code, or by using the keyboard shortcut "Command/Ctrl+Enter".**

```
# Define a PauliSumCollector.
collector = cirq.PauliSumCollector(circuit, psum, samples_per_term=10_000)

# Provide a sampler. See also: collector.collect(...).
collector.collect(sampler=cirq.Simulator())

# Estimate the observable.
energy = collector.estimated_energy()
print("Energy:", energy)
```

Note that this method uses sampling with a number of samples given by `samples_per_term`.

Expectation values can also be computed from NumPy array representations of quantum states. For example, given a state vector we can do the following.

```
# Get the state vector.
psi = circuit.final_state_vector()

# Compute the expectation value.
energy = psum.expectation_from_state_vector(
    state_vector=psi, qubit_map={q: i for i, q in enumerate(qreg)}
)
print("Energy:", energy.real)
```

And given a density matrix, we can compute the expectation via:

```
# Get the density matrix.
dsim = cirq.DensityMatrixSimulator()
rho = dsim.simulate(circuit).final_density_matrix

# Compute the expectation value.
energy = psum.expectation_from_density_matrix(
    state=rho, qubit_map={q: i for i, q in enumerate(qreg)}
)
print("Energy:", energy.real)
```

### Pauli expansions

The function `cirq.pauli_expansion` can return Pauli basis representations of certain objects. For example, given the circuit $H \otimes H$ from above:

```
circuit
```

The Pauli expansion can be obtained via:

```
psum = cirq.pauli_expansion(circuit)
print(psum)
```

Generally, the argument to `cirq.pauli_expansion` must define the `_pauli_expansion_` method or have a small unitary representation. The argument `default` can be provided to return a default value if no expansion can be computed.


# Hello World!

The **Jupyter Notebook** is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text. Uses include: data cleaning and transformation, numerical simulation, statistical modeling, data visualization, machine learning, and much more.

There two types of cells in the Jupyter Notebook:
 - Markdowns
 - Code cells

## Changing cell types

Double click this cell, and change its type to `Code` using the dropdown menu in the toolbar.

Hit `crtl+enter` or `cmd+enter` (OSX) and try to run this cell as a Python code block. Python will throw you an error, given that this cell does not follow its syntax at all :)

Switch the type of this cell back to `Markdown` and run it again (`crtl+enter` or `cmd+enter` (OSX)) to restore its initial state.

## Markdown cells

**This is a Markdown cell aka text cell**

This is a table in Markdown

|  | a | b  | c  | d  |
|----|----|----|----|----|
| row1 | 1 | 2  | 3  | 4  |
| row2 | 5 | 6  | 7  | 8  |
| row3 | 9 | 10 | 11 | 12 |

To style your markdown cells you can use HTML and CSS

        This part uses HTML tags and it is styled with css

You can write LaTex in the markdown cells (see the example below)
\begin{definition} \label{def:FT}
Let $x[n]$ be a sequence of length $N$. Then, its **Fourier transform** is given by
\begin{equation}
\label{eq:FT}
X[k]= \frac{1}{N} \sum_{n=0}^{N-1} x[n] e^{-j2\pi \frac{kn}{N}}
\end{equation}
\end{definition}

## Code cells

```python
# This is a code cell. This notebook uses a Python3 kernel (see upper right of the notebook).
# So, we should be coding in Python!

# Run the following line by ctrl+enter or cmd+enter. Feel free to change Hello World text with something else.

print('Hello World!')
```

    Hello World!

## Some useful keyboard shortcuts to work with JN

Find more on [this website](https://www.cheatography.com/weidadeyue/cheat-sheets/jupyter-notebook/).

## Defining Python functions in JN

Function definitions follows the traditional Python syntax.

Before calling a function defined in the notebook, make sure that the cell containing the function definition is executed.

```python
# In this cell, we define the function.
# You have to run this cell before executing the next one.
# Otherwise function wont be recognized in the scope of the following cells.
def HelloFunction():
    print('Hello World from the function')
    print('test')
    print('test 2')

```

```python
# Call HelloFunction

HelloFunction()
```

    Hello World from the function
    test
    test 2

You can also save the function to a file using the magic command `%%file`

```python
%%file func1.py

#We are going to save the function above to a file called func.py

def testFunction():
    print('Hello World from the function')
    print('test')
    print('test 2')

```

    Writing func1.py

## Using your custom Python scripts (*.py) in JN

You need to make sure that the `*.py` file is in the same directory with your notebook.

I know you had enough of the `Hello World` in this tutorial. Let's have different flavors of them from the `Hello.py` script.

```python
# First we need to use magic command %run followed by the name of the python script we'd like to use
%run Hello.py
```

```python
from Hello import HelloAdele
from Hello import HelloLionelRichie
from Hello import WhatKindaWorld
```

```python
# Listen to your favorite Hello song on YouTube by calling the corresponding function.
# If you wonder to what kind of world we've been keep saying hello, run WhatKindaWorld().

WhatKindaWorld()
```

## Using widgets

To use the jupyter widgets we first need to install them either using pip or conda (see the commands bellow).

`pip install ipywidgets`

`jupyter nbextension enable --py widgetsnbextension`

or

`conda install -c conda-forge ipywidgets`

Some fun.

```python
import ipywidgets as widgets
from IPython.display import display, clear_output
from IPython.display import IFrame

ddown = widgets.Dropdown(
    options=['Select your song','Adele', 'Lionel Richie', 'World is'],
    value='Select your song',
    disabled=False,
)

def on_change(change):
    if change['type'] == 'change' and change['name'] == 'value':
        if change['new'] == 'Adele':
            lnk = HelloAdele(1)
            clear_output(wait=True)
            display(ddown)
            display(IFrame(src=lnk, width=420, height=315))
        elif change['new'] == 'Lionel Richie':
            lnk = HelloLionelRichie(1)
            clear_output(wait=True)
            display(ddown)
            display(IFrame(src=lnk, width=420, height=315))
        elif change['new'] == 'World is':
            lnk = WhatKindaWorld(1)
            clear_output(wait=True)
            display(ddown)
            display(IFrame(src=lnk, width=420, height=315))
        if change['new'] == 'Select your song':
            clear_output(wait=True)
            display(ddown)

ddown.observe(on_change)

display(ddown)
```

    Dropdown(index=1, options=('Select your song', 'Adele', 'Lionel Richie', 'World is'), value='Adele')

## Let's see some more Python code

Your anaconda installation comes with more than 720 Python packages. You can do much more than printing text and opening YouTube videos. In this section, we will use some of them for demonstration.

### Use pandas to create a DataFrame

Pandas is one of the most famous Python libraries for data manipulation. Pandas tidy dataframe is commonly used by Python community to analyze and visualize data. In the following cell, you will see how to create a simple dataframe and display it as a table.

```python
# This is how you import a python module
import pandas as pd

# From this point on, you can access pandas by the proxy name pd

# Variable raw_data has the type Dictionary. To find more about
# Dictionaries in Python you can visit this link:
# https://realpython.com/python-dicts/

raw_data = {'first_name': ['Jason', 'Molly', 'Tina', 'Jake', 'Amy'],
        'last_name': ['Miller', 'Jacobson', 'Ali', 'Milner', 'Cooze'],
        'age': [42, 52, 36, 24, 73],
        'preTestScore': [4, 24, 31, 2, 3],
        'postTestScore': [25, 94, 57, 62, 70]}

# Once you have a dictionary variable, you can easily convert it to a dataframe
# by telling Pandas column names:

df = pd.DataFrame(raw_data, columns = ['first_name', 'last_name', 'age', 'preTestScore', 'postTestScore'])

# Let's see how our dataframe looks like
df
```

      first_name
      last_name
      age
      preTestScore
      postTestScore

      0
      Jason
      Miller
      42
      4
      25

      1
      Molly
      Jacobson
      52
      24
      94

      2
      Tina
      Ali
      36
      31
      57

      3
      Jake
      Milner
      24
      2
      62

      4
      Amy
      Cooze
      73
      3
      70

### Read a data from a csv file into a DataFrame

In the directory at which this notebook is present, you will find `iris.csv` which belongs to the [Iris flower dataset](https://en.wikipedia.org/wiki/Iris_flower_data_set).

In the following cell, we will read this `csv` file into a `pandas` dataframe by simply calling `read_csv` method. Then we will show first five data entries in the dataset by calling the `head` method.

```python
iris = pd.read_csv('iris.csv')
iris.head()

# Hint: run iris? to see its type
# Hint: Change iris.head() with iris.tail() above to see last 5 entries instead.
```

      sepal.length
      sepal.width
      petal.length
      petal.width
      variety

      0
      5.1
      3.5
      1.4
      0.2
      Setosa

      1
      4.9
      3.0
      1.4
      0.2
      Setosa

      2
      4.7
      3.2
      1.3
      0.2
      Setosa

      3
      4.6
      3.1
      1.5
      0.2
      Setosa

      4
      5.0
      3.6
      1.4
      0.2
      Setosa

You can easily see a statistical summary of your dataset by evoking `describe()` method.

```python
iris.describe()
```

      sepal.length
      sepal.width
      petal.length
      petal.width

      count
      150.000000
      150.000000
      150.000000
      150.000000

      mean
      5.843333
      3.057333
      3.758000
      1.199333

      std
      0.828066
      0.435866
      1.765298
      0.762238

      min
      4.300000
      2.000000
      1.000000
      0.100000

      25%
      5.100000
      2.800000
      1.600000
      0.300000

      50%
      5.800000
      3.000000
      4.350000
      1.300000

      75%
      6.400000
      3.300000
      5.100000
      1.800000

      max
      7.900000
      4.400000
      6.900000
      2.500000

### Some basic visualizations using Iris dataset

Looking at tables and their statistical summaries are useful, but they can't beat data visualization. In the following code cell, we use matplotlib to see the correlations between `width` and `height` of the iris flower for its sepal and petal regions.

Hint: Petal length-width correlation is higher than that of sepal length-width.

```python
# Import matplotlib. Use plt as proxy to access package modules
import matplotlib.pyplot as plt
%matplotlib inline
# Create a figure
plt.figure()

# Split figure into subplots 1 row, 2 columns
fig,ax=plt.subplots(1,2,figsize=(17, 9))

# Use iris.plot() method to create scatter plots. Remember that
# iris is an pandas object. To see the full reference of
# pandas.DataFrame.plot visit:
# https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.DataFrame.plot.html

iris.plot(x="sepal.length",y="sepal.width",kind="scatter",ax=ax[0],sharex=False,sharey=False,label="sepal",color='r')
iris.plot(x="petal.length",y="petal.width",kind="scatter",ax=ax[1],sharex=False,sharey=False,label="petal",color='b')

# Set axes labels for subplots.
ax[0].set(title='Sepal comparison ', ylabel='sepal-width')
ax[1].set(title='Petal comparison',  ylabel='petal-width')
ax[0].legend()
ax[1].legend()

# Show the plots
plt.show()
```


# Neutron Balance and Criticality

## Learning Objectives

At the end of this lesson, the student should be able to:

- Define the basic components of a reactor (fuel, coolant, moderator, reflector, etc.)
- Classify conventional reactors based on their components
- Outline the life cycle of a neutron in a reactor
- Explain the 6 factors, conceptually: $\eta$, $\epsilon$, $p$, $f$, $P_{NL}^{th}$, and $P_{NL}^f$
- State the 2, 4, and 6 factor formulas
- Define the terms in the 2, 4, and 6 factor formulas
- Associate the 2, 4, and 6 factor formulas with their corresponding assumptions
- Predict the impact of reactor designs on each of the 6 factors
- Calculate criticality based on the 2, 4, and 6 factor formulas

## Reactor basics

Reactors have a number of components:

- fuel (uranium, plutonium, various enrichments, fertile isotopes, mixed isotopes, ... )
- moderator (scattering medium: water, heavy water, graphite, floride or chloride salts, lithium salts, ...)
- coolant (water, heavy water, helium, carbon dioxide, salts, liquid metals, ...)
- reflector (graphite, beryllium, ...)

A really good summary of various types of reactors, with images, can be found [here](http://www.theiet.org/factfiles/energy/nuc-reac-page.cfm?type=pdf).

### CANDU

- natural uranium
- heavy water ($D_2O$)
- online refuelling
- each bundle: 50 cm long 10 cm diameter

### PWR

- $UO_2$ fuel
- Zircalloy cladding (typically)
- Height: 4.1m
- Hardware mostly Zircaloy (Zr with Sn, Fe, Cr)
- Grid spacers: Zircaloy, Inconel, stainless steel
- End pieces: Stainless steel, Inconel
- 200-250 fuel assemblies per core
- Each Assembly:
  - 14 x 14 to 17 x 17 fuel elements
  - 21cm x 21cm
  - Fuel element size: 1 cm diameter,
  - Fuel element height: 3.9m
  - Assembly weight: 1400lb
  - Enrichment: 3-5%
  - May have separate burnable poison rods

```python
rod = 400 # pellets per rod
assembly = 17*17 - 5*5 # assembly array minus control rod tubes
core = 193 # typical number of assemblies per core
print("Total pellets in a core", core*assembly*rod)
print("Total rods in a core", core*assembly)
```

    Total pellets in a core 20380800
    Total rods in a core 50952

### BWR

A BWR has about 600-800 fuel assemblies per core. Each assembly:

- 14ft (4.5m) tall
- Zircalloy channel around the assembly
- Hardware mostly Zircaloy (Zr with Sn, Fe, Cr)
- Grid spacers: Zircaloy
- Channel (aka shroud): Zircaloy
- End pieces: Stainless steel
- Each assembly
  - 700lb
  - 5.5x5.5 inches (14 cm x 14 cm) square assembly
  - Fuel element array: 8 x 8 or 10 x 10
  - Fuel element size: 1.25 cm diameter, height = 4.1m
  - Enrichment: 2.5-4.5%
  - May have Gd in some rods and variable enrichment in 3D

### Reaction Rates

- The microscopic cross section is just the likelihood of the event per unit area.
- The macroscopic cross section is just the likelihood of the event per unit area of a certain density of target isotopes.
- The reaction rate is the macroscopic cross section times the flux of incident neutrons.

\begin{align}
R_{i,j}(\vec{r}) &= N_j(\vec{r})\int dE \phi(\vec{r},E)\sigma_{i,j}(E)\\
R_{i,j}(\vec{r}) &= \mbox{reactions of type i involving isotope j } [reactions/cm^3s]\\
N_j(\vec{r}) &= \mbox{number of nuclei participating in the reactions } [\#/cm^3]\\
E &= \mbox{energy} [MeV]\\
\phi(\vec{r},E)&= \mbox{flux of neutrons with energy E at position i } [\#/cm^2s]\\
\sigma_{i,j}(E)&= \mbox{cross section } [cm^2]\\
\end{align}

This can be written more simply as $R_x = \Sigma_x I N$, where I is intensity of the neutron flux.

### Neutron Life Cycle

Let's observe the life cycle of a neutron:

We can be more explicit about this with some descriptions of the losses.

### Two Factor Formula

The source of neutrons in a reactor are the neutrons from fission.

\begin{align}
s &=\nu \Sigma_f \phi
\end{align}

where

\begin{align}
s &= \mbox{neutrons available for next generation of fissions}\\
\nu &= \mbox{the number born per fission}\\
\Sigma_f &= \mbox{the number of fissions in the material}\\
\phi &= \mbox{initial neutron flux}
\end{align}

This can also be written as:

\begin{align}
s &= \nu\Sigma_f\phi\\
  &= \nu\frac{\Sigma_f}{\Sigma_{a,fuel}}\frac{\Sigma_{a,fuel}}{\Sigma_a}{\Sigma_a} \phi\\
  &= \eta f {\Sigma_a} \phi\\
\eta &= \frac{\nu\Sigma_f}{\Sigma_{a,fuel}} \\
      &= \mbox{number of neutrons produced per neutron absorbed by the fuel, "neutron reproduction factor"}\\
f &= \frac{\Sigma_{a,fuel}}{\Sigma_a} \\
   &= \mbox{number of neutrons absorbed in the fuel per neutron absorbed anywhere, "fuel utilization factor"}\\
\end{align}

This absorption and flux term at the end seeks to capture the fact that some of the neutrons escape. However, if we assume an infinite reactor, we know that all the neutrons are eventually absorbed in either the fuel or the coolant, so we can normalize by $\Sigma_a\phi$ and therefore:

\begin{align}
k_\infty &= \frac{\eta f \Sigma_a\phi}{\Sigma_a \phi}\\
&= \eta f
\end{align}

### Four Factor Formula

So, then, if we consider the impact of neutron energy on the likelihood of absorption, we can break the neutrons up into a thermal and a fast group.

So, we can say more explicitly:

\begin{align}
k_\infty =& \epsilon p \eta f\\
\epsilon =& \mbox{ fast fission factor}\\
         =& \frac{\mbox{neutrons produced by all fissions}}{\mbox{neutrons produced by thermal fissions}}\\
p =& \mbox{ resonance escape probability}\\
         =& \frac{\mbox{neutrons that reach thermal energies}}{\mbox{number of fast neutrons that start to slow down}}
\end{align}

### Six Factor Formula

If we consider the non-infinite nature of most reactors, we have to add two more factors to the formula:

\begin{align}
k_{eff} =& P_{fnl} P_{tnl}\epsilon p \eta f\\
P_{NL}^{f} =& \mbox{ fast neutron non leakage probability}\\
         =& \frac{\mbox{fast neutrons that do not leak from the reactor}}{\mbox{fast neutrons produced by thermal fissions}}\\
P_{NL}^{th} =& \mbox{ thermal neutron non leakage probability}\\
         =& \frac{\mbox{thermal neutrons that do not leak from the reactor}}{\mbox{neutrons that reach thermal energies}}\\
\end{align}

### Criticality and the full life cycle

If criticality, $k$ is the ratio :

\begin{align}
k = \frac{\mbox{neutrons in generation N}}{\mbox{neutrons in generation N-1}}
\end{align}

Then we can use the life cycle diagram to plug in our six factors to arrive at $k = \frac{n'}{n}$:

```python

```


```python
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
import os
from torch.utils.data import DataLoader, sampler
# load modules
from pytorchtools import EarlyStopping #, Encoder, Decoder

torch.manual_seed(1)
```

    <torch._C.Generator at 0x11fc28390>

```python
USE_CUDA = torch.cuda.is_available()
device = torch.device("cuda" if USE_CUDA else "cpu")
print(device)
```

    cpu

# Data Processing

```python
PAD_TOKEN = 0
SOS_TOKEN = 1
EOS_TOKEN = 2

class BuildVocab:
    def __init__(self):
        self.word2index = {'<PAD>': PAD_TOKEN, '<SOS>': SOS_TOKEN, '<EOS>': EOS_TOKEN}
        self.word2count = {}
        self.index2word = {PAD_TOKEN:'<PAD>', SOS_TOKEN: '<SOS>', EOS_TOKEN: '<EOS>'}
        self.num_words = 3

    def fromCorpus(self, corpus):
        for line in corpus:
            self.fromSentence(line)

    def fromSentence(self, line):
        for word in line:
            self.toVocab(word)

    def toVocab(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.num_words
            self.word2count[word] = 1
            self.index2word[self.num_words] = word
            self.num_words += 1
        else:
            self.word2count[word] += 1
```

```python
df = pd.read_csv("protein_seq.csv", index_col=0)
df.drop_duplicates(inplace=True)
print("Trimming sentence from: ", df.shape)

# chop off the sequence length
# keep sentence length <= a threshhold
mask = (df["meso_seq"].str.len()<=100)&(df["thermal_seq"].str.len()<=100) ########
df = df.loc[mask].reset_index(drop=True)
print("To: ", df.shape)

input_seq = df["meso_seq"].tolist()
target_seq = df["thermal_seq"].tolist()

voc = BuildVocab()
voc.fromCorpus(input_seq + target_seq)
print("Word Count: ", len(voc.word2index))
```

    Trimming sentence from:  (401885, 4)
    To:  (1686, 4)
    Word Count:  24

```python
# mapping character to index for all sentences
def map_character(voc, line):
    return [voc.word2index[word] for word in line] + [EOS_TOKEN] # EOS token

def sequence_padding(ctx, max_len):
    for index, seq in enumerate(ctx):
        pad_to_seq = [PAD_TOKEN]*(max_len - len(seq))
        ctx[index] = [*seq, *pad_to_seq]
    return ctx

def target_binary_mask(ctx):
    for i, line in enumerate(ctx):
        for j, index in enumerate(line):
            if index != 0:
                ctx[i][j] = 1
    return ctx
```

```python
# input and output batch shape should be (max_length, batch_size)
def input_init(input_batch, voc):
    mapped_batch = [map_character(voc, line) for line in input_batch]
    lengths = [len(seq_len) for seq_len in mapped_batch]
    length_t = torch.tensor(lengths)
    max_input_len = max(lengths)
    padded_batch = sequence_padding(mapped_batch, max_input_len)
    padded_batch_t = torch.LongTensor(padded_batch).t()
    return padded_batch_t, length_t

def target_init(target_batch, voc):
    mapped_batch = [map_character(voc, line) for line in target_batch]
    max_target_len = max(len(seq_len) for seq_len in mapped_batch)
    padded_batch = sequence_padding(mapped_batch, max_target_len)
    padded_batch_t = torch.LongTensor(padded_batch).t()
    target_mask = target_binary_mask(padded_batch)
    target_mask_t = torch.ByteTensor(target_mask).t()
    return max_target_len, padded_batch_t, target_mask_t

def batch_inp_out(loader):
    batches = []
    for input_batch, target_batch in loader:
        # sort lengths to pack_padded_sequence later
        input_batch, target_batch = zip(*sorted(zip(input_batch, target_batch),
                                                key=lambda x:len(x[0]),
                                                reverse=True))
        input_batch = list(input_batch)
        target_batch = list(target_batch)
        input_t, lengths = input_init(input_batch, voc)
        max_target_len, target_t, mask_t = target_init(target_batch, voc)
        batches.append((input_t, lengths, max_target_len, target_t, mask_t))
    return batches

def train_val_split(input_seq, target_seq, batch_size):
    num_train = len(input_seq)
    indices = list(range(num_train))
    np.random.shuffle(indices)
    split = int(np.floor(0.1 * num_train)) # validate on 10% of data
    train_indx, val_indx = indices[split:], indices[:split]

    data = list(zip(input_seq, target_seq))
    train_sampler = sampler.SubsetRandomSampler(train_indx)
    val_sampler = sampler.SubsetRandomSampler(val_indx)

    train_loader = DataLoader(data, batch_size=batch_size, sampler=train_sampler, drop_last=True)
    val_loader = DataLoader(data, batch_size=batch_size, sampler=val_sampler, drop_last=True)

    train_batches = batch_inp_out(train_loader)
    val_batches = batch_inp_out(val_loader)
    return train_batches, val_batches
```

```python
### test
train_batches, val_batches = train_val_split(input_seq, target_seq, 64)
# input_t, lengths, max_target_len, target_t, mask_t = train_batches[-1]
input_t, lengths, max_target_len, target_t, mask_t = val_batches[-1]
print("input sequences tensor: ", input_t.shape)
print("input length tensor: ", len(lengths))
print("max target length: ", max_target_len)
print("target sequences tensor: ", target_t.shape)
print("target binary mask: ", mask_t.shape)
```

    input sequences tensor:  torch.Size([101, 64])
    input length tensor:  64
    max target length:  101
    target sequences tensor:  torch.Size([101, 64])
    target binary mask:  torch.Size([101, 64])

# Build Neural Network
## Part I: Encoder

```python
### comment this part if import modules above
# network inherit from nn.Module (container) and override forward() method
class Encoder(nn.Module):
    def __init__(self, hidden_dim, embedding, layer_dim, dropout=0):
        super(Encoder, self).__init__()
        self.layer_dim, self.hidden_dim, self.embedding = layer_dim, hidden_dim, embedding
        # nn.GRU(input_dim, hidden_dim, layer_dim)
        self.gru = nn.GRU(hidden_dim, hidden_dim, layer_dim, dropout=dropout, bidirectional=True)

    # input_seq: (max_length, b atch_size)
    # input_length: (batch_size)
    def forward(self, input_seq, input_length):

        # for each batch_size add a random feature with shape of embedding_dim
        # embeddings: (max_length, batch_size, embedding_dim=hidden_dim)
        embeddings = self.embedding(input_seq)

        # tensor will be concated in time axis
        # basically, it will again concat the first column (seq), second, ....
        # pack: (sum_seq_len, num_directions * hidden_size)
        pack = torch.nn.utils.rnn.pack_padded_sequence(embeddings, input_length)

        # hidden: (n_layers * num_directions, batch_size, hidden_size)
        outputs, hidden = self.gru(pack)

        # unpack outputs: (max_length, batch_size, num_direction * hidden_size)
        outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)

        # sum bidirectional outputs
        outputs = outputs[:, :, :self.hidden_dim] + outputs[:, :, self.hidden_dim:]
        return outputs, hidden
```

```python
## dimension test
rnn = nn.GRU(6, 20, 2, bidirectional=True)  # (input_size = features, hidden_size, n_layers)
inputs = torch.randn(5, 3, 6)  # (max_length, batch_size, input_size)
h0 = torch.randn(2*2, 3, 20)  # (n_layers * num_direction, batch_size, hidden_size)
output, hn = rnn(inputs, h0)
print("Output shape: ", output.shape)  # (max_length, batch_size, hidden_size)
print("hidden shape:", hn.shape)  # (n_layers * num_direction, batch_size, hidden_size)
output = output[:, :, :20] + output[:, :, 20:]
print("output shape: ", output.shape)
```

    Output shape:  torch.Size([5, 3, 40])
    hidden shape: torch.Size([4, 3, 20])
    output shape:  torch.Size([5, 3, 20])

```python
## something about pack_padded_sequence...
# (batch_size, max_length)
a = torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 0, 0], [9, 10, 0, 0, 0]])
length = [5, 3, 2]
targets = nn.utils.rnn.pack_padded_sequence(a, length, batch_first=True)
print(targets[0]) # concat first element of every batch, second element of every batch...
print(targets[1]) # different batch size for each timestep
```

    tensor([ 1,  6,  9,  2,  7, 10,  3,  8,  4,  5])
    tensor([3, 3, 2, 1, 1])

## Part II. Decoder: token by token until EOS
Use current hidden state of the decoder and output of encoder.
Global attention's score function:

$$
\begin{equation}
Scores =
\begin{cases}
    h_t^T h_s & \text{dot}\\
    h_t W_a h_s & \text{general}\\
    v_a^T \tanh(W_a[h_t;h_s]) & \text{concat}
\end{cases}
\end{equation}\\
$$
Note: $h_t$ is the current target decoder state and $h_s$ is all encoder states (= encoder output). $W_a$ is the weight from linear layer.

```python
### comment this part if modules loaded from above
class Attn(torch.nn.Module):
    def __init__(self, method, hidden_size):
        super(Attn, self).__init__()
        self.method = method
        self.hidden_size = hidden_size

    # current decoder state (ht): (1, batch_size, hidden_size)
    # encoder output (hs): (max_length, batch_size, hidden_size)
    # return: (max_length, batch_size)
    def attn_dot(self, ht, hs):
        return torch.sum(ht * hs, dim=2) # sum across hidden_size

    # return: (batch_size, 1, max_length)
    def forward(self, ht, hs):
        # (batch_size, max_length)
        score = self.attn_dot(ht, hs).t()
        # normalize across each row so that the sum of each col in a row will be 1
        # and add one dimension
        return F.softmax(score, dim=1).unsqueeze(1)
```

```python
## Test Attn
att = Attn('dot', 20)
ht = torch.randn(1, 5, 20)
hs = torch.randn(10, 5, 20)
att.forward(ht, hs).shape
```

    torch.Size([5, 1, 10])

```python
### comment this part if modules loaded above
class Decoder(nn.Module):
    # note: input_size=feature size, output_size = vocabulary size
    def __init__(self, method, embedding, hidden_size, output_size, n_layer=1, dropout=0):
        super(Decoder, self).__init__()
        self.method, self.hidden_size, self.output_size, self.n_layer = method, hidden_size, output_size, n_layer
        self.embedding = embedding
        self.embedding_dropout = nn.Dropout(dropout)
        # embed_size = hidden_size here
        self.gru = nn.GRU(hidden_size, hidden_size, n_layer, dropout=dropout)
        self.concat = nn.Linear(hidden_size*2, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)
        self.attn = Attn(method, hidden_size)

    # decoder_input: (1, batch_size) one batch of words at a time
    # decoder_hidden: (n_layer * num_directional, batch_size, hidden_size)
    # encoder_output: (max_length, batch_size, hidden_size)
    def forward(self, decoder_input, decoder_hidden, encoder_output):
        # embeds: (1, batch_size, embed_size=hidden_size)
        embeds = self.embedding_dropout(self.embedding(decoder_input))

        # only one word, no need to pack padded sequence...

        # decoder_output: (1, batch_size, hidden_size)
        # hidden: (n_layers * num_directions, batch_size, hidden_size)
        decoder_output, hidden = self.gru(embeds, decoder_hidden)
        # calculate attention weight
        # score: (batch_size, 1, max_length)
        score = self.attn(decoder_output, encoder_output)
        # context: (batch_size, hidden_size)
        context = score.bmm(encoder_output.transpose(0, 1)).squeeze(1)
        # decoder_output: (batch_size, hidden_size)
        decoder_output = decoder_output.squeeze(0)
        # concat into (batch_size, hidden_size*2)
        linear_input = torch.cat((decoder_output, context), 1)
        concat_output = torch.tanh(self.concat(linear_input))
        # predict next word: (batch_size, vocab_size)
        output = F.softmax(self.out(concat_output), dim=1)
        return output, hidden
```

```python
## test
linear_input_t = torch.randn(3, 8)  # (batch_size, hidden_size)
concat = nn.Linear(8, 4)
concat_output_t = torch.tanh(concat(linear_input_t))
out = nn.Linear(4, 5)
output = F.softmax(out(concat_output_t), dim=1)
print(output.shape)
print(output)
f, t = output.topk(1) # torch.max(decoder_output, dim=1)
print(f)
print(t)
```

    torch.Size([3, 5])
    tensor([[0.1057, 0.1441, 0.1823, 0.4148, 0.1531],
            [0.1375, 0.1852, 0.1354, 0.2544, 0.2876],
            [0.1331, 0.1758, 0.1664, 0.2730, 0.2516]], grad_fn=<SoftmaxBackward>)
    tensor([[0.4148],
            [0.2876],
            [0.2730]], grad_fn=<TopkBackward>)
    tensor([[3],
            [4],
            [3]])

```python
# calculate loss and ignore padded part
# dec_out: (batch_size, vocab_size)
def loss_func(dec_out, target, mask):
    # (batch_size, 1)
    target = target.view(-1, 1)
    # negative log likelihood
    crossEntropy = -torch.log(torch.gather(dec_out, 1, target).squeeze(1))
    # select non-zero elements and calculate the mean
    loss = crossEntropy.masked_select(mask).mean()
    loss = loss.to(device)
    return loss, mask.sum().item()
```

```python
## test loss_func
dec_out = torch.randn(5, 7)
dec_out = F.softmax(dec_out, dim=1)
target = torch.LongTensor([3, 5, 4, 1, 2])
mask = torch.tensor([1, 1, 0, 0, 1], dtype=torch.uint8)
loss_func(dec_out, target, mask)
# print("Dec_out:", dec_out)
# target = target.view(-1, 1)
# print("target:", target)
# gather = torch.gather(dec_out, 1, target).squeeze(1)
# print("gather:", gather)
# crossEntropy = -torch.log(gather)
# print("crossEntropy:", crossEntropy)
# loss = crossEntropy.masked_select(mask)
# print("loss:", loss)
```

    (tensor(2.1858), 3)

## Part III. Training Process

```python
def loss_calulation(encoder, decoder, batch, batch_size, teacher_forcing_r):

    input_t, lengths, max_target_len, target_t, mask_t = batch

    input_t = input_t.to(device)
    lengths = lengths.to(device)
    target_t = target_t.to(device)
    mask_t = mask_t.to(device)

    encoder_output, encoder_hidden = encoder(input_t, lengths)
    decoder_input = torch.LongTensor([[SOS_TOKEN for _ in range(batch_size)]])
    decoder_input = decoder_input.to(device)
    # set initial decoder hidden state to the encoder's final state
    decoder_hidden = encoder_hidden[:decoder.n_layer]

    loss = 0
    print_loss = []
    nTotals = 0
    if np.random.random() < teacher_forcing_r:
        for step in range(max_target_len):
            decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden, encoder_output)
            # teacher forcing: next input is current target
            decoder_input = target_t[step].view(1, -1)
            mask_loss, nTotal = loss_func(decoder_output, target_t[step], mask_t[step])
            loss += mask_loss
            print_loss.append(mask_loss.item()*nTotal)
            nTotals += nTotal
    else:
        for step in range(max_target_len):
            decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden, encoder_output)
            # non teacher forcing: next input is current decoder output
            # choose the index of word with highest possibility
            _, indx = torch.max(decoder_output, dim=1)
            decoder_input = torch.LongTensor([[indx[i] for i in range(batch_size)]])
            decoder_input.to(device)
            mask_loss, nTotal = loss_func(decoder_output, target_t[step], mask_t[step])
            loss += mask_loss
            print_loss.append(mask_loss.item()*nTotal)
            nTotals += nTotal

    return print_loss, loss, nTotals
```

```python
# train for a fixed iterations
def train(input_seq, target_seq, batch_size, epochs, hidden_dim, embedding, encoder, decoder,
          encoder_opt, decoder_opt, teacher_forcing_r, epoch_from, folder, early_stopping_flag,
          patience=20):

    avg_train_loss = []
    avg_val_loss = []

    train_batches, val_batches = train_val_split(input_seq, target_seq, batch_size)

    print(encoder)
    print(decoder)

    if early_stopping_flag:
        early_stopping = EarlyStopping(patience=patience)

    for epoch in range(epoch_from, epochs):
        ############
        # Training #
        ############
        encoder.train()
        decoder.train()
        train_loss = []
        for train_batch in train_batches:
            # initialize and move the model to GPU/CPU
            # set training mode
            encoder_opt.zero_grad()
            decoder_opt.zero_grad()

            print_loss, loss, nTotals = loss_calulation(encoder, decoder, train_batch, batch_size, teacher_forcing_r)

            loss.backward()

            _ = torch.nn.utils.clip_grad_norm_(encoder.parameters(), 1.0)
            _ = torch.nn.utils.clip_grad_norm_(decoder.parameters(), 1.0)

            encoder_opt.step()  # update weights
            decoder_opt.step()

            train_loss.append(sum(print_loss) / nTotals)

        print("train_finished!")
        ##############
        # validation #
        ##############
        encoder.eval()
        decoder.eval()
        val_loss = []
        for val_batch in val_batches:
            print_loss, _, nTotals = loss_calulation(encoder, decoder, val_batch, batch_size, teacher_forcing_r)

            val_loss.append(sum(print_loss) / nTotals)  # loss for batches

        avg_train_loss.append(np.average(train_loss))  # loss for epochs
        avg_val_loss.append(np.average(val_loss))
        print("Epoch: {}; train loss: {:.4}; val loss: {:.4}".format(epoch + 1, avg_train_loss[-1], avg_val_loss[-1]))

        model_states = (
            epoch,
            encoder.state_dict(),
            decoder.state_dict(),
            encoder_opt.state_dict(),
            decoder_opt.state_dict(),
            voc.__dict__,
            embedding.state_dict())

        if early_stopping_flag:
            early_stopping(avg_val_loss[-1], model_states, folder)

            if early_stopping.early_stop:
                print('EarlyStopping ...')
                break

    return avg_train_loss, avg_val_loss
```

## Part IV. Run the Model

```python
# hyperparameters
hidden_dim = 300
enc_layer_dim = 2
dec_layer_dim = 2
dropout = 0.1
attn_method = "dot"
lr = 0.0001
teacher_forcing_r = 0.5
batch_size = 64
epochs = 2000
```

```python
def run(folder, loadMode, early_stopping_flag):
    if not os.path.exists(folder):
        os.makedirs(folder)

    if loadMode:
        checkpoint = torch.load(loadMode)
        enc_chp = checkpoint["enc"]
        dec_chp = checkpoint["dec"]
        enc_opt_chp = checkpoint["enc_opt"]
        dec_opt_chp = checkpoint["dec_opt"]
        voc.__dict__ = checkpoint["voc_dict"]
        embedding_chp = checkpoint["embedding"]

    embedding = nn.Embedding(voc.num_words, hidden_dim)
    if loadMode:
        embedding.load_state_dict(embedding_chp)

    encoder = Encoder(hidden_dim, embedding, enc_layer_dim, dropout)
    decoder = Decoder(attn_method, embedding, hidden_dim, voc.num_words, dec_layer_dim, dropout)
    if loadMode:
        encoder.load_state_dict(enc_chp)
        decoder.load_state_dict(dec_chp)

    encoder = encoder.to(device)
    decoder = decoder.to(device)

    encoder_opt = optim.Adam(encoder.parameters(), lr=lr)
    decoder_opt = optim.Adam(decoder.parameters(), lr=lr)

    epoch_from = 0
    if loadMode:
        encoder_opt.load_state_dict(enc_opt_chp)
        decoder_opt.load_state_dict(dec_opt_chp)
        epoch_from = checkpoint['epoch'] + 1

    train_loss, val_loss = train(input_seq, target_seq, batch_size, epochs, hidden_dim, embedding,
                                 encoder, decoder, encoder_opt, decoder_opt, teacher_forcing_r,
                                 epoch_from, folder, early_stopping_flag)

    with open(os.path.join(folder, 'loss.txt'), 'w') as f:
        for loss1, loss2 in list(zip(train_loss, val_loss)):
            f.write("{}\t{}\n".format(loss1, loss2))
```

```python
## training process
folder = os.path.join("checkpoints", "{}_{}_{}".format(enc_layer_dim, dec_layer_dim, hidden_dim))
loadMode = None
early_stopping_flag = False

run(folder, loadMode, early_stopping_flag)
```

    Encoder(
      (embedding): Embedding(24, 300)
      (gru): GRU(300, 300, num_layers=2, dropout=0.1, bidirectional=True)
    )
    Decoder(
      (embedding): Embedding(24, 300)
      (embedding_dropout): Dropout(p=0.1)
      (gru): GRU(300, 300, num_layers=2, dropout=0.1)
      (concat): Linear(in_features=600, out_features=300, bias=True)
      (out): Linear(in_features=300, out_features=24, bias=True)
      (attn): Attn()
    )
    train_finished!
    Epoch: 1; train loss: 3.035; val loss: 2.93
    train_finished!
    Epoch: 2; train loss: 2.916; val loss: 2.89
    train_finished!
    Epoch: 3; train loss: 2.884; val loss: 2.868
    train_finished!
    Epoch: 4; train loss: 2.871; val loss: 2.866
    train_finished!
    Epoch: 5; train loss: 2.863; val loss: 2.861
    train_finished!
    Epoch: 6; train loss: 2.859; val loss: 2.86
    train_finished!
    Epoch: 7; train loss: 2.854; val loss: 2.854
    train_finished!
    Epoch: 8; train loss: 2.85; val loss: 2.853
    train_finished!
    Epoch: 9; train loss: 2.847; val loss: 2.849
    train_finished!
    Epoch: 10; train loss: 2.843; val loss: 2.829
    train_finished!
    Epoch: 11; train loss: 2.838; val loss: 2.825
    train_finished!
    Epoch: 12; train loss: 2.829; val loss: 2.816
    train_finished!
    Epoch: 13; train loss: 2.815; val loss: 2.801
    train_finished!
    Epoch: 14; train loss: 2.814; val loss: 2.799
    train_finished!
    Epoch: 15; train loss: 2.808; val loss: 2.81
    train_finished!
    Epoch: 16; train loss: 2.805; val loss: 2.795
    train_finished!
    Epoch: 17; train loss: 2.801; val loss: 2.8
    train_finished!
    Epoch: 18; train loss: 2.799; val loss: 2.792
    train_finished!
    Epoch: 19; train loss: 2.799; val loss: 2.81
    train_finished!
    Epoch: 20; train loss: 2.793; val loss: 2.804
    train_finished!
    Epoch: 21; train loss: 2.791; val loss: 2.801
    train_finished!
    Epoch: 22; train loss: 2.786; val loss: 2.795
    train_finished!
    Epoch: 23; train loss: 2.786; val loss: 2.782
    train_finished!
    Epoch: 24; train loss: 2.786; val loss: 2.808
    train_finished!
    Epoch: 25; train loss: 2.78; val loss: 2.783
    train_finished!
    Epoch: 26; train loss: 2.779; val loss: 2.776
    train_finished!
    Epoch: 27; train loss: 2.774; val loss: 2.774
    train_finished!
    Epoch: 28; train loss: 2.772; val loss: 2.808
    train_finished!
    Epoch: 29; train loss: 2.783; val loss: 2.807
    train_finished!
    Epoch: 30; train loss: 2.764; val loss: 2.772
    train_finished!
    Epoch: 31; train loss: 2.763; val loss: 2.772
    train_finished!
    Epoch: 32; train loss: 2.763; val loss: 2.792
    train_finished!
    Epoch: 33; train loss: 2.769; val loss: 2.791
    train_finished!
    Epoch: 34; train loss: 2.755; val loss: 2.768
    train_finished!
    Epoch: 35; train loss: 2.759; val loss: 2.781
    train_finished!
    Epoch: 36; train loss: 2.76; val loss: 2.779
    train_finished!
    Epoch: 37; train loss: 2.756; val loss: 2.779
    train_finished!
    Epoch: 38; train loss: 2.753; val loss: 2.763
    train_finished!
    Epoch: 39; train loss: 2.748; val loss: 2.787
    train_finished!
    Epoch: 40; train loss: 2.759; val loss: 2.785
    train_finished!
    Epoch: 41; train loss: 2.745; val loss: 2.779
    train_finished!
    Epoch: 42; train loss: 2.749; val loss: 2.797
    train_finished!
    Epoch: 43; train loss: 2.74; val loss: 2.776
    train_finished!
    Epoch: 44; train loss: 2.735; val loss: 2.786
    train_finished!
    Epoch: 45; train loss: 2.735; val loss: 2.781
    train_finished!
    Epoch: 46; train loss: 2.742; val loss: 2.783
    train_finished!
    Epoch: 47; train loss: 2.732; val loss: 2.771
    train_finished!
    Epoch: 48; train loss: 2.734; val loss: 2.783
    train_finished!
    Epoch: 49; train loss: 2.731; val loss: 2.804
    train_finished!
    Epoch: 50; train loss: 2.741; val loss: 2.775
    train_finished!
    Epoch: 51; train loss: 2.743; val loss: 2.775
    train_finished!
    Epoch: 52; train loss: 2.731; val loss: 2.775
    train_finished!
    Epoch: 53; train loss: 2.732; val loss: 2.78
    train_finished!
    Epoch: 54; train loss: 2.73; val loss: 2.754
    train_finished!
    Epoch: 55; train loss: 2.716; val loss: 2.782
    train_finished!
    Epoch: 56; train loss: 2.729; val loss: 2.803
    train_finished!
    Epoch: 57; train loss: 2.722; val loss: 2.755
    train_finished!
    Epoch: 58; train loss: 2.719; val loss: 2.802
    train_finished!
    Epoch: 59; train loss: 2.706; val loss: 2.75
    train_finished!
    Epoch: 60; train loss: 2.714; val loss: 2.771
    train_finished!
    Epoch: 61; train loss: 2.719; val loss: 2.774
    train_finished!
    Epoch: 62; train loss: 2.713; val loss: 2.756
    train_finished!
    Epoch: 63; train loss: 2.717; val loss: 2.801
    train_finished!
    Epoch: 64; train loss: 2.698; val loss: 2.806
    train_finished!
    Epoch: 65; train loss: 2.717; val loss: 2.804
    train_finished!
    Epoch: 66; train loss: 2.706; val loss: 2.776
    train_finished!
    Epoch: 67; train loss: 2.723; val loss: 2.787
    train_finished!
    Epoch: 68; train loss: 2.702; val loss: 2.773
    train_finished!
    Epoch: 69; train loss: 2.692; val loss: 2.757
    train_finished!
    Epoch: 70; train loss: 2.7; val loss: 2.752
    train_finished!
    Epoch: 71; train loss: 2.71; val loss: 2.781
    train_finished!
    Epoch: 72; train loss: 2.698; val loss: 2.769
    train_finished!
    Epoch: 73; train loss: 2.683; val loss: 2.785
    train_finished!
    Epoch: 74; train loss: 2.675; val loss: 2.784
    train_finished!
    Epoch: 75; train loss: 2.681; val loss: 2.752
    train_finished!
    Epoch: 76; train loss: 2.681; val loss: 2.809
    train_finished!
    Epoch: 77; train loss: 2.68; val loss: 2.807
    train_finished!
    Epoch: 78; train loss: 2.672; val loss: 2.78
    train_finished!
    Epoch: 79; train loss: 2.662; val loss: 2.771
    EarlyStopping ...

## Part V. Inference Process

```python
# Inference shares the same encoder/decoder structure as training process
class InferenceDecoder(nn.Module):
    def __init__(self, encoder, decoder):
        super(InferenceDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, input_t, lengths, max_target_len):
        encoder_output, encoder_hidden = encoder(input_t, lengths)
        decoder_input = torch.LongTensor([[SOS_TOKEN]])
        decoder_hidden = encoder_hidden[:decoder.n_layer]

        preds = torch.LongTensor()
        probs = torch.zeros([0])

        decoder_input = decoder_input.to(device)
        preds = preds.to(device)
        probs = probs.to(device)

        for _ in range(max_target_len):
            decoder_output, decoder_hidden = self.decoder(decoder_input, decoder_hidden, encoder_output)
            prob, indx = torch.max(decoder_output, dim=1)
            # non teacher forcing
            decoder_input = torch.LongTensor([[indx]])
            preds = torch.cat((preds, indx), dim=0)
            probs = torch.cat((probs, prob), dim=0)
            if preds[-1] == EOS_TOKEN:
                break

        return preds, probs
```

```python
# one test_seq at a time
def evaluation(inference, voc, test_seq):
    # word2index
    mapped_batch = [map_character(voc, test_seq)]
    lengths = torch.tensor([len(mapped_batch[0])])
    input_batch = torch.LongTensor(mapped_batch).t()
    input_batch = input_batch.to(device)
    lengths = lengths.to(device)
    preds, probs = inference(input_batch, lengths, 100)
    decoded_words = [voc.index2word[indx.item()] for indx in preds]
    return decoded_words
```

```python
def translate(loadMode, rand_indx):
    checkpoint = torch.load(loadMode)
    enc_chp = checkpoint["enc"]
    dec_chp = checkpoint["dec"]
    voc.__dict__ = checkpoint["voc_dict"]
    embedding_chp = checkpoint["embedding"]

    embedding = nn.Embedding(voc.num_words, hidden_dim)
    embedding.load_state_dict(embedding_chp)

    encoder = Encoder(hidden_dim, embedding, enc_layer_dim, dropout)
    decoder = Decoder(attn_method, embedding, hidden_dim, voc.num_words, dec_layer_dim, dropout)
    encoder.load_state_dict(enc_chp)
    decoder.load_state_dict(dec_chp)

    encoder = encoder.to(device)
    decoder = decoder.to(device)

    encoder.eval()
    decoder.eval()

    test_seq = input_seq[rand_indx]
    test_tar = target_seq[rand_indx]
    inference = InferenceDecoder(encoder, decoder)
    decoded_words = evaluation(inference, voc, test_seq)
    print("Input: ", test_seq)
    print("Target: ", test_tar)
    print("Prediction:", ''.join(decoded_words))
```

```python
## translation process
seq_indx = np.random.choice(range(len(input_seq)))
loadMode = os.path.join(folder, "checkpoint.tar")
translate(loadMode, seq_indx)
```

    Input:  MQINYPINPDWTTDELIEVVEFYNVVAQANEGGVEREVFLDSYRAFKRIVTSKSEEKQLNAVHDEQTGYSTYRTVQAAMKSSSKMIKL
    Target:  MKFYTIKLPRFLGGIVRAVLNAFKKG
    Prediction: MKIPPPPPVPDDDERRRRRRRRRRRRRRRLLLKKLKKLKKLKLKKLKLKLKLKELKEEELKEEEELKEEEEEEELEEEEEEEEEEEEE<EOS>

## Part VI. Plot Result

```python
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# visualize the loss as the network trained
fig = plt.figure(figsize=(10,8))
plt.plot(range(1,len(train_loss)+1),train_loss, label='Training Loss')
plt.plot(range(1,len(val_loss)+1),val_loss,label='Validation Loss')

# find position of lowest validation loss
minposs = val_loss.index(min(val_loss))+1
plt.axvline(minposs, linestyle='--', color='r',label='Early Stopping Checkpoint')

plt.xlabel('epochs')
plt.ylabel('loss')
plt.xlim(0, len(train_loss)+1) # consistent scale
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
fig.savefig('loss_plot.png', bbox_inches='tight')
```

```python

```

```python

```