# Announcments

* Start the CP if you haven't already
* We have a course tutor, Brady Moore bradenm3@illinois.edu

```python
%matplotlib inline

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

plt.rcParams["figure.figsize"] = (12, 9)
plt.rcParams["font.size"] = 18
```

## Binary Nuclear Reactions

### Learning Objectives:

- Connect concepts in particle collisions and decay to binary reactions
- Categorize nuclear reactions using standard nomenclature
- Apply conservation of nucleons to binary nuclear reactions
- Formulate Q value equations for binary nuclear reactions
- Apply conservation of energy and linear momentum to scattering
- Apply coulombic threshold
- Apply kinematic threshold
- Determine when coulombic and kinematic thresholds apply or do not

## Recall from Weeks 3 & 4

To acheive these objectives, we need to recall 3 major themes from weeks three and four.

### 1: Compare Exothermic and Endothermic reactions

- In **_exothermic_** or **_exoergic_** reactions, energy is **emitted** ($Q>0$)
- In **_endothermic_** or **_endoergic_** reactions, energy is **absorbed** ($Q<0$)

(credit: BBC)

### 2:  Relate energy and mass  $E=mc^2$

When the masses of reactions change, this is tied to a change in energy from whence we learn the Q value.
This change in mass is equivalent to a change in energy because **$E=mc^2$**

\begin{align}
A + B + \cdots &\rightarrow C + D + \cdots\\
\mbox{(reactants)} &\rightarrow \mbox{(products)}\\
\implies \Delta M &= (\mbox{reactants}) - (\mbox{products})\\
         &= (M_A + M_B + \cdots) - (M_C + M_D + \cdots)\\
\implies \Delta E &= \left[(M_A + M_B + \cdots) - (M_C + M_D + \cdots)\right]c^2\\
\end{align}

### 3: Apply conservation of energy and momentum to scattering collisions

Conservation of total energy and linear momentum can inform Compton scattering reactions. X-rays scattered from electrons had a change in wavelength $\Delta\lambda = \lambda' - \lambda$ proportional to $(1-\cos{\theta_s})$

We used the law of cosines:

\begin{align}
p_e^2 &= p_\lambda^2 + p_{\lambda'}^2 - 2p_\lambda p_{\lambda'}\cos{\theta_s}
\end{align}

And we also used conservation of energy:
\begin{align}
p_\lambda c+m_ec^2 &=  p_{\lambda'}c + mc^2\\
\mbox{where }&\\
m_e&=\mbox{rest mass of the electron}\\
m &= \mbox{relativistic electron mass after scattering}
\end{align}

Combining these with our understanding of photon energy ($E=h\nu=pc$) gives:

\begin{align}
\lambda' - \lambda &= \frac{h}{m_ec}(1-\cos{\theta_s})\\
\implies \frac{1}{E'} - \frac{1}{E} &= \frac{1}{m_ec^2}(1-\cos{\theta_s})\\
\implies E'  &= \left[\frac{1}{E} + \frac{1}{m_ec^2}(1-\cos{\theta_s})\right]^{-1}\\
\end{align}

## More Types of Reactions

Previously we were interested in fundamental particles striking one another (e.g. the electron and proton in Compton scattering) or nuclei emitting such particles (e.g. $\beta^\pm$ decay).

**Today:** We are interested in myriad additional reactants and/or products. In particular, we're interested in:

- neutron absorption and production reactions
- _binary, two-product nuclear reactions_ in which two products emerge with new energies after the collision.

## Reaction Nomenclature

**Transfer Reactions:** Nucleons (1 or 2) are transferred between the projectile and product.

**Scattering reactions:** The projectile and product emerge from a collision with the same identities as when they started, exchanging only kinetic energy.

**Knockout reactions:** The projectile directly interacts with the target nucleus and is re-emitted **along with** nucleons from the target nucleus.

**capture reactions:** The projectile is absorbed, typically exciting the nucleus. The excited nucleus may emit that energy decaying via photon emission.

**nuclear photoeffect:** A photon projectile liberates a nucleon from the target nucleus.

### Think Pair Share : categorize these reactions

One example of each of the above appears below. Use the definitions to categorize them.

- $(n, n)$
- $(n, \gamma)$
- $(n, 2n)$
- $(\gamma, n)$
- $(\alpha, n)$

## Binary, two-product nuclear reactions

**Two initial nuclei collide to form two product nuclei.**

\begin{align}
^{A_1}_{Z_1}X_1 + ^{A_2}_{Z_2}X_2 \longrightarrow ^{A_3}_{Z_3}X_3 + ^{A_4}_{Z_4}X_4
\end{align}

#### Applying Conservation of Neutrons and Protons

The total number of nucleons is always conserved.
If the `______________` force is not involved, we can also apply this conservation separately.

In most binary, two-product nuclear reactions, this is the case, so the number of protons and neutrons are conserved. Thus:

\begin{align}
Z_1 + Z_2 = Z_3 + Z_4\\
A_1 + A_2 = A_3 + A_4
\end{align}

Apply this to the following:

\begin{align}
^{3}_{1}H + ^{16}_{8}O \longrightarrow \left(X\right)^* \longrightarrow  ^{16}_{7}N + ^{A_4}_{Z_4}X_4
\end{align}

### Think Pair Share:

What are :

- $A_4$
- $Z_4$
- $X_4$?

- Bonus: What is $\left(X\right)^*$?

#### Applying conservation of mass and energy.

The Q-value calculation is the same as it has been before.
The Q value represents the `________` in kinetic energy and, equivalently, a `________` in the rest masses.

\begin{align}
Q &= E_y + E_Y − E_x − E_X \\
 &= (m_x + m_X − m_y − m_Y )c^2\\
 &= \left(m\left(^{A_1}_{Z_1}X_1\right) + m\left(^{A_2}_{Z_2}X_2\right) - m\left(^{A_3}_{Z_3}X_3\right) - m\left(^{A_4}_{Z_4}X_4\right)\right)c^2\\
\end{align}

If proton numbers are conserved (true for everything but electron capture or reactions involving the weak force.), we can use the approximation that $m(X) = M(X)$.

\begin{align}
Q &= E_y + E_Y − E_x − E_X \\
  &= (m_x + m_X − m_y − m_Y )c^2\\
  &= (M_x + M_X − M_y − M_Y )c^2\\
   &= \left(M\left(^{A_1}_{Z_1}X_1\right) + M\left(^{A_2}_{Z_2}X_2\right) - M\left(^{A_3}_{Z_3}X_3\right) - M\left(^{A_4}_{Z_4}X_4\right)\right)c^2\\
\end{align}

```python
def q(m_reactants, m_products):
    """Returns Q

    Parameters
    ----------
    m_reactants: list (of doubles)
        the masses of the reactant atoms [amu]
    m_products : list (of doubles)
        the masses of the product atoms [amu]
    """
    amu_to_mev = 931.5 # MeV/amu conversion
    m_difference = sum(m_reactants) - sum(m_products)
    return m_difference*amu_to_mev

# Look up the masses:
h_3_mass = 3.0160492675
o_16_mass = 15.9949146221
he_3_mass = 3.0160293097
n_16_mass = 16.0061014

m_react = [h_3_mass, o_16_mass]
m_prods = [he_3_mass, n_16_mass]

print("Q: ", q(m_react, m_prods))
```

#### Applying conservation of linear momentum

Let's get back to collision kinematics.

First, we'll assume the target nucleus ($X_2$) is initially at rest.

# Kinematic Threshold

Relying on a combination of kinetic energies $E_i$ and corresponding linear momenta:

\begin{align}
p_i = \sqrt{2m_iE_i}
\end{align}

We can determine that some reactions aren't possible without a certain minimum quantity of kinetic energy.

The solution to $E_3$ can become nonphysical if :

- $\cos{\theta_3} < 0$
- $Q < 0$
- $m_4 - m_1 < 0$

## For Exoergic Reactions ($Q>0$)

For $Q>0$ and $m_{4} > m_{1}$, $E_{3} = (a + \sqrt{a^2+b^2})^2$ is the only real, positive, meaningful solution.

The kinetic energy of $E_3$ is, at minimum, the energy arrived at when $p_1 = 0$. Thus:

\begin{align}
E_3 \longrightarrow& \frac{m_4}{m_3 + m_4}Q\\
&\mbox{ when } Q>0, p_1=0
\end{align}

So, no exoergic reactions are restricted by kinetics, as $Q = E_3 + E_4$, for the minimum linear momentum case, which is real and positive.

## For Endoergic Reactions ($Q<0$)
Some $Q<0$ reactions aren't possible without a certain minimum quantity of kinetic energy.

For $Q<0$ and $m_{4} > m_{1}$, some values of $E_{1}$ are too small to carry forward a real, positive solution. That is, the incident projectile must supply a minimum amount of kinetic energy before the reaction can occur. Without this energy, the solution for $E_3$ results in physically meaningless values. This minimum energy can be found from eqn 6.11 in your book and is :

\begin{align}
E_1^{th,k} = -\frac{m_3 + m_4}{m_3 + m_4 - m_1}Q.
\end{align}

One can often simplify this (assuming $m_i >> Q/c^2$ and $m_3 + m_4 - m_1 \simeq m_2$)  :

\begin{align}
E_1^{th,k} \simeq - \left( 1 + \frac{m_1}{m_2} \right)Q.
\end{align}

```python
def kinematic_threshold(m_1, m_3, m_4, Q):
    """Returns the kinematic threshold energy [MeV]

    Parameters
    ----------
    m_1: double
        mass of incident projectile
    m_3: double
        mass of first product
    m_3: double
        mass of second product
    Q : double
        Q-value for the reaction [MeV]
    """
    num = -(m_3 + m_4)*Q
    denom = m_3 + m_4 - m_1
    return num/denom

def kinematic_threshold_simple(m_1, m_2, Q):
    """Returns the coulombic threshold energy [MeV]

    Parameters
    ----------
    m_1: double
        mass of incident projectile
    m_2: double
        mass of target
    Q : double
        Q-value for the reaction [MeV]
    """
    to_return = -(1 + m_1/m_2)*Q
    return to_return
```

# Coulombic Threshold

Coulomb forces repel a projectile if it is:

- a positively charged nucleus
- a proton

The force between the projectile (particle 1) and the target nucleus (particle 2) is :

\begin{align}
&F_C = \frac{Z_1Z_2e^2}{4\pi\epsilon_0r^2}\\
\mbox{where}&&\\
&\epsilon_0 = \mbox{the permittivity of free space.}
\end{align}

### Think pair share:
What are the other terms in the above equation:

- $Z_1$ ?
- $Z_2$ ?
- $e$ ?
- $r$ ?

By evaluating the work function for approach to the nucleus with a coulomb barrier, we can establish that the coulombic threshold energy (in MeV) is :

\begin{align}
E_1^{th,C} \simeq 1.20 \frac{Z_1Z_2}{A_1^{1/3}+A_2^{1/3}}
\end{align}

```python
def colombic_threshold(z_1, z_2, a_1, a_2):
    """Returns the coulombic threshold energy [MeV]

    Parameters
    ----------
    z_1: int
        proton number of incident projectile
    z_2: int
        proton number of target
    a_1 : int or double
        mass number of the incident projectile [amu]
    a_2 : int or double
        mass number of the target [amu]
    """
    num = 1.20*z_1*z_2
    denom = pow(a_1, 1/3) + pow(a_2, 1/3)
    return num/denom
```

### Think Pair Share

Which thresholds apply to the below situations:

- A chargeless incident particle, reaction $Q>0$
- A chargeless incident particle, reaction $Q<0$
- A positively charged incident particle, reaction $Q>0$
- A positively charged incident particle, reaction $Q<0$

## Overall threshold

For the case where both thresholds apply, the minimum energy for the reaction to occur is the highest of the two thresholds.

\begin{align}
\min{\left(E_1^{th}\right)}	= \max{\left(E^{th,C}_1,E_1^{th,k}\right)}.
\end{align}

## Example

Take the (p, n) reaction from $^{9}Be\longrightarrow^{9}B$. We will need to calculate:

- The Q value
- The kinematic threshold (if it applies)
- The coulombic threshold (if it applies)
- Determine which one is higher

```python
# Q value
# Look up the masses:
be_9_mass = 9.0121821
b_9_mass = 9.0133288
n_mass = 1.0086649158849
p_mass = 1.007825032 # hydrogen nucleus!

m_react = [be_9_mass, p_mass]
m_prods = [b_9_mass, n_mass]

q_example = q(m_react, m_prods)
print("Q: ", q_example)
```

```python
# Kinematic Threshold
# Which particles were which again?
m_1 = p_mass
m_2 = be_9_mass
m_3 = n_mass
m_4 = b_9_mass

# Calculate using both regular and simpler methods
E_k_th = kinematic_threshold(m_1, m_3, m_4, q_example)
E_k_th_simple = kinematic_threshold_simple(m_1, m_2, q_example)
print("E_k_th: ", E_k_th)
print("E_k_th (simplified): ", E_k_th_simple)
```

```python
# Coulombic Threshold
# Need some charge info and mass numbers
z_1 = 1 # proton
z_2 = 4 # Be
a_1 = 1 # proton
a_2 = 9 # Be

E_c_th = colombic_threshold(z_1, z_2, a_1, a_2)

print("E_c_th: ", E_c_th)
```

```python
## Which one is higher?

print("Total threshold: ", max(E_c_th, E_k_th))
```

# Applications: Neutron Detection
Neutron's don't tend to directly ionize matter as they pass through. However, they can instigate nuclear reactions which produce charged products. These products, in turn, can be detected due to the ionization they create. The scheme for a Boron Trifluoride detector is below (hosted at https://www.orau.org/ptp/collection/proportional%20counters/bf3info.htm).

The wall effect results in the following spectrum (approximately):

In (n,p) reactions, for example, variation in emission angle of particle 3 can be used to determine the energy of the original incident neutron.

# Applications: Neutron Production
Specific neutron energies can be targetted by collecting them at a certain angle away from the production collision.

The accelerator and spallation target at LANSCE and other spallation experiments rely on this fact.

## Two energies

In (p,n) reactions, for example, certain proton energies may result in more than one neutron energy observed at a single angle. How?

Recall the equation (Shultis and Faw 6.11):

\begin{align}
\sqrt{E_y}=&\sqrt{\frac{m_xm_yE_x}{(m_y + m_Y)^2}}\cos\theta_y \\
&\pm \sqrt{\frac{m_xm_yE_x}{(m_y + m_Y)^2}\cos^2\theta_y + \left[\frac{m_Y-m_x}{(m_y + m_Y)}E_x + \frac{m_YQ}{(m_y + m_Y)}\right]}
\end{align}

Dr. Munk prefers this notation:
\begin{align}
\sqrt{E_3}=&\sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}}\cos\theta_3 \\
&\pm \sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}\cos^2\theta_3 + \left[\frac{m_4-m_1}{(m_3 + m_4)}E_1 + \frac{m_4Q}{(m_3 + m_4)}\right]}
\end{align}

## Heavy Particle scattering from an electron

Much like the Compton reaction we saw between photons and electrons, we can see a similar reaction with heavy particles. Occaisionally, a heavy particle (e.g. a small nucleus, like an $\alpha$ particle) strikes the orbital electrons in atoms of a medium.

Thus: particles 2 and 3 are the electron. So:

\begin{align}
m_2 &= m_3 = m_e = \mbox{(the electron mass)}\\
E_3 &= E_e = \mbox{(the recoil electron energy)}\\
m_1 &= m_4 = \mbox{(the mass of the heavy particle)}\\
E_1 &= E_4 = \mbox{(the kinetic energy of the incident heavy particle)}
\end{align}

For this scattering process, there is no change in the rest masses of the reactants, so Q = 0.

We can use the Shutlis and Faw 6.11 equation above to arrive at:

\begin{align}
\sqrt{E_e}=& \frac{2}{m_4 + m_e}\sqrt{m_4m_eE_4}\cos{\theta_e}
\end{align}

We can approximate that $m_4 >> m_e$ such that the electron recoil energy becomes:

\begin{align}
\implies E_e =& 4\frac{m_e}{m_4}E_4\cos^2{\theta_e}
\end{align}

## Think Pair Share
What angle, $\theta_e$, corresponds to the maximimum loss of kinetic energy by the incident heavy particle?

At $\theta_e=0$, we find that:

\begin{align}
(E_e)_{max} = 4\frac{m_e}{m_4}E_4
\end{align}

```python
import math
def recoil_energy(m_4, e_4, theta_e):
    m_e = 0.0005486 # amu
    num = 4*m_e*e_4*pow(math.cos(theta_e), 2)
    return num/m_4

```

```python
th = [math.radians(-90),
      math.radians(-75),
      math.radians(-60),
      math.radians(-45),
      math.radians(-30),
      math.radians(-15),
      math.radians(0),
      math.radians(15),
      math.radians(30),
      math.radians(45),
      math.radians(60),
      math.radians(75),
      math.radians(90)]

m_4 = 4.003 # alpha particle

to_plot_4 = np.arange(0.,len(th))
to_plot_10 = np.arange(0.,len(th))

for k, v in enumerate(th):
    to_plot_4[k] = (recoil_energy(m_4, 4, v))
    to_plot_10[k] = (recoil_energy(m_4, 10, v))

plt.plot(th, to_plot_4, label="$4MeV$")
plt.plot(th, to_plot_10, label="$10MeV$")

plt.ylabel("Electron Recoil Energy ($MeV$)")
plt.xlabel("Angle (radians)")
plt.legend(loc=2)
```

```python

th = 0
m_4 = 4.003 # alpha particle
e_4 = 4 # MeV

print("Max (4MeV alpha): ", recoil_energy(m_4, e_4, th))
```

## Neutron Scattering

### Neutron interactions with matter.

\begin{align}
^1_0n + {^a_z}X \longrightarrow
\begin{cases}
^1_0n + {^a_z}X  & \mbox{Elastic Scattering}\\
^1_0n + \left({^a_z}X\right)^*  & \mbox{Inlastic Scattering}
\end{cases}
\end{align}

Using the ubiquitous equation 6.11 for a neutron scatter:

\begin{align}
\sqrt{E_3}=&\sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}}\cos\theta_3 \\
&\pm \sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}\cos^2\theta_3 + \left[\frac{m_4-m_1}{(m_3 + m_4)}E_1 + \frac{m_4Q}{(m_3 + m_4)}\right]}\\
\end{align}

We can define our particles as a neutron hitting a nucleus and changing in its energy.

\begin{align}
m_1 = m_3 = m_n\\
E_1 = E_n\\
E_3 = E_n'\\
\end{align}

Such that:

\begin{align}
\sqrt{E_n'} =&\sqrt{\frac{m_nm_nE_n}{(m_n + m_4)^2}}\cos\theta_s \\
&\pm \sqrt{\frac{m_nm_nE_n}{(m_n + m_4)^2}\cos^2\theta_s + \left[\frac{m_4-m_n}{(m_n + m_4)}E_n + \frac{m_4Q}{(m_n + m_4)}\right]}
\end{align}

We can also agree that $m_2=m_4$, which is some nucleus with a mass that is approximately the same at the beginning and end of the scatter (approximate if the scattering is inelastic) . This gives, with some rearrangement:

\begin{align}
\sqrt{E_n'} &= \frac{1}{m_4 + m_n}\times\\ &\left[\sqrt{m_n^2E_n}\cos{\theta_s} \pm \sqrt{E(m_4^2 + m_n^2\cos^2{\theta_s} − m_n^2) + m_4 ( m_4 + m_n ) Q }\right]
\end{align}

And, for elastic scattering ($Q=0$):

\begin{align}
E' = \frac{1}{(A+1)^2}\left[\sqrt{E}\cos{\theta_s} + \sqrt{E(A^2 - 1 + \cos{\theta_s}^2)}\right]^2
\end{align}

```python
def scattered_neutron_energy(A, E, th):
    """Returns the energy of a scattered neutron [MeV]
    Parameters
    ----------
    A: int or double
        mass number of medium
    E: double
        kinetic energy of the incident neutron [MeV]
    th : double
        scattering angle, in degrees
    """
    cos_th = math.cos(math.radians(th))
    term1 = 1/((A+1)**2)
    term2 = math.sqrt(E)*cos_th
    term3 = math.sqrt(E*(A**2 - 1 + cos_th**2))
    return term1*((term2 + term3)**2)
```

```python
th = [math.radians(-90),
      math.radians(-75),
      math.radians(-60),
      math.radians(-45),
      math.radians(-30),
      math.radians(-15),
      math.radians(0),
      math.radians(15),
      math.radians(30),
      math.radians(45),
      math.radians(60),
      math.radians(75),
      math.radians(90)]

e_initial = 2.0 # 2 MeV is special
a_light = 4.003 # alpha particle
a_heavy = 235.0 # uranium atom

to_plot_light = np.arange(0.,len(th))
to_plot_heavy = np.arange(0.,len(th))

for k, v in enumerate(th):
    to_plot_light[k] = (scattered_neutron_energy(a_light, e_initial, v))
    to_plot_heavy[k] = (scattered_neutron_energy(a_heavy, e_initial, v))

plt.plot(th, to_plot_light, label="light")
plt.plot(th, to_plot_heavy, label="heavy")

plt.ylabel("Scattered Neutron Energy ($MeV$)")
plt.xlabel("Angle (radians)")
plt.legend(loc=2)
```

## Average Energy Loss

For elastic scattering (Q = 0), we see the minimum and maxium energies occur at the maximum and minimum angles.

\begin{align}
E'_{max} &= E'(\theta_{s,min})\\
         &= E'(\theta_{s}=0)\\
         &= E\\
E'_{min} &= E'(\theta_{s,max})\\
         &= E'(\theta_{s}=\pi)\\
         &= \frac{(A-1)^2}{(A+1)^2} E\\
         &\equiv \alpha E\\
\end{align}

For isotropic scattering, we can find the average loss:

\begin{align}
(\Delta E)_{av} &\equiv E - E'_{av}\\
&= E− 1(E+\alpha E)\\
& = 1(1- \alpha)E
\end{align}

```python
def alpha(a):
    """Returns the average energy loss of a
    scattered neutron [MeV]
    Parameters
    ----------
    A: int or double
        mass number of medium
    """
    num = (a-1)**2
    denom = (a+1)**2
    return num/denom

def average_energy_loss(A, E):
    """Returns the average energy loss of a scattered neutron [MeV]
    Parameters
    ----------
    A: int or double
        mass number of medium
    E: double
        kinetic energy of the incident neutron [MeV]
    """
    return 1*(1-alpha(A))*E
```

```python
e_initial = np.arange(0, 2, 0.001)

to_plot_light = np.arange(0.,len(e_initial))
to_plot_heavy = np.arange(0.,len(e_initial))

for k, v in enumerate(e_initial):
    to_plot_light[k] = (average_energy_loss(a_light, v))
    to_plot_heavy[k] = (average_energy_loss(a_heavy, v))

plt.plot(e_initial, to_plot_light, label="light atom")
plt.plot(e_initial, to_plot_heavy, label="heavy atom")

plt.ylabel("Average Neutron Energy Loss ($MeV$)")
plt.xlabel("Initial neutron energy ($MeV$)")
plt.legend(loc=2)
```

## Logarithmic Energy Loss

It turns out, on a logarithmic energy scale, a neutron loses the same amount of logarithmic energy per elastic scatter, regardless of its initial energy. So, this is a helpful term, particularly since neutron energies can range by many orders of magnitude. So, we often use 'logarithmic energy loss' when discussing this downscattering. This is also called "lethargy".

\begin{align}
\left(\ln{(E)} - \ln{(E')}\right)_{av} & = \overline{\ln{\left(\frac{E}{E'}\right)}} \\
&= 1 + \frac{\alpha}{1-\alpha}\\
&= \xi\\
&= \mbox{average logarithmic energy loss per elastic scatter}\\
&= \mbox{lethargy}
\end{align}

```python
def lethargy(a):
    """Returns the average logarithmic energy
    loss per elastic scatter
    Parameters
    ----------
    A: int or double
        mass number of medium
    """
    return 1.0 + alpha(a)/(1-alpha(a))
```

```python
a = np.arange(1, 240)
plt.plot([lethargy(i) for i in a])
plt.ylabel("$\\xi$")
plt.xlabel("A($amu$)")

```

## Thermal Neutrons

1. a fast neutron slows down
2. may eventually come into thermal equilibrium with the medium
3. thermal motion of atoms in medium are in Maxwellian distribution
4. neutron may gain kinetic energy upon scattering from a rapidly moving nucleus
5. neutron may lose energy upon scattering from a slowly moving nucleus.

At room temperature, 293 K:
- the most probable kinetic energy of thermal neutrons is 0.025 eV
- 0.025 eV corresponds to a neutron speed of about 2200 m/s.

## Epithermal

Neutrons that are faster than thermal neutrons, but aren't quite "fast" are called _epithermal_. ($0.2eV < E_{epi} < 1 MeV$)

## Fast

$> 1MeV$

## Neutron Capture

- Free neutrons will eventually be absorbed by a nucleus (or escape the domain of interest)
- Neutron capture leaves the nucleus excited
- Actually, very excited (Recall: what is a typical binding energy per nucleon?)
- When it's released as a $\gamma$ that energy can be very hazardous

Neutron slowing down can help us to reduce very high energy $\gamma$ emissions.

## Fission Reactions

Some nuclei spontaneously fission (e.g. $^{252}Cf$). However, this isn't common.

\begin{align}
^1_0n + ^{235}_{92}U \longrightarrow \left( ^{236}_{92}U \right)^*
\begin{cases}
^{235}_{92}U + ^1_0n & \mbox{Elastic Scattering}\\
^{235}_{92}U + ^1_0n' +  \gamma & \mbox{Inelastic Scattering}\\
^{236}_{92}U + \gamma & \mbox{Radiative Capture}\\
^{A_H}_{Z_H}X_H + ^{A_L}_{Z_L}X_L + ^1_0n  + \cdots & \mbox{Fission}
\end{cases}
\end{align}

# Announcements

* I am modifying the schedule a bit. On Friday I will introduce different reactor types and then on Monday we are going to have a guest lecture on the nuclear fuel cycle by **Amanda Bachmann**.
* The homework I assign on Friday will be due the Monday after spring break
* If you wanted an adjustment on your midterm exam, you **need to email me** with the details so I can record it properly.
* Exam 2 moved to 4/4. Syllabus updated on github.

### Recall: Cross sections

The likelihood of each of these scattering events is captured by cross sections.

- $\sigma_x = $ microscopic cross section $[cm^2]$
- $\Sigma_x = $ macroscopic cross section $[1/length]$
- $\Sigma_x = N\sigma_x $
- $N = $ number density of target atoms $[\#/volume]$

### Cross sections are in units of area. Explain this to your neighbor.

### What energy neutron do we prefer for fission in $^{235}U$?

Nuclei that undergo neutron induced fission can be categorized into three types:

- fissile: can fission with a slow neutron ($^{235}U$, $^{233}U$, $^{239}Pu$)
- fissionable: require high energy (>1MeV) neutron ($^{238}U$, $^{240}Pu$)
- fertile: can be converted into fissile or fissionable nuclide (breeding reactions)

Key breeding reactions are :

\begin{align}
{^{232}_{90}}Th + ^1_0n \longrightarrow {^{233}_{90}}Th  \overset{\beta^-}{\longrightarrow}  {^{233}_{91}}Pa   \overset{\beta^-}{\longrightarrow}  {^{233}_{92}}U\\
{^{238}_{92}}U + ^1_0n \longrightarrow {^{239}_{92}}U  \overset{\beta^-}{\longrightarrow}  {^{239}_{93}}Np   \overset{\beta^-}{\longrightarrow}  {^{239}_{94}}Pu\\
\end{align}

## The fission process

\begin{align}
^1_0n + ^{235}_{92}U \longrightarrow \left( ^{236}_{92}U \right)^* \longrightarrow  X_H + X_L + \nu_p\left(^1_0n\right) + \gamma_p
\end{align}

Conserving neutrons and protons:

\begin{align}
A_L + A_H + \nu_p &= 236\\
N_L + N_H + \nu_p &= 144\\
Z_L + Z_H &= 92\\
\end{align}

## Fission Product Decay

The fission fragments end up very neutron rich.

### Think Pair Share
Recall the chart of the nuclides. How will these fission products likely decay?

### Fission Spectrum

$\chi(E)$ is an empirical probability density function describing the energies of prompt fission neutrons.

\begin{align}
\chi (E) &= 0.453e^{-1.036E}\sinh\left(\sqrt{2.29E}\right)\\
\end{align}

```python
import numpy as np
import math
def chi(energy):
    return 0.453*np.exp(-1.036*energy)*np.sinh(np.sqrt(2.29*energy))

energies = np.arange(0.0,10.0, 0.1)

plt.plot(energies, chi(energies))
plt.title(r'Prompt Neutron Energy Distribution $\chi(E)$')
plt.xlabel("Prompt Neutron Energy [MeV]")
plt.ylabel("probability")
```

#### Questions about this plot:

- What is the most likely prompt neutron energy?
- Can you write an equation for the average neutron energy?
- Can you write an equation for the average neutron energy?

```python
print(max([chi(e) for e in energies]), chi(0.7))
```

#### Expectation Value

Recall that the average energy will be the expectation value of the probability density function.

\begin{align}
<E> &= \int E\chi(E)dE\\
&= E \chi(E)
\end{align}

```python
plt.plot(energies, [chi(e)*e for e in energies])
```

## Prompt and Delayed neutrons

- Most of the neutrons in fission are emitted within $10^{-14}s$.
  - **prompt** neutrons
  - $\nu_p$
- Some, ($<1\%$) are produced by delayed decay of fission products.
  - **delayed** neutrons
  - $\nu_d$

We define the delayed neutron fraction as :

\begin{align}
\beta \equiv \frac{\nu_d}{\nu_d + \nu_p}
\end{align}

## Energy from fission

### 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.

### Source term

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}


```python
# %load /Users/facai/Study/book_notes/preconfig.py
%matplotlib inline

import matplotlib.pyplot as plt
import seaborn as sns

import numpy as np

import pandas as pd

from IPython.display import Image
```

Chapter 6 Temporal-Difference Learning
=====================

DP, TD, and Monte Carlo methods all use some variation of generalized policy iteration: primarily differences in their approaches to the prediction problem.

### 6.1 TD Prediction

constant-$\alpha$ MC: $V(S_t) \gets V(S_t) + \alpha \underbrace{\left [ G_t - V(S_t) \right ]}_{= \sum_{k=t}^{T-1} \gamma^{k-1} \theta_k}$

\begin{align}
    v_\pi(s) &\doteq \mathbb{E}_\pi [ G_t \mid S_s = s] \qquad \text{Monte Carlo} \\
             &= \mathbb{E}_\pi [ R_{t+1} + \gamma \color{blue}{v_\pi(S_{t+1})} \mid S_t = s ] \quad \text{DP}
\end{align}

one-step TD, or TD(0): $V(S_t) \gets V(S_t) + \alpha \left [ \underbrace{R_{t+1} + \gamma \color{blue}{V(S_{t+1})} - V(S_t)}_{\text{TD error: } \theta_t} \right ]$

TD: samples the expected values and uses the current estimate $V$ instead of the true $v_\pi$.

```python
Image('./res/fig6_1.png')
```

```python
Image('./res/TD_0.png')
```

### 6.2 Advantages of TD Prediction Methods

TD: they learn a guess from a guess - they boostrap.

+ advantage:
  1. over DP: TD do not require a model of the environment, of its reward and next-state probability distributions.
  2. over Monte Carlo: TD are naturally implemented in an online, fully incremental fashion.

TD: guarantee convergence.

In practice, TD methods have usually been found that converge faster than constant-$\alpha$ MC methods on stochastic tasks.

### 6.3 Optimality of TD(0)

batch updating: updates are made only after processing each complete batch of training data until the value function converges.

+ Batch Monte Carlo methods: always find the estimates that minimize mean-squared error on the training set.
+ Batch TD(0): always find the estimates that would be exactly correct for the maximum-likelihood model of the Markov process.

### 6.4 Sarsa: On-policy TD Control

$Q(S_t, A_t) \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) - Q(S_t,, A_t) \right ]$

```python
Image('./res/sarsa.png')
```

### 6.5 Q-learning: Off-policy TD Control

$Q(S_t, A_t) \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma \color{blue}{\max_a Q(S_{t+1}, a)} - Q(S_t,, A_t) \right ]$

```python
Image('./res/q_learn_off_policy.png')
```

### 6.6 Expected Sarsa

use expeteced value, how likely each action is under the current policy.

\begin{align}
    Q(S_t, A_t) & \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma \color{blue}{\mathbb{E}[Q(S_{t+1}, A_{t+1}) \mid S_{t+1}]} - Q(S_t,, A_t) \right ] \\
                & \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma \color{blue}{\sum_a \pi(a \mid S_{t+1}) Q(S_{t+1}, a)} - Q(S_t,, A_t) \right ]
\end{align}

+ con: additional computational cost.
+ pro: eliminate the variance due to the random seleciton of $A_{t+1}$.

### 6.7 Maximization Bias and Double Learning

maximization bias:
+ a maximum over estimated values => an estimate of the maximum value => significant positive bias.

root of problem: using the same samples (plays) both to determine the maximizing action and to estimate its value. => divide the plays in two sets ($Q_1, Q_2$) and use them to learn two indepedent estimates. (*double learning*)

$Q_1(S_t, A_t) \gets Q_1(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma Q_2 \left( S_{t+1}, \operatorname{argmax}_a Q_1(S_{t+1}, a) \right) - Q_1(S_t,, A_t) \right ]$

```python
Image('./res/double_learn.png')
```

### 6.8 Games, Afterstates, and Other Special Cases

```python

```


# Textual entailment classifier using an MLP plus attention

In textual entailment,
the input is 2 sentences (premise and hypothesis), and the output
is a label, specifying if P entails H, P contradicts H, or neither.
(This is also called "natural language inference".)
We use attention to align hypothesis to premise and vice versa,
then compare the aligned words to estimate similarity between the sentences, and pass the weighted similarities to an MLP.

Based on sec 15.5 of http://d2l.ai/chapter_natural-language-processing-applications/natural-language-inference-attention.html

```python
!pip install -q flax
```

         |████████████████████████████████| 184 kB 12.2 MB/s
         |████████████████████████████████| 136 kB 50.2 MB/s
         |████████████████████████████████| 72 kB 709 kB/s

```python
import jax
import jax.numpy as jnp                # JAX NumPy

from flax import linen as nn           # The Linen API
from flax.training import train_state  # Useful dataclass to keep train state
import torch
from torch.utils import data           # For data

import numpy as np                     # Ordinary NumPy
import optax                           # Optimizers

import collections
import re
import os
import requests
import zipfile
import tarfile
import hashlib
import time
import functools
from typing import Any, Callable, Sequence, Tuple

import matplotlib.pyplot as plt
import pandas as pd
import math
from IPython import display

rng = jax.random.PRNGKey(0)
!mkdir figures # for saving plots
ModuleDef = Any
```

# Data

We use SNLI (Stanford Natural Language Inference) dataset described in sec 15.4 of http://d2l.ai/chapter_natural-language-processing-applications/natural-language-inference-and-dataset.html.

```python
# Required functions for downloading data

def download(name, cache_dir=os.path.join('..', 'data')):
    """Download a file inserted into DATA_HUB, return the local filename."""
    assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}."
    url, sha1_hash = DATA_HUB[name]
    os.makedirs(cache_dir, exist_ok=True)
    fname = os.path.join(cache_dir, url.split('/')[-1])
    if os.path.exists(fname):
        sha1 = hashlib.sha1()
        with open(fname, 'rb') as f:
            while True:
                data = f.read(1048576)
                if not data:
                    break
                sha1.update(data)
        if sha1.hexdigest() == sha1_hash:
            return fname  # Hit cache
    print(f'Downloading {fname} from {url}...')
    r = requests.get(url, stream=True, verify=True)
    with open(fname, 'wb') as f:
        f.write(r.content)
    return fname

def download_extract(name, folder=None):
    """Download and extract a zip/tar file."""
    fname = download(name)
    base_dir = os.path.dirname(fname)
    data_dir, ext = os.path.splitext(fname)
    if ext == '.zip':
        fp = zipfile.ZipFile(fname, 'r')
    elif ext in ('.tar', '.gz'):
        fp = tarfile.open(fname, 'r')
    else:
        assert False, 'Only zip/tar files can be extracted.'
    fp.extractall(base_dir)
    return os.path.join(base_dir, folder) if folder else data_dir
```

```python
DATA_HUB = dict()
DATA_HUB['SNLI'] = ('https://nlp.stanford.edu/projects/snli/snli_1.0.zip',
                        '9fcde07509c7e87ec61c640c1b2753d9041758e4')

data_dir = download_extract('SNLI')
```

    Downloading ../data/snli_1.0.zip from https://nlp.stanford.edu/projects/snli/snli_1.0.zip...

```python
def read_snli(data_dir, is_train):
    """Read the SNLI dataset into premises, hypotheses, and labels."""
    def extract_text(s):
        # Remove information that will not be used by us
        s = re.sub('\\(', '', s)
        s = re.sub('\\)', '', s)
        # Substitute two or more consecutive whitespace with space
        s = re.sub('\\s{2,}', ' ', s)
        return s.strip()

    label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2}
    file_name = os.path.join(
        data_dir, 'snli_1.0_train.txt' if is_train else 'snli_1.0_test.txt')
    with open(file_name, 'r') as f:
        rows = [row.split('\t') for row in f.readlines()[1:]]
    premises = [extract_text(row[1]) for row in rows if row[0] in label_set]
    hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set]
    labels = [label_set[row[0]] for row in rows if row[0] in label_set]
    return premises, hypotheses, labels
```

Show first 3 training examples and their labels (“0”, “1”, and “2” correspond to “entailment”, “contradiction”, and “neutral”, respectively ).

```python
train_data = read_snli(data_dir, is_train=True)
for x0, x1, y in zip(train_data[0][:3], train_data[1][:3], train_data[2][:3]):
    print('premise:', x0)
    print('hypothesis:', x1)
    print('label:', y)
```

    premise: A person on a horse jumps over a broken down airplane .
    hypothesis: A person is training his horse for a competition .
    label: 2
    premise: A person on a horse jumps over a broken down airplane .
    hypothesis: A person is at a diner , ordering an omelette .
    label: 1
    premise: A person on a horse jumps over a broken down airplane .
    hypothesis: A person is outdoors , on a horse .
    label: 0

```python
test_data = read_snli(data_dir, is_train=False)
for data in [train_data, test_data]:
    print([[row for row in data[2]].count(i) for i in range(3)])
```

    [183416, 183187, 182764]
    [3368, 3237, 3219]

```python
def tokenize(lines, token='word'):
    """Split text lines into word or character tokens."""
    if token == 'word':
        return [line.split() for line in lines]
    elif token == 'char':
        return [list(line) for line in lines]
    else:
        print('ERROR: unknown token type: ' + token)

class Vocab:
    """Vocabulary for text."""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # Sort according to frequencies
        counter = count_corpus(tokens)
        self.token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                  reverse=True)
        # The index for the unknown token is 0
        self.unk, uniq_tokens = 0, ['<unk>'] + reserved_tokens
        uniq_tokens += [
            token for token, freq in self.token_freqs
            if freq >= min_freq and token not in uniq_tokens]
        self.idx_to_token, self.token_to_idx = [], dict()
        for token in uniq_tokens:
            self.idx_to_token.append(token)
            self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

def count_corpus(tokens):
    """Count token frequencies."""
    # Here `tokens` is a 1D list or 2D list
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # Flatten a list of token lists into a list of tokens
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)

```

```python
class SNLIDataset(torch.utils.data.Dataset):
    """A customized dataset to load the SNLI dataset."""
    def __init__(self, dataset, num_steps, vocab=None):
        self.num_steps = num_steps
        all_premise_tokens = tokenize(dataset[0])
        all_hypothesis_tokens = tokenize(dataset[1])
        if vocab is None:
            self.vocab = Vocab(all_premise_tokens + all_hypothesis_tokens,
                                   min_freq=5, reserved_tokens=['<pad>'])
        else:
            self.vocab = vocab
        self.premises = self._pad(all_premise_tokens)
        self.hypotheses = self._pad(all_hypothesis_tokens)
        self.labels = torch.tensor(dataset[2])
        print('read ' + str(len(self.premises)) + ' examples')

    def _pad(self, lines):
        return torch.tensor([
            truncate_pad(self.vocab[line], self.num_steps,
                             self.vocab['<pad>']) for line in lines])

    def __getitem__(self, idx):
        return (self.premises[idx], self.hypotheses[idx]), self.labels[idx]

    def __len__(self):
        return len(self.premises)
```

```python
def load_data_snli(batch_size, num_steps=50):
    """Download the SNLI dataset and return data iterators and vocabulary."""
    num_workers = 2
    data_dir = download_extract('SNLI')
    train_data = read_snli(data_dir, True)
    test_data = read_snli(data_dir, False)
    train_set = SNLIDataset(train_data, num_steps)
    test_set = SNLIDataset(test_data, num_steps, train_set.vocab)
    train_iter = torch.utils.data.DataLoader(train_set, batch_size,
                                             shuffle=True,
                                             num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(test_set, batch_size,
                                            shuffle=False,
                                            num_workers=num_workers)
    return train_iter, test_iter, train_set.vocab

def truncate_pad(line, num_steps, padding_token):
    """Truncate or pad sequences."""
    if len(line) > num_steps:
        return line[:num_steps]  # Truncate
    return line + [padding_token] * (num_steps - len(line))
```

```python
train_iter, test_iter, vocab = load_data_snli(128, 50)
len(vocab)
```

    read 549367 examples
    read 9824 examples

    18678

# Model

The model is described in the book. Below we just give the code.

## Attending

We define attention weights
$$
e_{ij} = f(a_i)^T f(b_j)
$$
where $a_i \in R^E$ is the embedding of the $i$'th token from the premise,
$b_j \in R^E$ is the embedding of the $j$'th token from the hypothesis,
and $f: R^E \rightarrow R^H$ is an MLP that maps from the embedding space to another hidden space.

```python
class mlp(nn.Module):
    num_hiddens: int
    flatten: bool

    @nn.compact
    def __call__(self, X, train=True):
        X = nn.Dropout(rate=0.2, deterministic=not train)(X)
        X = nn.Dense(self.num_hiddens)(X)
        X = nn.relu(X)
        if self.flatten:
            X = X.reshape((X.shape[0], -1))  # flatten
        X = nn.Dropout(rate=0.2, deterministic=not train)(X)
        X = nn.Dense(self.num_hiddens)(X)
        X = nn.relu(X)
        if self.flatten:
            X = X.reshape((X.shape[0], -1))  # flatten
        return X
```

```python
class Attend(nn.Module):
    num_hiddens: int

    def setup(self):
        self.f = mlp(self.num_hiddens, False)

    @nn.compact
    def __call__(self, A, B, train=True):
        # Shape of `A`/`B`: (`batch_size`, no. of words in sequence A/B,
        # `embed_size`)
        # Shape of `f_A`/`f_B`: (`batch_size`, no. of words in sequence A/B,
        # `num_hiddens`)
        f_A = self.f(A, train)
        f_B = self.f(B, train)
        # Shape of `e`: (`batch_size`, no. of words in sequence A,
        # no. of words in sequence B)
        e = f_A@(f_B.transpose((0, 2, 1)))
        # Shape of `beta`: (`batch_size`, no. of words in sequence A,
        # `embed_size`), where sequence B is softly aligned with each word
        # (axis 1 of `beta`) in sequence A
        beta = nn.softmax(e, axis=-1)@B
        # Shape of `alpha`: (`batch_size`, no. of words in sequence B,
        # `embed_size`), where sequence A is softly aligned with each word
        # (axis 1 of `alpha`) in sequence B
        alpha = nn.softmax(e.transpose((0, 2, 1)) , axis=-1)@A
        return beta, alpha
```

## Comparing

We concatenate word $i$ in A, $a_i$, with its "soft counterpart" in B, $\beta_i$, and vice versa, and then pass this through another MLP $g$
to get a "comparison vector" for each input location.
$$
\begin{align}
  v_{A,i} &= g([a_i, \beta_i]), \; i=1,\ldots, m \\
  v_{B,j} &= g([b_j, \alpha_j]), \; j=1,\ldots, n
\end{align}
$$

```python
class Compare(nn.Module):
    num_hiddens: int

    def setup(self):
        self.g = mlp(self.num_hiddens, False)

    @nn.compact
    def __call__(self, A, B, beta, alpha, train=True):
        V_A = self.g(jnp.concatenate((A, beta), axis=2), train)
        V_B = self.g(jnp.concatenate((B, alpha), axis=2), train)
        return V_A, V_B
```

## Aggregation

We sum-pool the "comparison vectors" for each input sentence, and then pass the pair of poolings to yet another MLP $h$ to generate the final classification.

$$
\begin{align}
  v_A &= \sum_{i=1}^m v_{A,i} \\
  v_B &= \sum_{j=1}^n v_{B,j} \\
  \hat{y} &= h([v_A, v_B])
\end{align}
$$

```python
class Aggregate(nn.Module):
    num_hiddens: int
    num_outputs: int

    @nn.compact
    def __call__(self, V_A, V_B, train=True):
        # Sum up both sets of comparison vectors
        V_A = V_A.sum(axis=1)
        V_B = V_B.sum(axis=1)
        # Feed the concatenation of both summarization results into an MLP
        Y_hat = nn.Dense(self.num_outputs)(mlp(self.num_hiddens, True)(jnp.concatenate((V_A, V_B), axis=1) ,train))
        return Y_hat
```

## Putting it altogether

We use a pre-trained embedding of size E=100.
The $f$ (attend) function maps from $E=100$  to $H=200$ hiddens.
The $g$ (compare) function maps $2E=200$ to $H=200$.
The $h$ (aggregate) function maps $2H=400$ to 3 outputs.

```python
class DecomposableAttention(nn.Module):
    vocab: Any
    embed_size: int
    num_hiddens: int
    embed_init: Callable

    def setup(self):
        self.embedding = nn.Embed(len(self.vocab), self.embed_size, embedding_init=self.embed_init)
        self.attend = Attend(self.num_hiddens)
        self.compare = Compare(self.num_hiddens)
        # There are 3 possible outputs: entailment, contradiction, and neutral
        self.aggregate = Aggregate(self.num_hiddens, 3)

    def __call__(self, X, train=True):
        premises, hypotheses = X
        A = self.embedding(premises)
        B = self.embedding(hypotheses)
        beta, alpha = self.attend(A, B, train)
        V_A, V_B = self.compare(A, B, beta, alpha, train)
        Y_hat = self.aggregate(V_A, V_B, train)
        return Y_hat
```

```python
class TokenEmbedding:
    """Token Embedding."""
    def __init__(self, embedding_name):
        self.idx_to_token, self.idx_to_vec = self._load_embedding(
            embedding_name)
        self.unknown_idx = 0
        self.token_to_idx = {
            token: idx for idx, token in enumerate(self.idx_to_token)}

    def _load_embedding(self, embedding_name):
        idx_to_token, idx_to_vec = ['<unk>'], []
        data_dir = download_extract(embedding_name)
        # GloVe website: https://nlp.stanford.edu/projects/glove/
        # fastText website: https://fasttext.cc/
        with open(os.path.join(data_dir, 'vec.txt'), 'r') as f:
            for line in f:
                elems = line.rstrip().split(' ')
                token, elems = elems[0], [float(elem) for elem in elems[1:]]
                # Skip header information, such as the top row in fastText
                if len(elems) > 1:
                    idx_to_token.append(token)
                    idx_to_vec.append(elems)
        idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec
        return idx_to_token, jnp.array(idx_to_vec)

    def __getitem__(self, tokens):
        indices = [
            self.token_to_idx.get(token, self.unknown_idx)
            for token in tokens]
        vecs = self.idx_to_vec[jnp.array(indices)]
        return vecs

    def __len__(self):
        return len(self.idx_to_token)

```

```python
def embedding_init(rng, shape, dtype):
    # get pre-trained GloVE embeddings of size 100
#     glove_embedding = TokenEmbedding('glove.6b.100d')
#     embeds = glove_embedding[vocab.idx_to_token]
    return embeds
```

```python
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip'
DATA_HUB['glove.6b.100d'] = (DATA_URL, 'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a')

glove_embedding = TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]

embed_size, num_hiddens = 100, 200
AttentionNetwork = functools.partial(DecomposableAttention, vocab, embed_size, num_hiddens, embedding_init)
```

    Downloading ../data/glove.6B.100d.zip from http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip...

# Training

```python
class Animator:
    """For plotting data in animation."""
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
                 ylim=None, xscale='linear', yscale='linear',
                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
                 figsize=(3.5, 2.5)):
        # Incrementally plot multiple lines
        if legend is None:
            legend = []
        display.set_matplotlib_formats('svg')
        self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes,]
        # Use a lambda function to capture arguments
        self.config_axes = lambda: set_axes(self.axes[
            0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # Add multiple data points into the figure
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        display.display(self.fig)
        display.clear_output(wait=True)

class Timer:
    """Record multiple running times."""
    def __init__(self):
        self.times = []
        self.start()

    def start(self):
        """Start the timer."""
        self.tik = time.time()

    def stop(self):
        """Stop the timer and record the time in a list."""
        self.times.append(time.time() - self.tik)
        return self.times[-1]

    def avg(self):
        """Return the average time."""
        return sum(self.times) / len(self.times)

    def sum(self):
        """Return the sum of time."""
        return sum(self.times)

    def cumsum(self):
        """Return the accumulated time."""
        return np.array(self.times).cumsum().tolist()

class Accumulator:
    """For accumulating sums over `n` variables."""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
```

```python
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
    """Set the axes for matplotlib."""
    axes.set_xlabel(xlabel)
    axes.set_ylabel(ylabel)
    axes.set_xscale(xscale)
    axes.set_yscale(yscale)
    axes.set_xlim(xlim)
    axes.set_ylim(ylim)
    if legend:
        axes.legend(legend)
    axes.grid()
```

```python
def cross_entropy_loss(logits, labels) -> float:
    one_hot = jax.nn.one_hot(labels, num_classes=3)
    loss = optax.softmax_cross_entropy(logits=logits, labels=one_hot)
    return loss.sum()

def compute_metrics(logits, labels):
    """Computes metrics and returns them."""
    loss = cross_entropy_loss(logits, labels)
    accuracy = jnp.sum(jnp.argmax(logits, -1) == labels)
    metrics = {
        'loss': loss,
        'accuracy': accuracy,
    }
    return metrics
```

```python
def get_initial_params(model, rng):
    X = jnp.ones((2, 128, 50), dtype=jnp.int32)
    variables = model.init(jax.random.PRNGKey(0), X, False)
    return variables['params']

def get_train_state(rng, lr) -> train_state.TrainState:
    """Returns a train state."""
    model = AttentionNetwork()
    params = get_initial_params(model, rng)
    tx = optax.adam(lr)
    return train_state.TrainState.create(
        apply_fn=model.apply, params=params, tx=tx)
```

```python
def evaluate_accuracy(state: train_state.TrainState, data_iter):
    """Compute the accuracy for a model on a dataset using a GPU."""
    # No. of correct predictions, no. of predictions
    metric = Accumulator(2)
    for X, y in data_iter:
        X = [jnp.array(x) for x in X]
        y = jnp.array(y)
        logits = state.apply_fn({'params': state.params}, X, False)
        accuracy = jnp.sum(jnp.argmax(logits, -1) == y)
        metric.add(accuracy, y.size)
    return metric[0] / metric[1]
```

```python
@jax.jit
def train_step(state: train_state.TrainState, dropout_rng, features, labels):

    """Trains one step."""
    def loss_fn(params):
        logits = state.apply_fn({'params': params}, features, True, rngs={'dropout': dropout_rng})
        loss = cross_entropy_loss(logits, labels)
        return loss, logits

    grad_fn = jax.value_and_grad(loss_fn, has_aux=True)
    (_, logits), grads = grad_fn(state.params)
    state = state.apply_gradients(grads=grads)
    metrics = compute_metrics(logits, labels)

    return state, metrics
```

```python
def train(train_iter, test_iter, num_epochs, lr):
    key = jax.random.PRNGKey(42)

    state = get_train_state(key, lr)

    timer, num_batches = Timer(), len(train_iter)
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], legend=['train loss', 'train acc', 'test acc'])

    for epoch in range(num_epochs):
        # Store training_loss, training_accuracy, num_examples, num_features
        metric = Accumulator(4)
        for i, (features, labels) in enumerate(train_iter):
            features = [jnp.array(x) for x in features]
            labels = jnp.array(labels)
            timer.start()
            state, metrics = train_step(state, key, features, labels)
            l = metrics['loss']
            acc = metrics['accuracy']
            metric.add(l, acc, labels.shape[0], labels.size)
            timer.stop()
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(
                    epoch + (i + 1) / num_batches,
                    (metric[0] / metric[2], metric[1] / metric[3], None))
        # Calculate Test Accuracy
        test_acc = evaluate_accuracy(state, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
    device = jax.default_backend()
    print(f'loss {metric[0] / metric[2]:.3f}, train acc '
          f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
          f'{str(device)}')
    return state
```

```python
lr, num_epochs = 0.001, 4
state = train(train_iter, test_iter, num_epochs, lr)
```

    loss 0.500, train acc 0.802, test acc 0.811
    6486.3 examples/sec on gpu

# Testing

```python
def predict_snli(state, vocab, premise, hypothesis):
    model = AttentionNetwork()
    premise = jnp.array(vocab[premise])
    hypothesis = jnp.array(vocab[hypothesis])
    features = [premise.reshape((1, -1)), hypothesis.reshape((1, -1))]
    logits = state.apply_fn({'params': state.params}, features, False)
    label = jnp.argmax(logits)
    return 'entailment' if label == 0 else 'contradiction' if label == 1 \
            else 'neutral'
```

```python
predict_snli(state, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
```

    'contradiction'

```python
predict_snli(state, vocab, ['he', 'is', 'very', 'naughty', '.'], ['he', 'is', 'bad', '.'])
```

    'entailment'

```python
predict_snli(state, vocab, ['he', 'is', 'awful', '.'], ['he', 'is', 'bad', '.'])
```

    'entailment'

```python
predict_snli(state, vocab, ['he', 'is', 'handsome', '.'], ['he', 'is', 'bad', '.'])
```

    'contradiction'

## Examples from training set

```python
predict_snli(state, vocab,
             ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'log' '.'],
              ['a', 'person', 'is', 'outdoors', 'on', 'a', 'horse', '.'])
```

    'entailment'

```python
predict_snli(state, vocab,
             ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'log' '.'],
              ['a', 'person', 'is', 'at', 'a', 'diner', 'ordering', 'an', 'omelette', '.'])
```

    'contradiction'

```python
predict_snli(state, vocab,
             ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'log' '.'],
              ['a', 'person', 'is', 'training', 'a', 'horse', 'for', 'a', 'competition', '.'])
```

    'neutral'


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

Promijeni vidljivost ovdje.

## Upravljanje bočnog položaja lunarnog prizemljivača

Ovaj primjer ilustrira razvoj regulatora (promatrača i kontrolera u formi povratne veze stanja) za upravljanje bočnim položajem lunarnog prizemljivača, počevši od jednadžbi sustava.

Sustav je predstavljen na gornjoj slici, njegov vertikalni spust usporen je pomoću vertikalnog potisnika koji proizvodi konstantnu silu $F$. Horizontalno kretanje može se postići laganim naginjanjem prizemljivača za kut $\theta$; naginjanjem se stvara bočna sila koja je približno jednaka $F\theta$. Nagib se postiže generiranjem momenta $T$ pomoću skupa upravljačkih raketa (maksimalni moment = 500 Nm). Kut nagiba mora biti unutar$\pm15$ stupnjeva kako bi se izbjeglo opasno povećanje brzine vertikalnog spusta. Izmjerene veličine su bočni položaj i brzina. Pretpostavlja se da je atmosferski otpor zanemariv, a vrijednosti parametara navedene su u donjoj tablici.

| Parametar |                     Vrijednost |
|-----------|-------------------------------:|
|$m$        |                        1000 kg |
|$J$        |           1000 kg$\text{m}^2$ |
|$F$        |                         1500 N |

Cilj dizajna upravljačkog sustava je postići sljedeće performanse za regulaciju vodoravnog položaja $z$:
1. Maksimalno prekoračenje od 30%.
2. Vrijeme smirivanja (za 5% pojasa tolerancije) manje od 15 sekundi.
3. Kut $\theta$ uvijek unutar svojih granica za željenu maksimalnu bočnu promjenu od 10 metara.
4. Nulta pogreška kao odziv na naredbeni korak.

Jednadžbe sustava su:

\begin{cases}
J\ddot{\theta}=T \\
m\ddot{z}=F\theta
\end{cases}
i definiranjem $\textbf{x}=[x_1,x_2,x_3,x_4]^T=[z,\dot{z},\theta,\dot{\theta}]^T$ kao vektora stanja te $u=T$ kao ulaza, u formi prostora stanja dobivamo:

\begin{cases}
\dot{\textbf{x}}=\underbrace{\begin{bmatrix}0&1&0&0 \\ 0&0&F/m&0 \\ 0&0&0&1 \\ 0&0&0&0\end{bmatrix}}_{A}\textbf{x}+\underbrace{\begin{bmatrix}0\\0\\0\\1/J\end{bmatrix}}_{B}u \\ \\
\textbf{y}=\underbrace{\begin{bmatrix}1&0&0&0 \\ 0&1&0&0\end{bmatrix}}_{C}\textbf{x}.
\end{cases}

### Dizajn kontrolera
Kako bi postigli nultu pogrešku za odziv na referentni korak, sustav se proširuje dodavanjem novog stanja $\dot{x_5}=y_1-y_d$ gdje je $y_1$ izmjereni bočni položaj, a $y_d$ je vrijednost željenog položaja. Stoga je prošireni sustav:

\begin{cases}
\dot{\textbf{x}_a}=\underbrace{\begin{bmatrix}0&1&0&0&0 \\ 0&0&F/m&0&0 \\ 0&0&0&1&0 \\ 0&0&0&0&0 \\ 1&0&0&0&0 \end{bmatrix}}_{A_a}\textbf{x}_a+\underbrace{\begin{bmatrix} 0&0\\0&0\\0&0\\1/J&0\\0&-1 \end{bmatrix}}_{B_a}\underbrace{\begin{bmatrix} u\\y_d \end{bmatrix}}_{u_a} \\ \\
\textbf{y}_a=\underbrace{\begin{bmatrix}1&0&0&0&0\\0&1&0&0&0\\0&0&0&0&1\end{bmatrix}}_{C_a}\textbf{x}_a
\end{cases}

koji se može kontrolirati (upravljiv) s prvim stupcem od $B_a$, tako da je moguće koristiti metodu postavljanja polova. Imajte na umu da je, kako bi se održala osmotrivost sustava, dodan red u matrici $C$ jer je poznato novo stanje $x_5$.

Matrica pojačanja $K_a$ koja zadovoljava sve zadane uvjete je:
$$
K_a=\begin{bmatrix}2225.0&6244.0&13861.0&5275.0&316.0\end{bmatrix}
$$
čime se polovi od $(A_a-B_aK_a)$ pozicioniraju na $-0.28$, $-2.24+2.23i$, $-2.24-2.23i$, $-0.26+0.32i$ i $-0.26-0.32i$.

### Dizajn promatrača
Sustav je osmotriv, a, budući da se mjere tri stanja, moguće je dizajnirati promatrač s reduciranim stanjima (za $\theta$ i $\dot{\theta}$) koji ima strukturu:
$$
\dot{\hat{\textbf{v}}}=(A_{11}+L_aA_{21})\hat{\textbf{v}}+(A_{12}+L_aA_{22}-A_{11}L_a-L_aA_{21}L_a)\textbf{y}_a+(B_1+L_aB_2)u_a,
$$
gdje je
$$
T^{-1}A_aT=\begin{bmatrix}A_{11}&A_{12} \\ A_{21}&A_{22}\end{bmatrix},
\quad T^{-1}B_a=\begin{bmatrix}B_1 \\ B_2\end{bmatrix},
\quad \overline{\textbf{x}_a}=T^{-1}\textbf{x}_a=\begin{bmatrix}V \\ C\end{bmatrix}\textbf{x}_a,
\quad V=\begin{bmatrix}0&0&1&0&0 \\ 0&0&0&1&0\end{bmatrix},
\quad \hat{\textbf{x}_a}=\begin{bmatrix}\hat{\textbf{v}}-L_a\textbf{y}_a \\ \textbf{y}_a\end{bmatrix}.
$$

Odabir svojstvenih vrijednosti promatrača vrši se na način da dinamika pogrešaka konvergira brže od dinamike sustava specificirane zahtjevima. Svojstvene vrijednosti odabrane za $A_{11}+L_aA_{21}$ su $\lambda_i=-10$ rad/s, $i=1,2$ uz
$$ L_a=\begin{bmatrix}0&-\frac{40}{3}&0 \\ 0&-\frac{200}{3}&0\end{bmatrix} $$

### Kako koristiti ovaj interaktivni primjer?
Učinkovitost sustava možete provjeriti s razvijenim regulatorom i izravno modificirati kontroler i/ili promatrač. Simulacija započinje početnom pogreškom promatrača.

```python
#Preparatory Cell

%matplotlib notebook
import control as ctrl
import numpy
import sympy as sym
from IPython.display import display, Markdown
import ipywidgets as widgets
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.patches as patches
import matplotlib.transforms as transforms
import matplotlib.lines as lines

#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(ctrl.StateSpace):
    def __init__(self,*args):
        #call base class init constructor
        ctrl.StateSpace.__init__(self,*args)
    #disable function below in base class
    def _remove_useless_states(self):
        pass
```

```python
# Define matrixes

A = numpy.matrix('0 1 0 0; 0 0 1.5 0; 0 0 0 1; 0 0 0 0')
B = numpy.matrix('0;0;0;0.001')
C = numpy.matrix('1 0 0 0; 0 1 0 0')
Aa = numpy.matrix('0 1 0 0 0; 0 0 1.5 0 0; 0 0 0 1 0; 0 0 0 0 0; 1 0 0 0 0')
Ba = numpy.matrix('0 0;0 0;0 0;0.001 0;0 -1')
Ca = numpy.matrix('1 0 0 0 0; 0 1 0 0 0; 0 0 0 0 1')
Ka1 = numpy.matrix('[2225.0, 6244.0, 13861.0, 5275.0, 316.0') #318.9333 835 2012.5 2000 59.6
Ta = (numpy.matrix('0 0 1 0 0; 0 0 0 1 0; 1 0 0 0 0;0 1 0 0 0; 0 0 0 0 1'))**(-1)
Ar = Ta**(-1)*Aa*Ta
Br = Ta**(-1)*Ba
A11 = Ar[0:2,0:2]
A12 = Ar[0:2,2:5]
A21 = Ar[2:5,0:2]
A22 = Ar[2:5,2:5]
B1 = Br[0:2,:]
B2 = Br[2:5,:]
La1 = numpy.matrix([[0, -4*10/3, 0],[0, -3/8*(-4*10/3)**2, 0]])
X0a = numpy.matrix('0;0;0;0;0;0;0;0;0;0;0.002;0.002;0;0;0;0;0;0.002;0.002;0;0;0;0;0')
# X0a = numpy.matrix('0;0;0;0;0')
# V0 = numpy.matrix('0;0')
```

```python
# Define matrixes widget
Kaw = matrixWidget(1,5)
Law = matrixWidget(2,3)
eig1 = matrixWidget(1,1)
eig2 = matrixWidget(2,1)
eig3 = matrixWidget(2,1)
eig4 = matrixWidget(1,1)
eig5 = matrixWidget(1,1)
eig1o = matrixWidget(1,1)
eig2o = matrixWidget(2,1)

Ydw = widgets.FloatSlider(
                         value=10,
                         min=0,
                         max=10.0,
                         step=0.1,
                         description='$y_d$:',
                         disabled=False,
                         continuous_update=False,
                         orientation='horizontal',
                         readout=True,
                         readout_format='.1f',
                        )

# Init matrix widgets
Kaw.setM(Ka1)
Law.setM(La1)
#[-0.6,-0.5-0.35j,-0.5+0.35j,-0.2-0.6j,-0.2+0.6j]
eig1.setM(numpy.matrix([-0.28]))
eig2.setM(numpy.matrix([[-2.24],[-2.23]]))
eig3.setM(numpy.matrix([[-0.26],[-0.32]]))
eig4.setM(numpy.matrix([-1]))
eig5.setM(numpy.matrix([-1]))
eig1o.setM(numpy.matrix([-10]))
eig2o.setM(numpy.matrix([[-10],[0]]))
```

```python
# Support functions
# Simulation function
def simulation(Aa, Baa, Ca, A11, A12, A21, A22, B1, B2, La, Ka, Ta):
    Aa, Baa, Ca = sym.Matrix(Aa), sym.Matrix(Baa), sym.Matrix(Ca)
    A11, A12, A21, A22 = sym.Matrix(A11), sym.Matrix(A12), sym.Matrix(A21), sym.Matrix(A22)
    B1, B2 = sym.Matrix(B1), sym.Matrix(B2)
    La, Ka = sym.Matrix(La), sym.Matrix(Ka)
    Ta = sym.Matrix(Ta)
    sysS = sss(Aa, Baa, Ca, sym.zeros(3,2))
    sysX = sss(Aa, Baa, sym.eye(5), sym.zeros(5,2))
    sysO1 = sss((A11+La*A21), (B1+La*B2).row_join(A12+La*A22-A11*La-La*A21*La), sym.eye(2), sym.zeros(2,5))
    sysO2 = ctrl.append(sysO1, sysS)
    sysO3 = ctrl.connect(sysO2, [[3, 3], [4, 4], [5, 5]], [1, 2, 6, 7], [1, 2, 3, 4, 5])
    sysO = sss(sysO3.A,
               sysO3.B*sym.eye(2).col_join(sym.eye(2)),
               Ta*(sym.eye(2).row_join(-La)).col_join(sym.zeros(3, 2).row_join(sym.eye(3)))*sysO3.C,
               sym.zeros(5,2))
    sysU = sss(sysO.A, sysO.B, -Ka*sysO.C, sym.zeros(1,2))
    sysT = ctrl.append(sysS, sysX, sysO, sysU)
    sysT1 = ctrl.connect(sysT, [[1, 14], [3, 14], [5, 14], [7, 14]], [2, 4, 6, 8], [i for i in range(1, 15)])
    sys = sss(sysT1.A, sysT1.B*sym.Matrix([1, 1, 1, 1]), sysT1.C, sym.zeros(14, 1))
    return sys

# check functions
def eigen_choice(selc,selo):
    if selc == '0 kompleksnih svojstvenih vrijednosti':
        eig2.children[1].children[0].disabled = True
        eig3.children[1].children[0].disabled = True
        eig3.children[0].children[0].disabled = False
        eig4.children[0].children[0].disabled = False
        eig5.children[0].children[0].disabled = False
        eigc = 0
    if selc == '2 kompleksne svojstvene vrijednosti':
        eig2.children[1].children[0].disabled = False
        eig3.children[1].children[0].disabled = True
        eig3.children[0].children[0].disabled = True
        eig4.children[0].children[0].disabled = False
        eig5.children[0].children[0].disabled = False
        eigc = 2
    if selc == '4 kompleksne svojstvene vrijednosti':
        eig2.children[1].children[0].disabled = False
        eig3.children[1].children[0].disabled = False
        eig3.children[0].children[0].disabled = False
        eig4.children[0].children[0].disabled = True
        eig5.children[0].children[0].disabled = True
        eigc = 4
    if selo == '0 kompleksnih svojstvenih vrijednosti':
        eig1o.children[0].children[0].disabled = False
        eig2o.children[1].children[0].disabled = True
        eigo = 0
    if selo == '2 kompleksne svojstvene vrijednosti':
        eig1o.children[0].children[0].disabled = True
        eig2o.children[1].children[0].disabled = False
        eigo = 2
    return (eigc, eigo)

def method_choice(selm):
    if selm == 'Postavi Ka i La':
        method = 1
        selc.disabled = True
        selo.disabled = True
    if selm == 'Postavi svojstvene vrijednosti':
        method = 2
        selc.disabled = False
        selo.disabled = False
    return method

# Animation functions
def fun_animation(index):
    global Ydw, yout, T
    yd = Ydw.value
    frame = 1

    linez.set_data(T[0:index*frame],yd*yout[0][0:index*frame])
    linezv.set_data(T[0:index*frame],yd*yout[1][0:index*frame])
    lined.set_data(T,[yd for i in range(0,len(T))])
    lineu.set_data(T[0:index*frame],yd*yout[13][0:index*frame])
    linelimu1.set_data(T,[500 for j in range(0,len(T))])
    linelimu2.set_data(T,[-500 for j in range(0,len(T))])
    linethetaest.set_data(T[0:index*frame],yd*yout[10][0:index*frame]*180/numpy.pi)
    linetheta.set_data(T[0:index*frame],yd*yout[5][0:index*frame]*180/numpy.pi)

    rotation_transform.clear().translate(yd*yout[0][index*frame]*numpy.cos(float(yd*yout[6][index*frame])), yd*yout[0][index*frame]*numpy.sin(float(yd*yout[6][index*frame]))).rotate(float(-yd*yout[6][index*frame]))

    return (linez,linezv,lined,lineu,linelimu1,linelimu2,linethetaest,linetheta)

def anim_init():
    linez.set_data([], [])
    linezv.set_data([], [])
    lined.set_data([], [])
    lineu.set_data([], [])
    linelimu1.set_data([], [])
    linelimu2.set_data([], [])
    linethetaest.set_data([], [])
    linetheta.set_data([], [])
    return (linez,linezv,lined,lineu,linelimu1,linelimu2,linethetaest,linetheta)

```

```python
# Main cell
# Data
global yd, T, yout
yd = 10.
T = []
yout = []

# Figures
fig = plt.figure(num='Simulacija sustava za upravljanje bočnog položaja lunarnog prizemljivača')
fig.set_size_inches((9.8, 6))
fig.set_tight_layout(True)

ax0 = fig.add_subplot(221)
ax0.set_title('Lunarni prizemljivač')
ax0.set_xlim(-12,12)
ax0.set_ylim(-4,4)
ax0.grid()
# ax0.axis('off')

ax1 = fig.add_subplot(222)
linez = ax1.plot([],[])[0]
linezv = ax1.plot([],[])[0]
lined = ax1.plot([],[])[0]
ax1.set_title('Bočni položaj i brzina')
ax1.set_xlabel('$t$ [s]')
ax1.set_ylabel('y [m], $\dot y$ [m/s]')
ax1.set_xlim([0,17])
ax1.axvline(x=0,color='black',linewidth=0.8)
ax1.axhline(y=0,color='black',linewidth=0.8)
ax1.grid()
ax1.legend(['Nočni položaj','Bočna brzina','Željena vrijednost'])

ax2 = fig.add_subplot(223)
lineu = ax2.plot([],[])[0]
linelimu1 = ax2.plot([],[],'r')[0]
linelimu2 = ax2.plot([],[],'r')[0]
ax2.set_title('Ulazni moment T')
ax2.set_xlabel('$t$ [s]')
ax2.set_ylabel('$T$ [Nm]')
ax2.set_xlim([0,17])
ax2.axvline(x=0,color='black',linewidth=0.8)
ax2.axhline(y=0,color='black',linewidth=0.8)
ax2.grid()
ax2.legend(['T','Limit'])

ax3 = fig.add_subplot(224)
linethetaest = ax3.plot([],[])[0]
linetheta = ax3.plot([],[])[0]
ax3.set_title(r'$\theta_{est}$ vs $\theta$')
ax3.set_xlabel('$t$ [s]')
ax3.set_ylabel(r'$\theta$ [deg]')
ax3.axvline(x=0,color='black',linewidth=0.8)
ax3.axhline(y=0,color='black',linewidth=0.8)
ax3.set_xlim([0,17])
ax3.grid()

# Patches
rotation_transform = transforms.Affine2D()
circle = patches.Circle((0, 0.6), fill=True, radius=0.5, ec='black', fc='gray', lw=1, zorder=20,
                        transform=rotation_transform + ax0.transData)
rect = patches.Rectangle((-1, -0.4), 2, 0.5, fill=True, ec='black', fc='gray', lw=1, zorder=20,
                         transform=rotation_transform + ax0.transData)
poly = patches.Polygon(numpy.stack(([-0.25, -0.15, 0.15, 0.25], [-0.8, -0.4, -0.4, -0.8])).T,
                       closed=True, fill=True, ec='black', fc='black', lw=1, zorder=20,
                       transform=rotation_transform + ax0.transData)
lleg = patches.Rectangle((-1, -1.2), 0.05, 1, angle=-15, fill=True, ec='black', fc='black', lw=1, zorder=10,
                         transform=rotation_transform + ax0.transData)
rleg = patches.Rectangle((1, -1.2), 0.05, 1, angle=15, fill=True, ec='black', fc='black', lw=1, zorder=10,
                         transform=rotation_transform + ax0.transData)
lfoot = patches.Rectangle((-1.1, -1.2), 0.2, 0.05, fill=True, ec='black', fc='black', lw=1, zorder=20,
                         transform=rotation_transform + ax0.transData)
rfoot = patches.Rectangle((0.9, -1.2), 0.2, 0.05, fill=True, ec='black', fc='black', lw=1, zorder=20,
                         transform=rotation_transform + ax0.transData)
ax0.add_patch(circle)
ax0.add_patch(rect)
ax0.add_patch(poly)
ax0.add_patch(lleg)
ax0.add_patch(rleg)
ax0.add_patch(lfoot)
ax0.add_patch(rfoot)
plt.show()

# Functions
def main_function(Ka,La,Ydw,eig1,eig2,eig3,eig4,eig5,eig1o,eig2o,selm,selc,selo,DW):
    global T, yout, yd, Aa, Ba, A11, A21
    method = method_choice(selm)
    eigc, eigo = eigen_choice(selc,selo)
    yd = Ydw
    ax1.set_ylim([-0.1*yd,yd*1.5])
    ax2.set_ylim([-51*yd,51*yd])
    ax3.set_ylim([-15,15])

    if method == 1: #Setted matrix gain
        sol = numpy.linalg.eig((Aa-Ba[:,0]*Ka))
        print('Svojstvene vrijednosti od Aa su: '+str(round(sol[0][0],3))+', '+str(round(sol[0][1],3))+', '+str(round(sol[0][2],3))+', '+str(round(sol[0][3],3))+' i '+str(round(sol[0][4],3)))
        sol = numpy.linalg.eig(A11+La*A21)
        print('Svojstvene vrijednosti od A11+La*A21 su: '+str(round(sol[0][0],3))+' i '+str(round(sol[0][1],3)))
        sys = simulation(Aa, Ba, Ca, A11, A12, A21, A22, B1, B2, La, Ka, Ta)
        T = numpy.linspace(0, 17, 100)
        T, yout = ctrl.step_response(sys, T, X0a)
    if method == 2: #Setted eigenvalues
        if eigc == 0:
            Ka = ctrl.acker(Aa, Ba[:,0], [eig1[0,0], eig2[0,0], eig3[0,0], eig4[0,0], eig5[0,0]])
            Kaw.setM(Ka)
        if eigc == 2:
            Ka = ctrl.acker(Aa, Ba[:,0], [eig1[0,0], numpy.complex(eig2[0,0],eig2[1,0]), numpy.complex(eig2[0,0],-eig2[1,0]), eig4[0,0], eig5[0,0]])
            Kaw.setM(Ka)
        if eigc == 4:
            Ka = ctrl.acker(Aa, Ba[:,0], [eig1[0,0], numpy.complex(eig2[0,0],eig2[1,0]), numpy.complex(eig2[0,0],-eig2[1,0]), numpy.complex(eig3[0,0],eig3[1,0]), numpy.complex(eig3[0,0],-eig3[1,0])])
            Kaw.setM(Ka)
        if eigo == 0:
            La = numpy.matrix([[0, 2*eig1o[0,0]/3 + 2*eig2o[0,0]/3, 0], [0, -2*eig1o[0,0]*eig2o[0,0]/3, 0]])
            Law.setM(La)
        if eigo == 2:
            La = numpy.matrix([[0, 2*numpy.complex(eig2o[0,0],eig2o[1,0])/3 + 2*numpy.complex(eig2o[0,0],-eig2o[1,0])/3, 0], [0, -2*numpy.complex(eig2o[0,0],eig2o[1,0])*numpy.complex(eig2o[0,0],-eig2o[1,0])/3, 0]])
            Law.setM(La)
        sol = numpy.linalg.eig((Aa-Ba[:,0]*Ka))
        print('Svojstvene vrijednosti od Aa su: '+str(round(sol[0][0],3))+', '+str(round(sol[0][1],3))+', '+str(round(sol[0][2],3))+', '+str(round(sol[0][3],3))+' i '+str(round(sol[0][4],3)))
        sol = numpy.linalg.eig(A11+La*A21)
        print('Svojstvene vrijednosti od A11+La*A21 su: '+str(round(sol[0][0],3))+' i '+str(round(sol[0][1],3)))
        sys = simulation(Aa, Ba, Ca, A11, A12, A21, A22, B1, B2, La, Ka, Ta)
        T = numpy.linspace(0, 17, 100)
        T, yout = ctrl.step_response(sys, T, X0a)

ani = animation.FuncAnimation(fig, fun_animation, init_func=anim_init, frames=100, repeat=True, interval=170, blit=True)

#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= ['Postavi Ka i La', 'Postavi svojstvene vrijednosti'],
    value= 'Postavi Ka i La',
    description='',
    disabled=False
)

# Define the number of complex eigenvalues for the controller
selc = widgets.Dropdown(
    options= ['0 kompleksnih svojstvenih vrijednosti', '2 kompleksne svojstvene vrijednosti', '4 kompleksne svojstvene vrijednosti'],
    value= '4 kompleksne svojstvene vrijednosti',
    description='Aa:',
    disabled=False
)

# Define the number of complex eigenvalues for the observer
selo = widgets.Dropdown(
    options= ['0 kompleksnih svojstvenih vrijednosti', '2 kompleksne svojstvene vrijednosti'],
    value= '0 kompleksnih svojstvenih vrijednosti',
    description='Aobs:',
    disabled=False
)

alltogether = widgets.VBox([
    widgets.HBox([
        selm,
        selc,
        selo
    ]),
    widgets.Label('',border=3),
    widgets.HBox([
        widgets.Label('Ka:',border=3),
        Kaw,
        widgets.Label('',border=3),
        widgets.Label('',border=3),
        widgets.Label('La:',border=3),
        Law
    ]),
    widgets.Label('',border=3),
    widgets.HBox([
        widgets.Label('Svojstvene vrijednosti od Aa:',border=3),
        eig1, eig2, eig3, eig4, eig5,
        widgets.Label('',border=3),
        widgets.Label('',border=3),
        widgets.Label('Svojstvene vrijednosti od Aobs:',border=3),
        eig1o, eig2o
    ]),
    widgets.Label('',border=3),
    widgets.HBox([
        Ydw,
        widgets.Label('',border=3),
        widgets.Label('',border=3),
        widgets.Label('',border=3),
        START
    ])
])

out = widgets.interactive_output(main_function,{'Ka':Kaw, 'La':Law, 'Ydw':Ydw, 'eig1':eig1, 'eig2':eig2,
                                                'eig3':eig3, 'eig4':eig4, 'eig5':eig5,
                                                'eig1o':eig1o, 'eig2o':eig2o,
                                                'selm':selm, 'selc':selc, 'selo':selo, 'DW':DW})
display(out, alltogether)
```

    <IPython.core.display.Javascript object>

    Output()

    VBox(children=(HBox(children=(Dropdown(options=('Postavi Ka i La', 'Postavi svojstvene vrijednosti'), value='P…

```python

```