# Solution Graded Exercise 8: Hopfield Network model of associative memory

first name: ...

last name: ...

sciper: ...

date: ...

*Your teammate*

first name of your teammate: ...

last name of your teammate: ...

sciper of your teammate: ...

Note: You are allowed to discuss the concepts with your class mates. You are not allowed to share code. You have to understand every line of code you write in this notebook. We will ask you questions about your submission during a fraud detection session during the last week of the semester.

** Remember **

If you are asked for plots: The appearance of the plots (labelled axes, ** useful scaling **, etc.) is important!

If you are asked for discussions: Answer in a precise way and try to be concise.

** Submission **

Rename this notebook to Ex8_FirstName_LastName_Sciper.ipynb and upload that single file on moodle before the deadline.

** Link to the exercise **

http://neuronaldynamics-exercises.readthedocs.io/en/latest/exercises/hopfield-network.html

## \* \* \* \* Programming Notes : Copying and slicing lists and numpy arrays \* \* \* \*
We would like to take the opportunity to bring to your attention certain features of Python, that might lead to unwanted behaviour and serious mistakes, if one is not aware of them.
Please check the Python Cheat Sheet file on the moodle (https://moodle.epfl.ch/mod/page/view.php?id=981134 Part 4 of the ipynb file) for some examples of the following notes:

* Assigning a list to a new variable does not create a copy of the list, but creates a variable that points to the list. This means that modifying the second variable, also modifies the original list.
* Assigning a slice of a list to a new variable, creates a copy of the list. Any modification to the sliced list does not modify the original.

Now when it comes to numpy arrays:
* Assigning a numpy array to a new variable does not create a copy of the array, but creates a variable that points to the array. This means that modifying the second variable, also modifies the original array. (same as above)
* Assigning a slice of a numpy array to a new variable creates a variable that points to the corresponding elements of the original array as well! (contrary to what we saw above!) This means that modifying the second variable, also modifies the original array!
* To copy the original array and ensure that it is not modified by any modification of its copied version, the method copy() should be used.

# Exercise 8.1. Getting started

```python
%matplotlib inline
from neurodynex.hopfield_network import network, pattern_tools, plot_tools

pattern_size = 5

# create an instance of the class HopfieldNetwork
hopfield_net = network.HopfieldNetwork(nr_neurons= pattern_size**2)
# instantiate a pattern factory
factory = pattern_tools.PatternFactory(pattern_size, pattern_size)
# create a checkerboard pattern and add it to the pattern list
checkerboard = factory.create_checkerboard()
pattern_list = [checkerboard]

# add random patterns to the list
pattern_list.extend(factory.create_random_pattern_list(nr_patterns=3, on_probability=0.5))
plot_tools.plot_pattern_list(pattern_list)
# how similar are the random patterns and the checkerboard? Check the overlaps
overlap_matrix = pattern_tools.compute_overlap_matrix(pattern_list)
plot_tools.plot_overlap_matrix(overlap_matrix)

# let the hopfield network "learn" the patterns. Note: they are not stored
# explicitly but only network weights are updated !
hopfield_net.store_patterns(pattern_list)

# create a noisy version of a pattern and use that to initialize the network
noisy_init_state = pattern_tools.flip_n(checkerboard, nr_of_flips=4)
hopfield_net.set_state_from_pattern(noisy_init_state)

# from this initial state, let the network dynamics evolve.
states = hopfield_net.run_with_monitoring(nr_steps=4)

# each network state is a vector. reshape it to the same shape used to create the patterns.
states_as_patterns = factory.reshape_patterns(states)
# plot the states of the network
plot_tools.plot_state_sequence_and_overlap(states_as_patterns, pattern_list, reference_idx=0, suptitle="Network dynamics")

```

# 8.3. Exercise: N=4x4 Hopfield-network
## 8.3.1. Question: Storing a single pattern
#### [2 + 2 + 3 points]

```python
# write your code here
```

```python
# Plot the sequence of network states along with the overlap of network state with the checkerboard
```

Now test whether the network can still retrieve the pattern if we increase the number of flipped pixels. What happens at nr_flipped_pixels = 8, what if nr_flipped_pixels > 8 ?

```python

```

## 8.3.2. Question: the weights matrix
#### [1 + 3 + 2 points]

```python
# write your code here: Bullet points 1-5
```

```python
# write your code here: Bullet points 6-11
```

How does this matrix compare to the two previous matrices?

## 8.3.3. Question (optional): Weights Distribution

```python
# You can easily plot a histogram by adding the following two lines to your script.
#It assumes you have stored your network in the variable ‘hopfield_net’.
"""
plt.figure()
plt.hist(hopfield_net.weights.flatten())
"""
```

# 8.4. Exercise: Capacity of an N=100 Hopfield-network
## 8.4.1. Associative memory.
#### [2 points]

A Hopfield network implements so called associative or content-adressable memory. Explain what this means. (max 4 lines)

## 8.4.2. Capacity of the network.
#### [1 points]

Using the value $C_{store}$
 given in the book, how many patterns can you store in a N=10x10 network? Use this number K in the next question:

## 8.4.3. Checkerboard and random patterns.
#### [3 points]

```python
# write your code here
```

Rerun your script a few times. What do you observe?
#### [2 points]

# 8.5. Exercise: Non-random patterns
## 8.5.1. Alphabet.

```python
%matplotlib inline
import matplotlib.pyplot as plt
from neurodynex.hopfield_network import network, pattern_tools, plot_tools
import numpy

# the letters we want to store in the hopfield network
letter_list = ['A', 'B', 'C', 'S', 'X', 'Y', 'Z']

# set a seed to reproduce the same noise in the next run
# numpy.random.seed(123)

abc_dictionary =pattern_tools.load_alphabet()
print("the alphabet is stored in an object of type: {}".format(type(abc_dictionary)))
# access the first element and get it's size (they are all of same size)
pattern_shape = abc_dictionary['A'].shape
print("letters are patterns of size: {}. Create a network of corresponding size".format(pattern_shape))
# create an instance of the class HopfieldNetwork
hopfield_net = network.HopfieldNetwork(nr_neurons= pattern_shape[0]*pattern_shape[1])

# create a list using Pythons List Comprehension syntax:
pattern_list = [abc_dictionary[key] for key in letter_list ]
plot_tools.plot_pattern_list(pattern_list)

# store the patterns
hopfield_net.store_patterns(pattern_list)

# # create a noisy version of a pattern and use that to initialize the network
noisy_init_state = pattern_tools.get_noisy_copy(abc_dictionary['A'], noise_level=0.2)
hopfield_net.set_state_from_pattern(noisy_init_state)

# from this initial state, let the network dynamics evolve.
states = hopfield_net.run_with_monitoring(nr_steps=4)

# each network state is a vector. reshape it to the same shape used to create the patterns.
states_as_patterns = pattern_tools.reshape_patterns(states, pattern_list[0].shape)

# plot the states of the network
plot_tools.plot_state_sequence_and_overlap(
    states_as_patterns, pattern_list, reference_idx=0, suptitle="Network dynamics")
```

## 8.5.2. Add a letter.
#### [2 points]

```python
# write your code here
```

Is the pattern ‘A’ still a fixed point?
Does the overlap between the network state and the reference pattern ‘A’ always decrease? (max 3 lines)
#### [3 points]

## 8.5.3. Capacity.
#### [3 points]

```python
# write your code here
```

Explain the discrepancy between the network capacity C (computed above) and your observation. (max 4 lines)
#### [3 points]

# 8.6. Implementing different types of dynamics
### *** Note:*** this exercise is NOT a bonus and it is graded
## 8.6.1. Exercise:
Implement a Hopfield network with asynchronous dynamics (use the function set_dynamics_sign_async()). Store 5 random patterns (take inspiration from exercise 8.3). Fix a noisy initial state, run the network a few times and observe the performance over the different trials.

Parameters: network size=5x5, number of rnd patterns=5, on probability=0.5, number of flips=8.
#### [5 points]

```python
# write your code here
```

Comment on your results (max 3 lines).
#### [3 points]

```python

```

## 8.6.2. Stochastic neuron
Implement a synchronous and stochastic neuron. Use the function HopfieldNetwork.set_dynamics_to_user_function() in order to pass _get_sigmoid_update_function(), which you should implement.

In the stochastic network the dynamics depend on the so-called inverse temperature $\beta$. The temperature in this system can be seen as noise. The activity $S_i(t+1)$ in the next time-step follows the probability distribution:
\begin{equation}
P\left(S_i(t+1)=1\right) = \frac1{1+\exp\left[-h_i(t)\beta\right]} \qquad \textrm{ where } \qquad h_i(t) = \sum_j w_{ij} S_j(t)
\end{equation}
That is, it is more likely for a neuron $i$ to be set to +1 if its input $h_i$ is higher.

Parameters: network size = 5x5, number of rnd patterns = 5, on probability = 0.5, number of flips = 8, $\beta$=10.

*** Hint 1: *** For inspiration check out the source code of the function _get_sign_update_function()

*** Hint 2: *** In order to implement stochasticity, we suggest to use the python-library numpy.random.

#### [7 points]

```python
# Synchronous stochastic update
def _get_sigmoid_update_function(beta):
    """
    for internal use

    Returns:
        A function implementing a synchronous state update using the sigmoid function (h)
    """
    def upd(state_s0, weights):

        ???

        return s1
    return upd

beta = ???
hopfield_net_stoch = network.HopfieldNetwork(nr_neurons= pattern_size**2)
hopfield_net_stoch.set_dynamics_to_user_function(_get_sigmoid_update_function(beta=beta))
```

## 8.6.3 Finite temperature
Choose one of the stored patterns (let's say number 1) and flip 10 bits. Then plot the overlap between the network state and this pattern, $m^1$, over time for different values of $\beta$, $\beta\in \left[0.1,2,5,100\right]$.

Parameters: network size = 10x10, number of rnd patterns = 5, on probability = 0.5, number of flips = 10, number of trials = 100, number of steps = 9.

*** Hint 1:***  In order to plot $m^1$ over time, run the script several times and compute the mean  and the standard deviation $\sigma$ of the overlap $m^1$ over trials. You can use the function plt.fill and its parameter $\alpha$ (the transparency), in order to fill with color the area between $+\sigma$ and $-\sigma$.

*** Hint 2: *** Use the function pattern_tools.compute_overlap()

*** Hint 3:*** We suggest to implement and test your code with a smaller network size before getting to the 10x10 units network.
#### [7 points]

```python
def compute_overlap_over_time(hopfield_net, pattern_list, idx, init_state, nTrials, nr_steps):
    """ For a given hopfield_net and a pattern_list, initialize it with the init_state and run it
    Compute its overlap with the pattern pattern_list[idx] in time (for nr_steps)
    Repeat the above over many trial (nTrials)
    """
    overlap = np.zeros((nTrials,nr_steps+1))

    for i in range(0,nTrials):

        # learn patterns
        ???

        # run a synch and stoch Hopfield model with the same initial state
        ???

        # let the netwrok dynamics evolve
        ???
        states_as_patterns = ???

        for i_step in range(len(states_as_patterns)):
            overlap[i,i_step] = ???
    return overlap

# Create the patterns and store them

nTrials = ???
nr_steps= ???
betas = ???

overlap_mean = np.zeros((len(betas), nr_steps+1))
overlap_std = np.zeros((len(betas), nr_steps+1))
for j in range(0, len(betas)):

    ???

    overlap_mean[j,:] = ???
    overlap_std[j,:] = ???

    # Plot
    ???

```

Comment on your plot (max 5 lines).
#### [5 points]

write your answer here

## 8.6.4 Asynchronous stochastic neuron
Implement the stochastic neuron of exercise 8.6.2, but with asynchronous update.

*** Hint: *** For help check out the source code of the function _get_async_sign_update_function()
#### [4 points]

```python
def _get_asynch_sigmoid_update_function(beta):
    """
    for internal use

    Returns:
        A function implementing a asynchronous state update using the sigmoid function
    """
    def upd(state_s0, weights):
        random_neuron_idx_list = np.random.permutation(len(state_s0))
        state_s1 = state_s0.copy()

        for i in range(len(random_neuron_idx_list)):

            ???

        return state_s1
    return upd

beta = ???
hopfield_net_asynch_stoch = network.HopfieldNetwork(nr_neurons= pattern_size**2)
hopfield_net_asynch_stoch.set_dynamics_to_user_function(_get_asynch_sigmoid_update_function(beta=beta))
```

# 8.7 Energy

## 8.7.1 Exercise
Set up a deterministic and asynchronous Hopfield network and store 3 random patterns.

The energy can be defined as $E=-\sum_i^N \sum_j^N w_{ij} S_i S_j$.

Implement a function that calculates the energy according to the above definition.

Parameters: network size = 10x10, number of rnd patterns = 3, on probability = 0.5.

#### [4 points]

```python
# Set up your network
???

def energy(weight, state):
    return ???
```

## 8.7.2 Exercise
Compute the energy of one of the stored patterns: $E (\xi^{\mu})$.

Choose one pattern and gradually flip more and more bits. For that you need to implement a function flip_idx, after checking out the function pattern_tools.flip_n(). How does the energy change and why? (max 3 lines)

*** Hint: *** You can get an intuition by plotting the energy of the state as a function of the number of flipped bits.
#### [1 + 6 + 4 points]

```python
nr_flips = np.arange(0,10,1)
energy_flips = np.zeros(len(nr_flips))

for i in range(len(nr_flips)):

    ???

    e_flips[i] = ???

# Plot
???
```

your answer

## 8.7.3 Question.
Compute the energy of the reverse of one of the stored patterns $E(-\xi^1)$. What do you observe, what are the consequences on the dynamics of the system and why? (max 3 lines)
#### [1 + 3 points]

## 8.7.4 Exercise.
1 - Initialize the network with the state $S_0 = sgn(\sum_{\mu}^3 \xi^{\mu})$ and run the dynamics. What is the energy of this state?

Parameters: number steps = 4.

#### [2 points]

```python
# write your code here
```

2 - Consider the ensemble of all states that differ from $S_0$ in exactly one flip. Initialize the network in each of those states and check what is the final state of the dynamical evolution.

*** Hint: *** Compare the final states with $S_0$.
#### [8 points]

```python
# Function to flip one neuron
def flip_idx(template, idx):
    """
    makes a copy of the template pattern and flips the idx-th state.
    Args:
        template:
        nr_of_flips:
    Returns:
        a new pattern
    """
    ???
    return ???

# Flip all neurons one after the other
for i in range(0, pattern_size**2):

    s_0_flipped = flip_idx(s_0, i)

    ???
```

3 - What can you tell about the stability of $S_0$? Briefly comment (max 5 lines).

***Hint:*** For help, refer to the pdf on associative memory on the MOODLE (https://moodle.epfl.ch/pluginfile.php/1091071/mod_resource/content/0/Lecture5/Hertz_Hopfield.pdf).

#### [6 points]

your answer

## 8.7.5 Question.
Does your argumentation above hold in the case of $S_0 = sgn(\sum_{\mu}^2 \xi^{\mu})$? Why? (max 3 lines)
#### [3 points]

## 8.7.6 Exercise.
Use now the stochastic neuron with asynchronous update you implemented in 8.6.4. Initialize the network with the state $S_0 = sgn(\sum_{\mu}^3 \xi^{\mu})$. For $\beta$ varying in $\beta\in \left[1,2,8,10,100\right]$, plot the overlap of the network initialized with $S_0$ with each of the 3 stored patterns as a function of time.

What do you observe? Comment on the stability of the spurious state $S_0$ in each case. (max 4 lines)

Parameters: network size = 10x10, number of rnd patterns = 3, on probability = 0.5, number of steps = 9.

*** Hint 1: *** Make 4 plots (or 1 plot with 4 subplots), one for each value of $\beta$.

*** Hint 2: *** You can use the function pattern_tools.compute_overlap_list (or pattern_tools.compute_overlap).
#### [5 + 4 points]

```python
???

for j in range(0, len(betas)):

    ???

    # Compute

    # Plot
```

your answer


[Table of Contents](http://nbviewer.ipython.org/github/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/table_of_contents.ipynb)

# Designing Nonlinear Kalman Filters

```python
#format the book
%matplotlib inline
from __future__ import division, print_function
from book_format import load_style
load_style()
```

<style>
@import url('http://fonts.googleapis.com/css?family=Source+Code+Pro');
@import url('http://fonts.googleapis.com/css?family=Lora');

//@import url('http://fonts.googleapis.com/css?family=Open+Sans');
//@import url('http://fonts.googleapis.com/css?family=Vollkorn');
//@import url('http://fonts.googleapis.com/css?family=Karla');
//@import url('http://fonts.googleapis.com/css?family=Poppins');
//@import url('http://fonts.googleapis.com/css?family=Arimo');
//@import url('http://fonts.googleapis.com/css?family=Roboto');
//@import url('http://fonts.googleapis.com/css?family=Lato');
//@import url('http://fonts.googleapis.com/css?family=Domine');
//@import url('http://fonts.googleapis.com/css?family=Chivo');
//@import url('http://fonts.googleapis.com/css?family=Cardo');
//@import url('http://fonts.googleapis.com/css?family=Arvo');
//@import url('http://fonts.googleapis.com/css?family=Crimson+Text');
//@import url('http://fonts.googleapis.com/css?family=Ubuntu');
//@import url('http://fonts.googleapis.com/css?family=Fontin');
//@import url('http://fonts.googleapis.com/css?family=Raleway');
//@import url('http://fonts.googleapis.com/css?family=Merriweather');

.CodeMirror pre {
    font-family: 'Source Code Pro', Consolas, monocco, monospace;
}
    div.cell{
        //width: 950px;
        margin-left: 0% !important;
        margin-right: auto;
    }
    div.text_cell_render{
        font-family: 'Lora';
        //font-family: 'Open Sans';
        //font-family: 'Karla',verdana,arial,sans-serif;
        //font-family: 'Roboto',verdana,arial,sans-serif;
        //font-family: 'Lato',verdana,arial,sans-serif;
        //font-family: 'Domine',verdana,arial,sans-serif;
        //font-family: 'Chivo',verdana,arial,sans-serif;
        //font-family: 'Cardo',verdana,arial,sans-serif;
        //font-family: 'Arvo',verdana,arial,sans-serif;
        //font-family: 'Poppins',verdana,arial,sans-serif;
        //font-family: 'Ubuntu',verdana,arial,sans-serif;
        //font-family: 'Fontin',verdana,arial,sans-serif;
        //font-family: 'Raleway',verdana,arial,sans-serif;
        //font-family: 'Merriweather',verdana,arial,sans-serif;
        //font-family: 'Crimson Text', verdana,arial,sans-serif;
        //font-family: verdana,arial,sans-serif;
        //font-family: arial,sans-serif;
        line-height: 125%;
        font-size: 130%;
        text-align: justify;
        text-justify:inter-word;
    }
    div.text_cell code {
        background: transparent;
        color: #000000;
        font-weight: 400;
        font-size: 12pt;
        //font-style: bold;
        font-family:  'Source Code Pro', Consolas, monocco, monospace;
   }
    h1 {
        font-family: 'Open sans',verdana,arial,sans-serif;
	}

    div.input_area {
        background: #F6F6F9;
        border: 1px solid #586e75;
    }

    .text_cell_render h1 {
        font-weight: 200;
        font-size: 30pt;
        line-height: 100%;
        color:#c76c0c;
        margin-bottom: 0.5em;
        margin-top: 1em;
        display: block;
        white-space: wrap;
        text-align: left;
    }
    h2 {
        font-family: 'Open sans',verdana,arial,sans-serif;
        text-align: left;
    }
    .text_cell_render h2 {
        font-weight: 200;
        font-size: 16pt;
        font-style: italic;
        line-height: 100%;
        color:#c76c0c;
        margin-bottom: 0.5em;
        margin-top: 1.5em;
        display: block;
        white-space: wrap;
        text-align: left;
    }
    h3 {
        font-family: 'Open sans',verdana,arial,sans-serif;
    }
    .text_cell_render h3 {
        font-weight: 200;
        font-size: 14pt;
        line-height: 100%;
        color:#d77c0c;
        margin-bottom: 0.5em;
        margin-top: 2em;
        display: block;
        white-space: wrap;
        text-align: left;
    }
    h4 {
        font-family: 'Open sans',verdana,arial,sans-serif;
    }
    .text_cell_render h4 {
        font-weight: 100;
        font-size: 14pt;
        color:#d77c0c;
        margin-bottom: 0.5em;
        margin-top: 0.5em;
        display: block;
        white-space: nowrap;
    }
    h5 {
        font-family: 'Open sans',verdana,arial,sans-serif;
    }

    .text_cell_render h5 {
        font-weight: 200;
        font-style: normal;
        color: #1d3b84;
        font-size: 16pt;
        margin-bottom: 0em;
        margin-top: 0.5em;
        display: block;
        white-space: nowrap;
    }
    div.output_subarea.output_text.output_pyout {
        overflow-x: auto;
        overflow-y: scroll;
        max-height: 50000px;
    }
    div.output_subarea.output_stream.output_stdout.output_text {
        overflow-x: auto;
        overflow-y: scroll;
        max-height: 50000px;
    }
    div.output_wrapper{
        margin-top:0.2em;
        margin-bottom:0.2em;
}

    code{
        font-size: 6pt;

    }
    .rendered_html code{
    background-color: transparent;
    }
    ul{
        margin: 2em;
    }
    ul li{
        padding-left: 0.5em;
        margin-bottom: 0.5em;
        margin-top: 0.5em;
    }
    ul li li{
        padding-left: 0.2em;
        margin-bottom: 0.2em;
        margin-top: 0.2em;
    }
    ol{
        margin: 2em;
    }
    ol li{
        padding-left: 0.5em;
        margin-bottom: 0.5em;
        margin-top: 0.5em;
    }
    ul li{
        padding-left: 0.5em;
        margin-bottom: 0.5em;
        margin-top: 0.2em;
    }
    a:link{
       color:#447adb;
    }
    a:visited{
       color: #1d3b84;
    }
    a:hover{
       color: #1d3b84;
    }
    a:focus{
       color:#447adb;
    }
    a:active{
       font-weight: bold;
       color:#447adb;
    }
    .rendered_html :link {
       text-decoration: underline;
    }
    .rendered_html :hover {
       text-decoration: none;
    }
    .rendered_html :visited {
      text-decoration: none;
    }
    .rendered_html :focus {
      text-decoration: none;
    }
    .rendered_html :active {
      text-decoration: none;
    }
    .warning{
        color: rgb( 240, 20, 20 )
    }
    hr {
      color: #f3f3f3;
      background-color: #f3f3f3;
      height: 1px;
    }
    blockquote{
      display:block;
      background: #fcfcfc;
      border-left: 5px solid #c76c0c;
      font-family: 'Open sans',verdana,arial,sans-serif;
      width:680px;
      padding: 10px 10px 10px 10px;
      text-align:justify;
      text-justify:inter-word;
      }
      blockquote p {
        margin-bottom: 0;
        line-height: 125%;
        font-size: 100%;
      }
</style>

## Introduction

** Author's note: I was initially planning to have a design nonlinear chapter that compares various approaches. This may or may not happen, but for now this chapter has no useful content and I suggest not reading it. **

We see that the Kalman filter reasonably tracks the ball. However, as already explained, this is a silly example; we can predict trajectories in a vacuum with arbitrary precision; using a Kalman filter in this example is a needless complication.

### Kalman Filter with Air Drag

I will dispense with the step 1, step 2, type approach and proceed in a more natural style that you would use in a non-toy engineering problem. We have already developed a Kalman filter that does excellently at tracking a ball in a vacuum, but that does not incorporate the effects of air drag into the model. We know that the process model is implemented with $\textbf{F}$, so we will turn our attention to that immediately.

Notionally, the computation that $\textbf{F}$ computes is

$$x' = Fx$$

With no air drag, we had

$$
\mathbf{F} = \begin{bmatrix}
1 & \Delta t & 0 & 0 & 0 \\
0 & 1 & 0 & 0 & 0 \\
0 & 0 & 1 & \Delta t & \frac{1}{2}{\Delta t}^2 \\
0 & 0 & 0 & 1 & \Delta t \\
0 & 0 & 0 & 0 & 1
\end{bmatrix}
$$

which corresponds to the equations

$$
\begin{aligned}
x &= x + v_x \Delta t \\
v_x &= v_x \\
\\
y &= y + v_y \Delta t + \frac{a_y}{2} {\Delta t}^2 \\
v_y &= v_y + a_y \Delta t \\
a_y &= a_y
\end{aligned}
$$

From the section above we know that our new Euler equations must be

$$
\begin{aligned}
x &= x + v_x \Delta t \\
v_x &= v_x \\
\\
y &= y + v_y \Delta t + \frac{a_y}{2} {\Delta t}^2 \\
v_y &= v_y + a_y \Delta t \\
a_y &= a_y
\end{aligned}
$$

## Realistic 2D Position Sensors

The position sensor in the last example are not very realistic. In general there is no 'raw' sensor that provides (x,y) coordinates. We have GPS, but GPS already uses a Kalman filter to create a filtered output; we should not be able to improve the signal by passing it through another Kalman filter unless we incorporate additional sensors to provide additional information. We will tackle that problem later.

Consider the following set up. In an open field we put two transmitters at a known location, each transmitting a signal that we can detect. We process the signal and determine  how far we are from that signal, with some noise. First, let's look at a visual depiction of that.

```python
import matplotlib.pyplot as plt

circle1=plt.Circle((-4, 0), 5, color='#004080',
                   fill=False, linewidth=20, alpha=.7)
circle2=plt.Circle((4, 0), 5, color='#E24A33',
                   fill=False, linewidth=5, alpha=.7)

fig = plt.gcf()
ax = fig.gca()

plt.axis('equal')
plt.xlim((-10, 10))
plt.ylim((-10, 10))

plt.plot ([-4, 0], [0, 3], c='#004080')
plt.plot ([4, 0], [0, 3], c='#E24A33')
plt.text(-4, -.5, "A", fontsize=16, horizontalalignment='center')
plt.text(4, -.5, "B", fontsize=16, horizontalalignment='center')

ax.add_artist(circle1)
ax.add_artist(circle2)
plt.show()
```

Here I have attempted to show transmitter A, drawn in red, at (-4,0) and a second one B, drawn in blue, at (4,0). The red and blue circles show the range from the transmitters to the robot, with the width illustrating the effect of the $1\sigma$ angular error for each transmitter. Here I have given the blue transmitter more error than the red one. The most probable position for the robot is where the two circles intersect, which I have depicted with the red and blue lines. You will object that we have two intersections, not one, but we will see how we deal with that when we design the measurement function.

This is a very common sensor set up. Aircraft still use this system to navigate, where it is called DME (Distance Measuring Equipment). Today GPS is a much more common navigation system, but I have worked on an aircraft where we integrated sensors like this into our filter along with the GPS, INS, altimeters, etc. We will tackle what is called *multi-sensor fusion* later; for now we will just address this simple configuration.

The first step is to design our state variables. We will assume that the robot is traveling in a straight direction with constant velocity. This is unlikely to be true for a long period of time, but is acceptable for short periods of time. This does not differ from the previous problem - we will want to track the values for the robot's position and velocity. Hence,

$$\mathbf{x} =
\begin{bmatrix}x\\v_x\\y\\v_y\end{bmatrix}$$

The next step is to design the state transition function. This also will be the same as the previous problem, so without further ado,

$$
\mathbf{x}' = \begin{bmatrix}1& \Delta t& 0& 0\\0& 1& 0& 0\\0& 0& 1& \Delta t\\ 0& 0& 0& 1\end{bmatrix}\mathbf{x}$$

The next step is to design the control inputs. We have none, so we set ${\mathbf{B}}=0$.

The next step is to design the measurement function $\mathbf{z} = \mathbf{Hx}$. We can model the measurement using the Pythagorean theorem.

$$
z_a = \sqrt{(x-x_A)^2 + (y-y_A)^2} + v_a\\[1em]
z_b = \sqrt{(x-x_B])^2 + (y-y_B)^2} + v_b
$$

where $v_a$ and $v_b$ are white noise.

We see an immediate problem. The Kalman filter is designed for linear equations, and this is obviously nonlinear. In the next chapters we will look at several ways to handle nonlinear problems in a robust way, but for now we will do something simpler. If we know the approximate position of the robot than we can linearize these equations around that point. I could develop the generalized mathematics for this technique now, but instead let me just present the worked example to give context to that development.

Instead of computing $\mathbf{H}$ we will compute the partial derivative of $\mathbf{H}$ with respect to the robot's position $\mathbf{x}$. You are probably familiar with the concept of partial derivative, but if not, it just means how $\mathbf{H}$ changes with respect to the robot's position. It is computed as the partial derivative of $\mathbf{H}$ as follows:

$$\frac{\partial \mathbf{h}}{\partial \mathbf{x}} =
\begin{bmatrix}
\frac{\partial h_1}{\partial x_1} & \frac{\partial h_1}{\partial x_2} &\dots \\
\frac{\partial h_2}{\partial x_1} & \frac{\partial h_2}{\partial x_2} &\dots \\
\vdots & \vdots
\end{bmatrix}
$$

Let's work the first partial derivative. We want to find

$$\frac{\partial }{\partial x} \sqrt{(x-x_A)^2 + (y-y_A)^2}
$$

Which we compute as

$$
\begin{aligned}
\frac{\partial h_1}{\partial x} &= ((x-x_A)^2 + (y-y_A)^2))^\frac{1}{2} \\
&= \frac{1}{2}\times 2(x-x_a)\times ((x-x_A)^2 + (y-y_A)^2))^{-\frac{1}{2}} \\
&= \frac{x_r - x_A}{\sqrt{(x_r-x_A)^2 + (y_r-y_A)^2}}
\end{aligned}
$$

We continue this computation for the partial derivatives of the two distance equations with respect to $x$, $y$, $dx$ and $dy$, yielding

$$\frac{\partial\mathbf{h}}{\partial\mathbf{x}}=
\begin{bmatrix}
\frac{x_r - x_A}{\sqrt{(x_r-x_A)^2 + (y_r-y_A)^2}} & 0 &
\frac{y_r - y_A}{\sqrt{(x_r-x_A)^2 + (y_r-y_A)^2}} & 0 \\
\frac{x_r - x_B}{\sqrt{(x_r-x_B)^2 + (y_r-y_B)^2}} & 0 &
\frac{y_r - y_B}{\sqrt{(x_r-x_B)^2 + (y_r-y_B)^2}} & 0 \\
\end{bmatrix}
$$

That is pretty painful, and these are very simple equations. Computing the Jacobian can be extremely difficult or even impossible for more complicated systems. However, there is an easy way to get Python to do the work for you by using the SymPy module [1]. SymPy is a Python library for symbolic mathematics. The full scope of its abilities are beyond this book, but it can perform algebra, integrate and differentiate equations, find solutions to differential equations, and much more. We will use it to compute our Jacobian!

First, a simple example. We will import SymPy, initialize its pretty print functionality (which will print equations using LaTeX). We will then declare a symbol for NumPy to use.

```python
import sympy
from sympy import init_printing
init_printing(use_latex='png')

phi, x = sympy.symbols('\phi, x')
phi
```

Notice how we use a latex expression for the symbol `phi`. This is not necessary, but if you do it will render as LaTeX when output. Now let's do some math. What is the derivative of $\sqrt{\phi}$?

```python
sympy.diff('sqrt(phi)')
```

We can factor equations.

```python
sympy.factor('phi**3 -phi**2 + phi - 1')
```

SymPy has a remarkable list of features, and as much as I enjoy exercising its features we cannot cover them all here. Instead, let's compute our Jacobian.

```python
from sympy import symbols, Matrix
phi = symbols('\phi')
phi

x, y, xa, xb, ya, yb, dx, dy = symbols('x y x_a x_b y_a y_b dx dy')

H = Matrix([[sympy.sqrt((x-xa)**2 + (y-ya)**2)],
            [sympy.sqrt((x-xb)**2 + (y-yb)**2)]])

state = Matrix([x, dx, y, dy])
H.jacobian(state)
```

In a nutshell, the entry (0,0) contains the difference between the x coordinate of the robot and transmitter A's x coordinate divided by the distance between the robot and A. (2,0) contains the same, except for the y coordinates of the robot and transmitters. The bottom row contains the same computations, except for transmitter B. The 0 entries account for the velocity components of the state variables; naturally the range does not provide us with velocity.

The values in this matrix change as the robot's position changes, so this is no longer a constant; we will have to recompute it for every time step of the filter.

If you look at this you may realize that this is just a computation of x/dist and y/dist, so we can switch this to a trigonometic form with no loss of generality:

$$\frac{\partial\mathbf{h}}{\partial\mathbf{x}}=
\begin{bmatrix}
-\cos{\theta_A} & 0 & -\sin{\theta_A} & 0 \\
-\cos{\theta_B} & 0 & -\sin{\theta_B} & 0
\end{bmatrix}
$$

However, this raises a huge problem. We are no longer computing $\mathbf{H}$, but $\Delta\mathbf{H}$, the change of $\mathbf{H}$. If we passed this into our Kalman filter without altering the rest of the design the output would be nonsense. Recall, for example, that we multiply $\mathbf{Hx}$ to generate the measurements that would result from the given estimate of $\mathbf{x}$ But now that $\mathbf{H}$ is linearized around our position it contains the *change* in the measurement function.

We are forced, therefore, to use the *change* in $\mathbf{x}$ for our state variables. So we have to go back and redesign our state variables.

>Please note this is a completely normal occurrence in designing Kalman filters. The textbooks present examples like this as *fait accompli*, as if it is trivially obvious that the state variables needed to be velocities, not positions. Perhaps once you do enough of these problems it would be trivially obvious, but at that point why are you reading a textbook? I find myself reading through a presentation multiple times, trying to figure out why they made a choice, finally to realize that it is because of the consequences of something on the next page. My presentation is longer, but it reflects what actually happens when you design a filter. You make what seem reasonable design choices, and as you move forward you discover properties that require you to recast your earlier steps. As a result, I am going to somewhat abandon my **step 1**, **step 2**, etc.,  approach, since so many real problems are not quite that straightforward.

If our state variables contain the velocities of the robot and not the position then how do we track where the robot is? We can't. Kalman filters that are linearized in this fashion use what is called a *nominal trajectory* - i.e. you assume a position and track direction, and then apply the changes in velocity and acceleration to compute the changes in that trajectory. How could it be otherwise? Recall the graphic showing the intersection of the two range circles - there are two areas of intersection. Think of what this would look like if the two transmitters were very close to each other - the intersections would be two very long crescent shapes. This Kalman filter, as designed, has no way of knowing your true position from only distance measurements to the transmitters. Perhaps your mind is already leaping to ways of working around this problem. If so, stay engaged, as later sections and chapters will provide you with these techniques. Presenting the full solution all at once leads to more confusion than insight, in my opinion.

So let's redesign our *state transition function*. We are assuming constant velocity and no acceleration, giving state equations of
$$
\dot{x}' = \dot{x} \\
\ddot{x}' = 0 \\
\dot{y}' = \dot{y} \\
\dot{y}' = 0$$

This gives us the the *state transition function* of

$$
\mathbf{F} = \begin{bmatrix}0 &1 & 0& 0\\0& 0& 0& 0\\0& 0& 0& 1\\ 0& 0& 0& 0\end{bmatrix}$$

A final complication comes from the measurements that we pass in. $\mathbf{Hx}$ is now computing the *change* in the measurement from our nominal position, so the measurement that we pass in needs to be not the range to A and B, but the *change* in range from our measured range to our nominal position.

There is a lot here to take in, so let's work through the code bit by bit. First we will define a function to compute $\frac{\partial\mathbf{h}}{\partial\mathbf{x}}$ for each time step.

```python
from math import sin, cos, atan2

def H_of(pos, pos_A, pos_B):
    """ Given the position of our object at 'pos' in 2D, and two
    transmitters A and B at positions 'pos_A' and 'pos_B', return
    the partial derivative of H
    """

    theta_a = atan2(pos_a[1] - pos[1], pos_a[0] - pos[0])
    theta_b = atan2(pos_b[1] - pos[1], pos_b[0] - pos[0])

    return np.array([[0, -cos(theta_a), 0, -sin(theta_a)],
                     [0, -cos(theta_b), 0, -sin(theta_b)]])
```

Now we need to create our simulated sensor.

```python
from numpy.random import randn

class DMESensor(object):
    def __init__(self, pos_a, pos_b, noise_factor=1.0):
        self.A = pos_a
        self.B = pos_b
        self.noise_factor = noise_factor

    def range_of(self, pos):
        """ returns tuple containing noisy range data to A and B
        given a position 'pos'
        """

        ra = math.sqrt((self.A[0] - pos[0])**2 + (self.A[1] - pos[1])**2)
        rb = math.sqrt((self.B[0] - pos[0])**2 + (self.B[1] - pos[1])**2)

        return (ra + randn()*self.noise_factor,
                rb + randn()*self.noise_factor)
```

Finally, we are ready for the Kalman filter code. I will position the transmitters at x=-100 and 100, both with y=-20. This gives me enough space to get good triangulation from both as the robot moves. I will start the robot at (0,0) and move by (1,1) each time step.

```python
import kf_book.book_plots as bp
from filterpy.kalman import KalmanFilter
import math
import numpy as np

pos_a = (100, -20)
pos_b = (-100, -20)

f1 = KalmanFilter(dim_x=4, dim_z=2)

f1.F = np.array ([[0, 1, 0, 0],
                  [0, 0, 0, 0],
                  [0, 0, 0, 1],
                  [0, 0, 0, 0]], dtype=float)

f1.R *= 1.
f1.Q *= .1

f1.x = np.array([[1, 0, 1, 0]], dtype=float).T
f1.P = np.eye(4) * 5.

# initialize storage and other variables for the run
count = 30
xs, ys = [], []
pxs, pys = [], []

# create the simulated sensor
d = DMESensor(pos_a, pos_b, noise_factor=3.)

# pos will contain our nominal position since the filter does not
# maintain position.
pos = [0, 0]

for i in range(count):
    # move (1,1) each step, so just use i
    pos = [i, i]

    # compute the difference in range between the nominal track
    # and measured ranges
    ra,rb = d.range_of(pos)
    rx,ry = d.range_of((pos[0] + f1.x[0, 0], pos[1] + f1.x[2, 0]))
    z = np.array([[ra - rx], [rb - ry]])

    # compute linearized H for this time step
    f1.H = H_of (pos, pos_a, pos_b)

    # store stuff so we can plot it later
    xs.append(f1.x[0, 0]+i)
    ys.append(f1.x[2, 0]+i)
    pxs.append(pos[0])
    pys.append(pos[1])

    # perform the Kalman filter steps
    f1.predict()
    f1.update(z)

bp.plot_filter(xs, ys)
bp.plot_track(pxs, pys)
plt.legend(loc=2)
plt.show()
```

## Linearizing the Kalman Filter

Now that we have seen an example of linearizing the Kalman filter we are in a position to better understand the math.

We start by assuming some function $\mathbf f$

## Example: A falling Ball

**author's note: ignore this section for now. **

In the **Designing Kalman Filters** chapter I first considered tracking a ball in a vacuum, and then in the atmosphere. The Kalman filter performed very well for vacuum, but diverged from the ball's path in the atmosphere. Let us look at the output; to avoid littering this chapter with code from that chapter I have placed it all in the file `ekf_internal.py'.

```python
import kf_book.ekf_internal as ekf
ekf.plot_ball()
```

We can artificially force the Kalman filter to track the ball by making $Q$ large. That would cause the filter to mistrust its prediction, and scale the kalman gain $K$ to strongly favor the measurments. However, this is not a valid approach. If the Kalman filter is correctly predicting the process we should not 'lie' to the filter by telling it there are process errors that do not exist. We may get away with that for some problems, in some conditions, but in general the Kalman filter's performance will be substandard.

Recall from the **Designing Kalman Filters** chapter that the acceleration is

$$a_x = (0.0039 + \frac{0.0058}{1+\exp{[(v-35)/5]}})*v*v_x \\
a_y = (0.0039 + \frac{0.0058}{1+\exp{[(v-35)/5]}})*v*v_y- g
$$

These equations will be *very* unpleasant to work with while we develop this subject, so for now I will retreat to a simpler one dimensional problem using this simplified equation for acceleration that does not take the nonlinearity of the drag coefficient into account:

$$\begin{aligned}
\ddot{y} &= \frac{0.0034ge^{-y/20000}\dot{y}^2}{2\beta} - g \\
\ddot{x} &= \frac{0.0034ge^{-x/20000}\dot{x}^2}{2\beta}
\end{aligned}$$

Here $\beta$ is the ballistic coefficient, where a high number indicates a low drag.

This is still nonlinear, so we need to linearize this equation at the current state point. If our state is position and velocity, we need an equation for some arbitrarily small change in $\mathbf{x}$, like so:

$$ \begin{bmatrix}\Delta \dot{x} \\ \Delta \ddot{x} \\ \Delta \dot{y} \\ \Delta \ddot{y}\end{bmatrix} =
\large\begin{bmatrix}
\frac{\partial \dot{x}}{\partial x} &
\frac{\partial \dot{x}}{\partial \dot{x}} &
\frac{\partial \dot{x}}{\partial y} &
\frac{\partial \dot{x}}{\partial \dot{y}} \\
\frac{\partial \ddot{x}}{\partial x} &
\frac{\partial \ddot{x}}{\partial \dot{x}}&
\frac{\partial \ddot{x}}{\partial y}&
\frac{\partial \dot{x}}{\partial \dot{y}}\\
\frac{\partial \dot{y}}{\partial x} &
\frac{\partial \dot{y}}{\partial \dot{x}} &
\frac{\partial \dot{y}}{\partial y} &
\frac{\partial \dot{y}}{\partial \dot{y}} \\
\frac{\partial \ddot{y}}{\partial x} &
\frac{\partial \ddot{y}}{\partial \dot{x}}&
\frac{\partial \ddot{y}}{\partial y}&
\frac{\partial \dot{y}}{\partial \dot{y}}
\end{bmatrix}\normalsize
\begin{bmatrix}\Delta x \\ \Delta \dot{x} \\ \Delta \dot{y} \\ \Delta \ddot{y}\end{bmatrix}$$

The equations do not contain both an x and a y, so any partial derivative with both in it must be equal to zero. We also know that $\large\frac{\partial \dot{x}}{\partial x}\normalsize = 0$ and that  $\large\frac{\partial \dot{x}}{\partial \dot{x}}\normalsize = 1$, so our matrix ends up being

$$\mathbf{F} = \begin{bmatrix}0&1&0&0 \\
\frac{0.0034e^{-x/22000}\dot{x}^2g}{44000\beta}&0&0&0
\end{bmatrix}$$

$$\begin{aligned}\ddot{x} &= -\frac{1}{2}C_d\rho A \dot{x}\\
\ddot{y} &= -\frac{1}{2}C_d\rho A \dot{y}-g\end{aligned}$$

```python
from sympy.abc import *
from sympy import *

init_printing(pretty_print=True, use_latex='mathjax')

x1 = (0.0034*g*exp(-x/22000)*((x)**2))/(2*b) - g

x2 = (a*g*exp(-x/c)*(Derivative(x)**2))/(2*b) - g

#pprint(x1)
#pprint(Derivative(x)*Derivative(x,n=2))
#pprint(diff(x2, x))
```

** orphan text
This approach has many issues. First, of course, is the fact that the linearization does not produce an exact answer. More importantly, we are not linearizing the actual path, but our filter's estimation of the path. We linearize the estimation because it is statistically likely to be correct; but of course it is not required to be. So if the filter's output is bad that will cause us to linearize an incorrect estimate, which will almost certainly lead to an even worse estimate. In these cases the filter will quickly diverge. This is where the 'black art' of Kalman filter comes in. We are trying to linearize an estimate, and there is no guarantee that the filter will be stable. A vast amount of the literature on Kalman filters is devoted to this problem. Another issue is that we need to linearize the system using analytic methods. It may be difficult or impossible to find an analytic solution to some problems. In other cases we may be able to find the linearization, but the computation is very expensive. **

## References

[1] http://sympy.org


# Pointers to alternative and more advanced methods

These alternative and more advanced packages mostly try to address the following problem:

Suppose that somebody has written some code in a function `f`, and we would like to access that code *after the fact*. How much can we recover about the function?

```julia
f(x, y) = 2x + y + 1
```

    f (generic function with 1 method)

## Tracing with Symbolics.jl

For certain functions that are "not too complicated", a powerful technique has recently become available using the `Symbolics.jl` package: we **trace** the function with **symbolic variables**. This gives us a symbolic version of the function that we can then manipulate in a similar way to Julia's `Expr`.

```julia
# using Pkg; Pkg.add("Symbolics")
```

        Updating registry at `~/.julia/registries/General`
        Updating git-repo `https://github.com/JuliaRegistries/General.git`
       Resolving package versions...
       Installed ConstructionBase ──────── v1.3.0
       Installed CommonSolve ───────────── v0.2.0
       Installed OpenSpecFun_jll ───────── v0.5.5+0
       Installed TimerOutputs ──────────── v0.5.12
       Installed LabelledArrays ────────── v1.6.3
       Installed DocStringExtensions ───── v0.8.5
       Installed ArrayInterface ────────── v3.1.19
       Installed DynamicPolynomials ────── v0.3.19
       Installed Symbolics ─────────────── v1.4.2
       Installed MultivariatePolynomials ─ v0.3.18
       Installed SortingAlgorithms ─────── v1.0.1
       Installed Setfield ──────────────── v0.7.0
       Installed PDMats ────────────────── v0.11.1
       Installed LogExpFunctions ───────── v0.2.5
       Installed DomainSets ────────────── v0.5.2
       Installed StatsBase ─────────────── v0.33.9
       Installed ChainRulesCore ────────── v0.10.13
       Installed Static ────────────────── v0.3.0
       Installed FillArrays ────────────── v0.12.1
       Installed MutableArithmetics ────── v0.2.20
       Installed DiffRules ─────────────── v1.2.1
       Installed RecursiveArrayTools ───── v2.16.1
       Installed Latexify ──────────────── v0.15.6
       Installed CompositeTypes ────────── v0.1.2
       Installed Reexport ──────────────── v1.1.0
       Installed SpecialFunctions ──────── v1.6.0
       Installed Distributions ─────────── v0.25.11
       Installed SymbolicUtils ─────────── v0.13.1
       Installed TreeViews ─────────────── v0.3.0
       Installed SciMLBase ─────────────── v1.18.2
        Updating `~/Projects/ML_DL/Notebooks/julia-notebooks/JuliaCon2021/Metaprogramming/Project.toml`
      [0c5d862f] + Symbolics v1.4.2
        Updating `~/Projects/ML_DL/Notebooks/julia-notebooks/JuliaCon2021/Metaprogramming/Manifest.toml`
      [1520ce14] + AbstractTrees v0.3.4
      [4fba245c] + ArrayInterface v3.1.19
      [d360d2e6] + ChainRulesCore v0.10.13
      [861a8166] + Combinatorics v1.0.2
      [38540f10] + CommonSolve v0.2.0
      [b152e2b5] + CompositeTypes v0.1.2
      [187b0558] + ConstructionBase v1.3.0
      [b552c78f] + DiffRules v1.2.1
      [31c24e10] + Distributions v0.25.11
      [ffbed154] + DocStringExtensions v0.8.5
      [5b8099bc] + DomainSets v0.5.2
      [7c1d4256] + DynamicPolynomials v0.3.19
      [da5c29d0] + EllipsisNotation v1.1.0
      [1a297f60] + FillArrays v0.12.1
      [59287772] + Formatting v0.4.2
      [615f187c] + IfElse v0.1.0
      [8197267c] + IntervalSets v0.5.3
      [2ee39098] + LabelledArrays v1.6.3
      [23fbe1c1] + Latexify v0.15.6
      [2ab3a3ac] + LogExpFunctions v0.2.5
      [e1d29d7a] + Missings v1.0.0
      [102ac46a] + MultivariatePolynomials v0.3.18
      [d8a4904e] + MutableArithmetics v0.2.20
      [77ba4419] + NaNMath v0.3.5
      [90014a1f] + PDMats v0.11.1
      [1fd47b50] + QuadGK v2.4.1
      [3cdcf5f2] + RecipesBase v1.1.1
      [731186ca] + RecursiveArrayTools v2.16.1
      [189a3867] + Reexport v1.1.0
      [79098fc4] + Rmath v0.7.0
      [0bca4576] + SciMLBase v1.18.2
      [efcf1570] + Setfield v0.7.0
      [a2af1166] + SortingAlgorithms v1.0.1
      [276daf66] + SpecialFunctions v1.6.0
      [aedffcd0] + Static v0.3.0
      [82ae8749] + StatsAPI v1.0.0
      [2913bbd2] + StatsBase v0.33.9
      [4c63d2b9] + StatsFuns v0.9.8
      [d1185830] + SymbolicUtils v0.13.1
      [0c5d862f] + Symbolics v1.4.2
      [a759f4b9] + TimerOutputs v0.5.12
      [a2a6695c] + TreeViews v0.3.0
      [700de1a5] + ZygoteRules v0.2.1
      [efe28fd5] + OpenSpecFun_jll v0.5.5+0
      [f50d1b31] + Rmath_jll v0.3.0+0
      [9fa8497b] + Future
      [4607b0f0] + SuiteSparse
    Precompiling project...
      ✓ CommonSolve
      ✓ StatsAPI
      ✓ TreeViews
      ✓ Reexport
      ✓ IfElse
      ✓ DocStringExtensions
      ✓ Formatting
      ✓ RecipesBase
      ✓ AbstractTrees
      ✓ PDMats
      ✓ CompositeTypes
      ✓ NaNMath
      ✓ ConstructionBase
      ✓ Missings
      ✓ Combinatorics
      ✓ FillArrays
      ✓ SortingAlgorithms
      ✓ LogExpFunctions
      ✓ TimerOutputs
      ✓ ZygoteRules
      ✓ Static
      ✓ ChainRulesCore
      ✓ Rmath_jll
      ✓ OpenSpecFun_jll
      ✓ QuadGK
      ✓ Latexify
      ✓ Setfield
      ✓ Rmath
      ✓ ArrayInterface
      ✓ EllipsisNotation
      ✓ StatsBase
      ✓ IntervalSets
      ✓ LabelledArrays
      ✓ MutableArithmetics
      ✓ SpecialFunctions
      ✓ RecursiveArrayTools
      ✓ DiffRules
      ✓ MultivariatePolynomials
      ✓ DomainSets
      ✓ StatsFuns
      ✓ DynamicPolynomials
      ✓ SciMLBase
      ✓ Distributions
      ✓ SymbolicUtils
      ✓ Symbolics
      45 dependencies successfully precompiled in 31 seconds (82 already precompiled)

```julia
using Symbolics
```

```julia
@variables x, y
```

\begin{equation}
\left[
\begin{array}{c}
x \\
y \\
\end{array}
\right]
\end{equation}

```julia
ex = f(x, y)
```

\begin{equation}
1 + y + 2 x
\end{equation}

We have recovered the mathematical expression corresponding to the function definition!

```julia
typeof(ex)
```

    Num

We can get information about the structure of the expression:

```julia
Symbolics.get_variables(ex)
```

    2-element Vector{Any}:
     y
     x

```julia
Symbolics.arguments(Symbolics.value(ex))
```

    3-element Vector{Any}:
     1
      y
      2x

The internal structure is more complicated, in a way, than a Julia `Expr`:

```julia
dump(ex, maxdepth=2)
```

    Num
      val: SymbolicUtils.Add{Real, Int64, Dict{Any, Number}, Nothing}
        coeff: Int64 1
        dict: Dict{Any, Number}
        sorted_args_cache: Base.RefValue{Any}
        hash: Base.RefValue{UInt64}
        metadata: Nothing nothing

```julia
println(propertynames(Symbolics))
```

    [Symbol("@acrule"), Symbol("@register"), Symbol("@rule"), Symbol("@symbolic_wrap"), Symbol("@syms"), Symbol("@variables"), Symbol("@wrapped"), :ConstrainedEquation, :Difference, :Differential, :Equation, :Integral, :Num, :Rewriters, :RuleSet, :SymbolicUtils, :Symbolics, :Variable, :build_function, :degree, :expand, :expand_derivatives, :getmetadata, :hasmetadata, :polynormalize, :setmetadata, :showraw, :simplify, :substitute, :symbolics_to_sympy, :term]

## Lowered form an Intermediate Representation (IR)

Julia unfortunately does not store the source code of the function definition. It does make accessible a **lowered** version, where the code has been rewritten into a simpler (for the compiler!) form:

```julia
codeinfo = @code_lowered f(3, 4)
```

    CodeInfo(
    1 ─ %1 = 2 * x
    │   %2 = %1 + y + 1
    └──      return %2
    )

```julia
codeinfo.code
```

    3-element Vector{Any}:
     :(2 * _2)
     :(%1 + _3 + 1)
     :(return %2)

## Cassette.jl

Cassette provides the possibility of making so-called **contexts**, within which it is possible to redefine the meaning of given functions using multiple dispatch.

## IRTools.jl

IRTools works with a lower-level Intermediate Representation (IR) of the code, similar to that which is produced by `@code_typed`

```julia
codeinfo = @code_typed f(3, 4)
```

    CodeInfo(
    1 ─ %1 = Base.mul_int(2, x)::Int64
    │   %2 = Base.add_int(%1, y)::Int64
    │   %3 = Base.add_int(%2, 1)::Int64
    └──      return %3
    ) => Int64

```julia
code = first(codeinfo).code
```

    4-element Vector{Any}:
     :(Base.mul_int(2, _2))
     :(Base.add_int(%1, _3))
     :(Base.add_int(%2, 1))
     :(return %3)

Working with a lower-level IR has the advantage that you can extract the IR for a given function directly and then modify it. It has the disadvantage that the code is much lower level.

## MLStyle.jl

[Note: ML here refers to the functional programming language, not to "Machine Learning".]

`MLStyle.jl` provides high-performance pattern matching that makes it easier to match and extract pieces of ASTs.
`MacroTools.jl` is an older package with some similar functionality.

JuliaCon2021/Metaprogramming/7. Alternative and advanced methods.ipynb


# Análisis senoidal en estado estable

## Pasos para analizar circuitos de ca
1. Transformar el circuito al dominio fasorial o de frecuencia.
2. Resolver el problema aplicando técnicas de circuitos (análisis nodal, análisis de malla, superposición, etc.)
3. Transformar el fasor resultante al dominio del tiempo.

## Análisis nodal
La base del análisis nodal es la ley de la corriente de Kirchhoff (LCK). Dado que la LCK es válida en el caso de los fasores, es posible analizar circuitos de ca por medio del análisis nodal.

## Ejemplo 10.1
Halle $i_x$ en el circuito de la figura aplicando el análisis nodal.

### Solución
Primero se convierte el circuito al dominio de frecuencia:

$$\begin{array}{rcll}
20 \cos 4t & \Rightarrow & 20 \angle 0^\circ & \omega = 4 \, \mathrm{rad/s} \\
1 \, \mathrm{H} & \Rightarrow & j \omega L = j 4 \\
0,5 \, \mathrm{F} & \Rightarrow & j \omega L = j 2 \\
0,1 \, \mathrm{F} & \Rightarrow & \displaystyle \frac{1}{j \omega C} = -j 2,5
\end{array}$$

Así, el circuito equivalente en el dominio de frecuencia es como se muestra en la figura.

Al aplicar LCK al nodo 1,
$$ \frac{20 - V_1}{10} = \frac{V_1}{-j2,5} + \frac{V_1 - V_2}{j4} $$

o sea
$$ (1  +j1,5) V_1 + j2,5 \, V_2 = 20 \tag{1} $$

En el nodo 2,
$$ 2 I_x + \frac{V_1 - V_2}{j4} = \frac{V_2}{j2} $$

Pero $I_x = \frac{V_1}{ -j2,5}$. La sustitución de esto da por resultado

$$ \frac{2V_1}{-j2,5} + \frac{V_1 - V_2}{j4} = \frac{V_2}{j2} $$

Por simplificación se obtiene
$$ 11 V_1 + 15 V_2 = 0 \tag{2} $$

Las ecuaciones (1) y (2) pueden ponerse en forma matricial como

$$\left[
\begin{array}{cc}
1+ j1,5 & j2,5 \\
11 & 15
\end{array}
\right]
\left[
\begin{array}{c}
V_1 \\
V_2
\end{array}
\right] =
\left[
\begin{array}{c}
20 \\
0
\end{array}
\right]
$$

Se obtienen los determinantes como

$$ \Delta =
\left|
\begin{array}{cc}
1 + j1,5 & j2,5 \\
11 & 15
\end{array}
\right| = 15 - j5$$

$$ \Delta_1 =
\left|
\begin{array}{cc}
20 & j2,5 \\
0 & 15
\end{array}
\right| = 300$$

$$ \Delta_2 =
\left|
\begin{array}{cc}
1 + j1,5 & 20 \\
11 & 0
\end{array}
\right| = -220 $$

$$ V_1 = \frac{\Delta_1}{\Delta} = \frac{300}{15 - j5} = 18,97 \angle 18,43^\circ \, \mathrm{V} $$

$$ V_2 = \frac{\Delta_2}{\Delta} = \frac{-220}{15 - j5} = 13,91 \angle 198,3^\circ \, \mathrm{V} $$

La corriente $I_x$ está dada por

$$ I_x = \frac{V_1}{-j2,5} = \frac{18,97 \angle 18,43^\circ}{2,5 \angle -90^\circ} = 7,59 \angle 108,4^\circ \, \mathrm{A} $$

Al transformar esto al dominio de tiempo,

$$ i_x = 7,59 \cos (4t + 108,4^\circ ) \, \mathrm{A} $$

## Problema de práctica 10.1
Aplicando el análisis nodal, halle $v_1$ y $v_2$ en el circuito de la figura.

### Solución
Primero se convierte el circuito al dominio de frecuencia:

$$\begin{array}{rcll}
10 \sin 2t & \Rightarrow & 10 \angle 0^\circ & \omega = 2 \, \mathrm{rad/s} \\
2 \, \mathrm{H} & \Rightarrow & j \omega L = j4 \\
0,2 \, \mathrm{F} & \Rightarrow & \displaystyle \frac{1}{j \omega C} = -j2,5
\end{array}$$

Al aplicar LCK al nodo 1

$$ 10 = \frac{V_1}{2} + \frac{V_1 - V_2}{-j2,5} $$

$$ 10 (2)(-j2,5) = \frac{V_1}{2} (2)(-j2,5) + \frac{V_1 - V_2}{-j2,5} (2) (-j2,5) $$

$$ -50j = V_1 (-j2,5) + 2(V_1 - V_2) $$

$$ (2 - 2,5j) V1 - 2 V_2 = -50j \tag{1} $$

Al aplicar LCK al nodo 2

$$ \frac{V_1 - V_2}{-j2,5} = \frac{V_2 - 3V_1}{4} + \frac{V_2}{j4} $$

$$ (V_1 - V_2) (4) (j4) = (V_2 - 3V_1) (-j2,5) (j4) + V_2 (-j2,5) (4) $$

$$ (V1 - V2) 16j = (V_2 - 3V_1) 10 + V_2 (-j10) $$

$$ (30 + j16) V_1 - (10 + j6) V_2 = 0 \tag{2} $$

Las ecuaciones (1) y (2) pueden ponerse en forma matricial como

$$\left[
\begin{array}{cc}
2 - 2,5j & -2 \\
30 + 16j & -10 - 6j
\end{array}
\right]
\left[
\begin{array}{c}
-50j \\
0
\end{array}
\right]$$

Se obtienen los determinantes como

$$ \Delta =
\left|
\begin{array}{cc}
2 - 2,5j & -2 \\
30 + 16j & -10 - 6j
\end{array}
\right|
$$

```python
import numpy as np
```

```python
M = np.array([[2-2.5j   ,   -2       ],
                       [30+16j ,-(10+6j)]  ])
Delta = np.linalg.det(M)
print('Delta = {:.2f}'.format(Delta))
```

    Delta = 25.00+45.00j

$$ \Delta_1 =
\left|
\begin{array}{cc}
-50j & -2 \\
0 & -10 - 6j
\end{array}
\right|
$$

```python
M1 = np.array( [[-50j,   -2   ],
                          [  0  ,-10-6j]] )
Delta1 = np.linalg.det(M1)
print('Delta1 = {:.2f}'.format(Delta1))
```

    Delta1 = -300.00+500.00j

$$ \Delta_2 =
\left|
\begin{array}{cc}
2 - 2.5j & -50j \\
30 + 16j & 0
\end{array}
\right|
$$

```python
M2 = np.array( [[2-2.5j  , -50j],
                          [30+16j,   0]] )
Delta2 = np.linalg.det(M2)
print('Delta2 = {:.2f}'.format(Delta2))
```

    Delta2 = -800.00+1500.00j

Entonces
$$ V_1 = \frac{\Delta_1}{\Delta} \qquad ; \qquad V_2 = \frac{\Delta_2}{\Delta} $$

```python
import cmath, math
```

```python
v1 = Delta1/Delta
v2 = Delta2/Delta

v1_polar = cmath.polar(v1)
v2_polar = cmath.polar(v2)
```

```python
print('v1 = {:.2f} V'.format(v1))
print('v2 = {:.2f} V'.format(v2))
print('v1_polar = (%.2f<%.3f rad)'%v1_polar)
print('v2_polar = (%.2f<%.3f rad)'%v2_polar)
```

    v1 = 5.66+9.81j V
    v2 = 17.92+27.74j V
    v1_polar = (11.33<1.048 rad)
    v2_polar = (33.02<0.997 rad)

```python
# Conversión a grados sexagesimales
r1 = v1_polar[0]   ;   phi1 = v1_polar[1]*(180/math.pi)
r2 = v2_polar[0]   ;   phi2 = v2_polar[1]*(180/math.pi)

print('v1_polar = (%.2f<%.2f°)'%(r1,phi1))
print('v2_polar = (%.2f<%.2f°)'%(r2,phi2))
```

    v1_polar = (11.33<60.02°)
    v2_polar = (33.02<57.13°)

$$\begin{array}{l}
v_1(t) = 11,33 \sin (2t + 60,02^\circ ) \\
v_2(t) = 33,02 \sin(2t + 57,13^\circ )
\end{array}
$$

------------
### Utilizando sympy para resolver el sistema de ecuaciones (opcional)

```python
import sympy as sp
```

```python
V1, V2 = sp.symbols('V1 V2')
```

```python
Eq1 = sp.Eq(10, V1/2 + (V1 - V2)/complex(0,-2.5))
Eq2 = sp.Eq((V1 - V2)/complex(0,-2.5) , (V2 - 3*V1)/4 + V2/complex(0,4))
```

```python
Sol = sp.solve([Eq1,Eq2],[V1,V2])
```

```python
print('v1 = %s V'%Sol[V1])
print('v2 = %s V'%Sol[V2])
```

    v1 = 5.66037735849057 + 9.81132075471698*I V
    v2 = 17.9245283018868 + 27.7358490566038*I V

Mismo resultado obtenido anteriormente.

```python
%reset -s -f
```

## Ejemplo 10.2
Calcule $V_1$ y $V_2$ en el circuito de la figura

### Solución
Los nodos 1 y 2 forman un supernodo, como se indica en la fig. La aplicación de la LCK al supernodo da como resultado

$$ 3 = \frac{V_1}{-j3} + \frac{V_2}{j6} + \frac{V_2}{12} $$

o sea
$$ 36 = j4 V_1 + (1 - j2) V_2 \tag{1} $$

Pero una fuente de tensión está conectada entre los nodos 1 y 2, de modo que

$$ V_1 = V_2 + 10 \angle 45^\circ \tag{2} $$

La sustitución de la ecuación 2 en la ecuación 1 da por resultado

$$ 36 - 40 \angle 135^\circ = (1 + j2) V_2 $$

Entonces
$$ V_2 = 31,41 \angle -87,18^\circ \, \mathrm{V} $$

Con base en la ecuación 2

$$ V_1 = V_2 + 10 \angle 45^\circ = 25,78 \angle -70,48^\circ \, \mathrm{V} $$

## Problema de práctica 10.2
Calcule $V_1$ y $V_2$ en el circuito que aparece en la fig.

### Solución
Los nodos 1 y 2 forman un supernodo. La aplicación de la LCK al supernodo da como resultado

$$ \frac{15 - V_1}{4} = \frac{V_1}{j4} + \frac{V_2}{-j1} + \frac{V_2}{2} $$

o sea
$$ (1 + 1j)V_1 - (4 - 2j)V_2  = 15j \tag{1} $$

Pero una fuerte tensión está conectada entre los nodos 1 y 2, de modo que

$$ V_1 - V_2 = 20 \angle 60^\circ \tag{2} $$

Las ecuaciones (1) y (2) pueden expresaarse en forma matricial como

$$\left[
\begin{array}{cc}
1 + 1j & -4 + 2j \\
1 & -1
\end{array}
\right]
\left[
\begin{array}{c}
15j \\
20 \angle 60^\circ
\end{array}
\right]$$

Resolvemos el sistema lineal usando regal de Cramer

```python
import numpy as np
import cmath, math
```

```python
# Convertimos 20<60° a coordenadas rectangulares
Vs2 =cmath.rect(20,math.pi/3)
print('Vs2 = {:.2f} V'.format(Vs2))
```

    Vs2 = 10.00+17.32j V

$$ \Delta =
\left|
\begin{array}{cc}
1 + 1j & -4 + 2j \\
1 & -1
\end{array}
\right|$$

$$ \Delta_1 =
\left|
\begin{array}{cc}
15j & -4 + 2j \\
10 + 17,32j & -1
\end{array}
\right|$$

$$ \Delta_2 =
\left|
\begin{array}{cc}
1 + 1j & 15j \\
1 & 10 + 17,32j
\end{array}
\right|$$

```python
M = np.array([ [1 + 1j , -4 + 2j],
                        [1        ,    -1     ] ])

M1 = np.array( [[15j  , -4 + 2j],
                         [Vs2 ,     -1     ] ])

M2 = np.array( [[1 + 1j , 15j],
                          [1        , Vs2] ])

Delta   = np.linalg.det(M)
Delta1 = np.linalg.det(M1)
Delta2 = np.linalg.det(M2)
```

    Delta  = 3.00-3.00j
    Delta1 = 74.64+34.28j
    Delta2 = -7.32+12.32j

```python
print('Delta  = {:.2f}'.format(Delta))
print('Delta1 = {:.2f}'.format(Delta1))
print('Delta2 = {:.2f}'.format(Delta2))
```

Entonces
$$ v_1 = \frac{\Delta_1}{\Delta} \qquad ; \qquad v_2 = \frac{\Delta_2}{\Delta} $$

```python
v1 = Delta1/Delta
v2 = Delta2/Delta
v1_pol = cmath.polar(v1)
v2_pol = cmath.polar(v2)
```

```python
print('* En coordenadas rectangulares:')
print('  v1 = {:.2} V'.format(v1))
print('  v2 = {:.2} V'.format(v2))
print('* En coordenadas polares (radianes):')
print('  v1_pol = (%.2f<%.3f rad) V'%v1_pol)
print('  v2_pol = (%.2f<%.3f rad) V'%v2_pol)
print('* En coordenadas polares (grados):')
print('  v1_pol = (%.2f<%.2f°) V'%(v1_pol[0],v1_pol[1]*(180/math.pi)))
print('  v2_pol = (%.2f<%.2f°) V'%(v2_pol[0],v2_pol[1]*(180/math.pi)))
```

    * En coordenadas rectangulares:
      v1 = (6.7+18j) V
      v2 = (-3.3+0.83j) V
    * En coordenadas polares (radianes):
      v1_pol = (19.36<1.216 rad) V
      v2_pol = (3.38<2.892 rad) V
    * En coordenadas polares (grados):
      v1_pol = (19.36<69.67°) V
      v2_pol = (3.38<165.72°) V

----------------
### Utilizando sympy para resolver el sistema de ecuaciones (opcional)

```python
import sympy as sp
```

```python
V1, V2 = sp.symbols('V1 V2')

Eq1 = sp.Eq( (15 - V1)/4 , V1/complex(0,4) - V2/complex(0,1) + V2/2 )
Eq2 = sp.Eq( V1 - V2 , Vs2 )
```

```python
Sol = sp.solve([Eq1,Eq2],[V1,V2])
```

```python
v1_polar = cmath.polar(Sol[V1])
v2_polar = cmath.polar(Sol[V2])
```

```python
print('v1 = (%.3f<%.3f rad) V'%v1_polar)
print('v2 = (%.3f<%.3f rad) V'%v2_polar)
```

    v1 = (19.360<1.216 rad) V
    v2 = (3.378<2.892 rad) V

```python
r1 = v1_polar[0]   ;   phi1 = v1_polar[1]*(180/math.pi)
r2 = v2_polar[0]   ;   phi2 = v2_polar[1]*(180/math.pi)
```

```python
print('v1 = (%.3f<%.2f°)'%(r1,phi1))
print('v2 = (%.3f<%.2f°)'%(r2,phi2))
```

    v1 = (19.360<69.67°)
    v2 = (3.378<165.72°)

```python
%reset -s -f
```

```python
# Esta celda da el estilo al notebook
from IPython.core.display import HTML
css_file = 'styles/aeropython.css'
HTML(open(css_file, "r").read())
```

/* This template is inspired in the one used by Lorena Barba
in the numerical-mooc repository: https://github.com/numerical-mooc/numerical-mooc
We thank her work and hope you also enjoy the look of the notobooks with this style */

<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro|Josefin+Sans:400,700,400italic|Ubuntu+Condensed' rel='stylesheet' type='text/css'>

El estilo se ha aplicado =)

<style>

#notebook_panel { /* main background */
    background: #f7f7f7;
}

div.cell { /* set cell width */
    width: 900px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 950px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.7em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    font-family: 'Source Sans Pro', sans-serif;
    background-color: rgb(256,256,256);
    font-size: 110%;
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'DejaVu Serif', serif;
    line-height: 145%;
    font-size: 125%;
    font-weight: 500;
    width:750px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1, .text_cell_render h2, .text_cell_render h3,
.text_cell_render h4, .text_cell_render h5 {
    font-family: 'Ubuntu Condensed', sans-serif;
}
/*
.text_cell_render h1 {
    font-family: Flux, 'Ubuntu Condensed', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 30pt;
    text-align: center;
    line-height: 100%;
    color: #335082;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
*/
.text_cell_render h1 {
    font-weight: 600;
    font-size: 35pt;
    line-height: 100%;
    color: #000000;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h2 {
    margin-top:16px;
    font-size: 27pt;
    font-weight: 550;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    font-style: regular;
    color: #2c6391;
}

.text_cell_render h3 {
    font-size: 20pt;
    font-weight: 550
    text-align: left;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    font-style: regular;
    color:  #387eb8;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-size: 18pt;
    font-weight: 450
    text-align: left;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    font-style: regular;
    color:  #5797cc;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-size: 18pt;
    font-weight: 550;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
    color:  #b21c0d;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Ubuntu Condensed', sans-serif;
    font-weight: 300;
    font-size: 14pt;
    line-height: 100%;
    color: #252525;
    text-align: right;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: 'Duru Sans', sans-serif;
        font-size: 100%;
}

</style>

```python

```


# Python を使ってネットワーク科学の問題を解決しよう
Chapter 3. 通信シミュレーション

----

明治大学理工学部情報科学科
飯塚 秀明

## 演習前に
この演習を行う前に、下記の注ぎ口を一度実行してください。
演習に必要な各種機能が自動的に導入されます。

```python
!pip install git+https://github.com/kazh98/ybook.git
```

## 可視化ツールdraw
**【課題】まずは、下記の注ぎ口を実行してみましょう。**

```python
%matplotlib inline
import numpy as np
from ybook import draw

draw(np.array([2, 3]), [(0, 1), (1, 2), (0, 2)], np.array([0, 1, 2]))
```

実行に成功すると、``LINK STATUS`` と``FLOW STATUS`` という２つのデータ、加えて１つの図が出てくると思います。
これらは、ネットワークのある状態を表しています。

`draw` 命令をひとつずつ紐解いていきましょう。
  * 図の青い丸は**ノード**(地点)、ノードを結ぶ黒い線分は**リンク**(通信線)、そしてその上下にある矢印はリンクを流れる**通信**を表します。
  * 第１引数`np.array([2, 3])` は、**リンクの許容容量** を左から順に指定しています。この場合、左からリンク$c_0$ は2、リンク$c_1$ は3 の許容容量をもつことを意味しています。
  * 第２引数`[(0, 1), (1, 2), (0, 2)]` は、各ノードを左から順に番号付けたとしての**通信の端点** を指定しています。最初の通信`(0, 1)` は、通信$x_0$ がノード0(左のノード) からノード1(中央のノード) へ流れることを、２番目の通信`(1, 2)` は、通信$x_1$ がノード1(中央のノード) からノード2(右のノード) へ流れることを、最後の通信`(0, 2)` は、通信$x_2$ がノード0(左のノード) からノード2(右のノード) へ流れることを意味しています。
  * 第３引数`np.array([0, 1, 2])` は、**通信の使用帯域幅** を表しています。この場合、通信$x_0$ は帯域幅0 (通信なし)、通信$x_1$ は帯域幅1、通信$x_0$ は帯域幅2 をそれぞれ使用していることを意味します。
  * 通信の使用帯域幅については**FLOW STATUS**、リンクの許容容量とリンクの使用帯域幅については**LINK STATUS** の出力を見ても確認することができます。

リンクを表す線分は、もし何も通信が流れていないときには灰色、何か通信が流れているときには黒色、流れている通信がリンクの許容容量を超える場合には赤色で表示されます。

**【課題】変数`x` の値を変更して、リンクが灰色、黒色、赤色となる場合をそれぞれ確認してみましょう。**

```python
%matplotlib inline
import numpy as np
from ybook import draw

c = np.array([2])
x = np.array([0, 0])

draw(c, [(0, 1), (0, 1)], x)
```

## KKT条件を用いた3変数の問題の解法
前回勉強したKKT 条件を用いて、３変数の問題を解決しましょう。
ここでは、同じく前回取り扱った次のプログラムで構築されるネットワークを考えます。

**【課題】下記の注ぎ口を実行してみましょう。**

```python
%matplotlib inline
import numpy as np
from ybook import draw

c = np.array([2, 3])
x = np.array([0, 0, 0])

draw(c, [(0, 1), (1, 2), (0, 2)], x)
```

このネットワークの転送レート最適化を、KKT条件を用いて解くことを考えましょう。

**【課題】下記の注ぎ口を編集し、ラグランジュ関数$L$ とそれから求められるKKT条件に基づく連立方程式を解くプログラムを完成させ、実行してみましょう。**

```python
%matplotlib inline
import numpy as np
from ybook import draw
from sympy import *
init_printing()

x0, x1, x2, l0, l1 = symbols("x_0 x_1 x_2 l_0 l_1", nonnegative=True)

c = np.array([2, 3])
L = ???
result = solve([
    ???
])[0]
x = np.array([result[x0], result[x1], result[x2]])

draw(c, [(0, 1), (1, 2), (0, 2)], x)
```

## 最適化ソルバーによる解法
いままでは、KKT条件を用いて、

  * 最適化問題 -> 連立方程式

という変換を行い、転送レート最適化問題を解きました。

**最適化ソルバー** を用いると、最適化問題を直接与えて問題を解くことができます。

## 最適化ソルバーの使用例
ここでは、次の最適化問題、
\begin{align}
\text{求む: }
& \log x_0 + \log x_1 + \log x_2 \rightarrow \text{最大}, \\
\text{制約: }
& x_0 + x_2 \le 2, \\
& x_1 + x_2 \le 3, \\
& 0 \le x_0, x_1, x_2.
\end{align}
を例に、最適化ソルバーを用いた解法を確認しましょう。
ソルバーに入力するため、まず次のように最適化問題を変形します。
\begin{align}
\text{求む: }
& -\log x_0 - \log x_1 - \log x_2 \rightarrow \text{最小}, \\
\text{制約: }
& 0 \le 2 - x_0 - x_2, \\
& 0 \le 3 - x_1 - x_2, \\
& 0 \le x_0, x_1, x_2.
\end{align}
ポイントは、

  * 目的関数は、**最小化** することを考える。
  * 制約は、**0以上の形** に統一する。

です。
この形に変形できたら、最適化ソルバー``scipy.optimize.minimize`` を使うことができます。
使い方は下記の通りです。
```python
import numpy as np
from scipy.optimize import Bounds, minimize

minimize(
    lambda x: [最小化したい目的関数],
    [初期点],
    bounds=Bounds([最小値], [最大値]),
    constraints=[
        {'type': 'ineq', 'fun': lambda x: [制約式]},
        ...
    ]).x
```
目的関数はそのまま指定すればよく、制約については右辺の式を`[制約式]` に記述します。
変数の取りうる値に上下限がある場合(ここでは非負制約なので下限0ですね)、`[最小値]`、`[最大値]`にこれを指定できます。
初期点は最適化をする上で重要なパラメータですが、ここでは`np.ones([次元数])` を指定すればよいでしょう。
また、対数関数$y=\log x$ は、Python では`y=np.log(x)` と書くことができます。

**【課題】先の最適化問題を、最適化ソルバーを用いて解いてみましょう。**

```python
import numpy as np
from scipy.optimize import Bounds, minimize

minimize(
    lambda x: ???,
    np.ones(3),
    bounds=Bounds(???, np.inf),
    constraints=[
        {'type': 'ineq', 'fun': lambda x: ???},
        {'type': 'ineq', 'fun': lambda x: ???},
    ]).x
```

## 演習
**【課題】最適化ソルバーと`draw` 関数を用いて、先の問題を解き、結果を表示しましょう。**

```python

```

**【課題】目的関数の各係数を調整して、より公平な割当を実現してみよう。**

```python

```

**【課題】リンクの許容容量を変更したときの、割当の変化を調べてみましょう。**

```python

```

**【課題】ノードが5つあるネットワーク上での帯域幅割当に挑戦してみましょう。**

```python

```


```python
"""Machine-learning applications in computational chemistry."""

__authors__ = "B. G. Peyton"
__credits__ = ["Doaa Altarawy", "Matthew Welborn", "Daniel G. A. Smith"]
__email__   = ["bgpeyton@vt.edu"]

__copyright__ = "(c) 2008-2020, The Psi4Education Developers"
__license__   = "BSD-3-Clause"
__date__      = "2020-07-13"
```

#  Machine-learning applications in computational chemistry
## 0. Environment setup

```python
import psi4
from sklearn.linear_model import LinearRegression
from sklearn.kernel_ridge import KernelRidge
from sklearn.model_selection import GridSearchCV
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
```

## 1. Introduction

Machine-learning (ML) is broadly defined as any algorithm which improves by feeding more data into the algorithm. Familiar concepts such as basic linear regression can be understood as applications of ML, where more data in the regression provides a better fit to a straight line which can then be used to predict new points. This is an example of **supervised** learning, where some input values (x) and output values (y) are known from the beginning. These values compose a **training set**, a defining feature of any ML method. Unsupervised learning is also possible, and is generally used for finding patterns in correlated data; however, we will restrict our discussion to supervised learning techniques.

ML has found applications across the physical sciences including (but not limited to) engineering, physics, biology, and chemistry. By utilizing training sets of molecules and their various properties, chemists can make use of ML algorithms by predicting molecular properties using only the molecular formula or structure without the need for time-consuming experiments. However, generating a robust database of reference values to train the ML model is a tedious task for experimental chemists. Computational chemistry provides a quick and consistent way to produce training data for a reference set of molecules. Once a model is trained with these reference data, properties for new molecules (or geometries of the same molecule) can be predicted at very low cost -- no experiment or electronic structure calculation required.

## 2. Representing a molecule
The ML algorithms we will be using attempt to find a model $f({\bf x})$ which can map a representation vector $\bf x$ onto some target value $y$:
$$f({\bf x}) = {\bf x}^T {\bf w},$$

$$y = f({\bf x}) + \epsilon$$
where $\epsilon$ is the **noise** or **error** of the model. These equations define a simple linear regression model, where we have allowed the input vector (and therefor the weights, or slope) to be of arbitrary dimension.

For our purposes, $\bf x$ is some vector **representation** of our molecule, and $y$ is some property we'd like to predict, such as the energy. We will start by calculating the energy of the water molecule during the symmetric stretching of the O-H bonds, which will be the property $y$ in our model. The following code block will calculate the energy at 31 different bond lengths and save the geometry, charges, and energy at each bond length.

```python
# run across an H2O symmetric bond stretching surface
# 0.5 - 2.0 Angstroms, increments of 0.05, total of 31 geometries
# use H-H bond angle 104.5 degrees
# save geometry, nuclear charges (same across the surface), and scf energy as lists

# set basis
psi4.set_options({
    'basis':'sto-3g'
})
# initialize geometry list
geoms = []
# initialize charge list
qs = []
# initialize energy list
Es = []
# generate bond lengths
rs = []
for i in range(0,31):
    rs.append(0.5 + i*0.05)

# loop over bond lengths
for i in rs:
    # generate a water molecule using a Z-matrix and set the O-H bond lengths
    mol = psi4.geometry("""
    O
    H 1 """ + str(i) + """
    H 1 """ + str(i) + """ 2 104.5
    """)
    # save the geometry
    geoms.append(mol.geometry().to_array())

    # save the charges for all three atoms as a list
    q = []
    for a in range(0,3):
        q.append(mol.fZ(a))
    qs.append(q)

    # calculate and save the energy
    Es.append(psi4.energy('scf'))

```

In the next cell, the change in the energy as the length of the O-H bonds change is plotted as a slice of the potential energy surface.

```python
# plot the original surface
plt.plot(rs,list(Es))
plt.title("H$_2$O Hartree-Fock symmetric O-H stretch")
plt.xlabel('r / $\AA$')
plt.ylabel('E / $E_h$')
plt.show()
```

At a glance, the energy seems to be a simple function of the bond length $r$. One could formulate this as a simple regression problem and solve for a function that maps $r$ into the energy $E$. However, if one wishes to explore more than the symmetric stretch, more information about the molecule is necessary. We want to describe every bond in the molecule.

To do this, we can encode the geometry of the molecule into a matrix using each atom distance $r_{ij}$ - that is, the distance between every pair of atoms. To differentiate the O-H bond length from the H-H "bond", we will use the atomic charge $Z$ of each atom. The resulting matrix is called a **Coulomb matrix** $\bf C$, which takes the form:
$$
\begin{align}
C_{ij} =
\begin{cases}
\frac{Z_iZ_j}{r_{ij}}&\text{for $i \neq j$}\\
0.5Z_i^{2.4}&\text{for $i=j$}\\
\end{cases}
\end{align}
$$

```python
# ==> Build coulomb matrices according to the above equation <==

def coulomb(geom,q):
    '''
    Generates the coulomb matrix given a geometry and list of atomic charges

    Parameters:
    geom: list of lists of atomic coordinates, [[xi,yi,zi],[xj,yj,zj],...]
    q: list of atomic charges, [qi,qj,...]

    Returns:
    cm: np.ndarray, coulomb matrix
    '''
    natom = len(q)
    cm = np.zeros((natom,natom)) # ==> Fill this matrix! <==

    # ==> YOUR CODE HERE <==#

    return cm

# Generate the coulomb matrix for all geometries and store them in a list
couls = []
for i in range(0,len(geoms)):
    coul = coulomb(geoms[i],qs[i])
    couls.append(coul)
```

```python
print(couls[0])
```

The above cell should evaluate to:
```
[[73.51669472  8.46683537  8.46683537]
 [ 8.46683537  0.5         0.66926039]
 [ 8.46683537  0.66926039  0.5       ]]
 ```

**STUDENT QUESTIONS:**

A. `mol.geometry()` always returns the geometry in cartesian units Bohr. How would the Coulomb matrix compare if you used different units for your bond distances?

B. Why do we not scale the diagonal elements by the bond distance?

**Answers:**

A.

B.

## 2. Training a ML model for molecular energy
Now we may take our Coulomb matrix elements and use them as features for a ML model. We will flatten $\bf C$ into a 1D array $\bf c$ (using the `numpy` function `ndarray.flatten()`), then find the optimum weights $\bf w$ which map the features onto the energy:

$$y \approx f({\bf x}) = {\bf c}^T {\bf w}.$$

Finding the weights is done by calling `fit(x,y)` on a `LinearRegression` object, which was imported from the `scikit-learn` ML package at the top of this notebook. The resulting model can then be used to `predict` values for any new representations.

**STUDENT QUESTION:** If our feature vector $\bf c$ is a vector containing all of the elements of $\bf C$, how many elements will there be in our weight vector $\bf w$?

**Answer:**

We will now select some points to train the ML model, and try predicting the energy for every other point. Since we know what the curve will look like, we can evaluate the accuracy of our model. Execute the following code blocks to see how the model performs.

```python
# set up a training set (X,y) - 4 evenly-spaced points on the PES
trainers = [0,7,15,23]
X_train = []
y_train = []
for t in trainers:
    X_train.append(couls[t].flatten())
    y_train.append(Es[t])

# prediction set will be everything else
testers = []
for i in range(0,31):
    testers.append(i)
# delete the test set!
for t in sorted(trainers,reverse=True):
    del testers[t]
# set up test set (X,y)
X_test = []
y_test = []
for t in testers:
    X_test.append(couls[t].flatten())
    y_test.append(Es[t])

# use Linear Regression from scikit-learn to train and predict
reg = LinearRegression().fit(X_train,y_train)
y_pred = reg.predict(X_test)
```

```python
# plot the true and ML surfaces
plt.plot(rs,list(Es),label='Truth')
plt.plot(np.asarray(rs)[testers],y_pred,label='ML')
plt.legend()
plt.xlabel('r / $\AA$')
plt.ylabel('E / $E_h$')
plt.show()
```

This model did not perform admirably. However, `scikit-learn` provides many pre-packaged ML algorithms which are more appropriate for this job. Let's try again, but instead of linear regression we use the popular **Kernel Ridge Regression** model. This model solves a modified linear regression equation:

$$
y \approx f({\bf x}) = \sum_i w_i k({\bf x},{\bf x'}_i)
$$

where $i$ runs over the training set (${\bf x'}$). The regression coefficients $\bf w$ are defined by

$$
{\bf w} \triangleq ({\bf K}({\bf x'},{\bf x'}) + \alpha{\bf I})^{-1}{\bf y}
$$

where $\bf I$ is the identity matrix. This method differs from linear regression in two key ways. The first is the inclusion of a dampening parameter (called a **hyperparameter**), $\alpha$, which is optimized during training and protects against over-training. The next difference is the use of a **kernel**, vector $k$ or matrix $\bf{K}$, which measures the **similarity** of two inputs $\bf x$ and ${\bf x'}$ rather than using the inputs themselves. There are many possible kernel definitions, but we will choose the popular radial basis function kernel:

$$
{\bf K}({\bf x},{\bf x'}) = exp(-\gamma||{\bf x} - {\bf x'}||^2)
$$

where $\gamma$ is included as an additional optimizable hyperparameter determined at training time to determine the "width" of the kernel. The hyperparameters are optimized by simply trying many different values, and choosing the combination which gives the best results on a subset of the training data in a process called cross-validation.
(NOTE: the algorithm must never be shown the test data until final evaluation!)

`scikit-learn` can build this model given the training set and a few starting parameters with just a few lines of code, shown below.

```python
# building the kernel ridge regression model
krr = KernelRidge(kernel='rbf')
# define hyperparameter grid: 12 points from 1E-12 to 1E12 for each
parameters = {'alpha':np.logspace(-12,12,num=12),
              'gamma':np.logspace(-12,12,num=12)}
# build the model - 4-fold grid search CV w/ NMSE scoring function
krr_regressor = GridSearchCV(krr,parameters,cv=4,scoring='neg_mean_squared_error')
# train the model on the training set
krr_regressor.fit(X_train,y_train)
# predict the test set
y_pred = krr_regressor.predict(X_test)
```

We will plot our results with `matplotlib` again, this time using an inset plot to zoom-in.

```python
plt.plot(rs,list(Es),label='Truth')
plt.plot(np.asarray(rs)[testers],y_pred,label='ML')

ax = plt.gca()
axin = ax.inset_axes([0.25,0.4,0.4,0.4])
axin.plot(rs,list(Es))
axin.plot(np.asarray(rs)[testers],y_pred)
axin.set_xlim([0.9,1.1])
axin.set_ylim([-74.965,-74.94])
axin.set_alpha(0)
ax.indicate_inset_zoom(axin,label='_nolegend_')

plt.legend()
plt.xlabel('r / $\AA$')
plt.ylabel('E / $E_h$')
plt.show()
```

As you can see, the inclusion of multiple optimizable parameters and the use of a kernel vastly improves the flexibility of our model. Now that we have a model that seems to work, we will try a more complex system.

## 4. Predicting a hypersurface
Of course, we do not need to limit ourselves to the symmetric stretch. The asymmetric stretch of water, a function of both O-H bond distances, can also be explored. Complete the following code to compute the hypersurface - you should try all values of `rs` for each bond, so reduce the number of rs to 16 instead of 31 (still ranging from 0.5 to 2 Angstroms). Running the calculations may take a few minutes.

```python
# ==> Generate the slice of the hypersurface where both bonds stretch independently. <==

psi4.set_options({
    'basis':'sto-3g'
})
geoms = []
qs = []
Es = np.zeros((len(rs),len(rs))) # store the energies in a matrix! we can flatten it later
rs = [] # 16 r values!

# ==> YOUR CODE HERE <==
```

Since we now want to plot the energy against two variables ($r_1$ and $r_2$), we can use a contour plot. The color will show us the deviation from the minimum at each point, $(r_1,r_2)$.

```python
fig,ax=plt.subplots(1,1)
cp = ax.contourf(rs,rs,(Es - np.min(Es)))
cbar = fig.colorbar(cp,label="(E - min) / $E_h$")
ax.set_title('H$_2$O hypersurface')
ax.set_xlabel('r$_1$ / $\AA$')
ax.set_ylabel('r$_2$ / $\AA$')
plt.show()
```

If the calculations have run correctly, the above cell should produce the following graph:

We can see that there is a broad enery well, meaning that many different bond distances produce energies near the minimum. Reproducing this well is key for a useful ML model of the system.

In the following cells, choose a number of training points and attempt to reproduce the above hypersurface. See if you can find the lowest number of training points to still get a good fit!

NOTE: you can choose the training points however you like. You may wish to try evenly-spaced points, points near the minima or maxima of the hypersurface, or even random points! (Hint: for the latter, try `np.random.randint`)

```python
# ==> Generate the coulomb matrix for all geometries <==
couls = []

# ==> YOUR CODE HERE <==
```

```python
# ==> Make your training set - X and y values.
# Remember: you must flatten each individual Coulomb matrix, but also the whole energy array
trainers = []
X_train = []
y_train = []

# ==> YOUR CODE HERE <==
```

```python
# ==> Finally, use Scikit-Learn to build a model to predict the rest of the surface <==
krr = KernelRidge(kernel='rbf')
parameters = {'alpha':np.logspace(-12,12,num=12),
              'gamma':np.logspace(-12,12,num=12)}
krr_regressor = GridSearchCV(krr,parameters,cv=4,scoring='neg_mean_squared_error')

# ==> COMPLETE THE FUNCTION CALL <==
krr_regressor.fit()
```

The following cell will predict the entire surface, including the training set, to make plotting easier. We will also look at the average error across the surface. How does it compare?

```python
y_pred = krr_regressor.predict([c.flatten() for c in couls])
fig,ax=plt.subplots(1,1)
cp = ax.contourf(rs,rs,(y_pred.reshape(16,16) - np.min(Es)))
cbar = fig.colorbar(cp,label="($E_{ML}$ - min) / $E_h$")
ax.set_title('H$_2$O hypersurface')
ax.set_xlabel('r$_1$ / $\AA$')
ax.set_ylabel('r$_2$ / $\AA$')
plt.show()
print("Mean prediction error across the surface: {} Hartrees".format(np.mean(np.abs(y_pred - Es.flatten()))))
```

**STUDENT QUESTION:** The shape of the well may be reproduced, but "chemical accuracy" in computational chemistry is generally 1kcal/mol. Does your model reach this (on average)?

**Answer:**

## 5. Atomization energies and the ANI-1 dataset
We can also apply the same logic to many different molecules. A variant of the Coulomb matrix can be generated for molecules from the ANI-1 dataset, a collection of over 57 thousand organic molecules with up to 8 heave (non-Hydrogen) atoms and their properties. Large datasets such as these can be used to train models to predict properties for new molecules without needing to perform an expensive calculation.

Below, a subset of the ANI-1 dataset is loaded into a Pandas dataframe containing the energy from an electronic structure calculation, a molecule object, the Coulomb matrix (`feature`), and the atomization energy (`AE`) for a 1000-molecule training set and 100-molecule test set. The Pandas dataframe acts as a powerful dictionary for storing large datasets.

```python
data_train = pd.read_pickle('data/ani1_train.pd')
data_test = pd.read_pickle('data/ani1_test.pd')

data_train
```

Now, all of our data is collected into just two objects which can be indexed by name, similarly to a Python `dict` object. Another advantage of this dataframe is that every entry can be visualized within a Jupyter notebook. Feel free to explore the different molecules present.

```python
data_test["molecule"][1]
```

In the cells below, train a new kernel ridge regression model. Then, evaluate the error for predicting the atomization energies of the test and training sets. The error on the training sets gives us a "lower bound" to what we hope the error in the test set to be. If the training set performs poorly, we know our model must be revised.

```python
# ==> Pandas dictionaries allow us to store features and data in one place. <==
# ==> Select the appropriate dictionary keys to build the training and test sets. <==
X_train = np.vstack(data_train[])
y_train = data_train[].values
X_test = np.vstack(data_test[])
y_test = data_test[].values
```

```python
# ==> train the model and predict for both the test and training set <==
# ==> this will take a moment <==
krr = KernelRidge(kernel='rbf')
parameters = {'alpha':np.logspace(-12,12,num=12),
              'gamma':np.logspace(-12,12,num=12)}
krr_regressor = GridSearchCV(krr,parameters,cv=4,scoring='neg_mean_squared_error')

# ==> COMPLETE THE FUNCTION CALLS <==
krr_regressor.fit()
y_pred_test = krr_regressor.predict()
y_pred_train = krr_regressor.predict()
```

Finally, we will plot the errors in a violin plot. The y-axis will tell us the error, while the width of the violin tells us the density (frequency) of points with that error. In other words, the widest point tells us the error where most of the data points fall. The minimum, maxiumum, and mean errors are marked by horizontal lines.

```python
test_err = np.abs(y_test - y_pred_test)
train_err = np.abs(y_train - y_pred_train)

fig,ax = plt.subplots(1,1)
ax.violinplot([test_err,train_err],showmeans=True)
ax.set_xticks([1,2])
ax.set_xticklabels(["Test Set","Training Set"])
ax.set_ylabel('ML error / $E_h$')
print("Mean prediction error across the test set: {} Hartrees".format(np.mean(test_err)))
```

**STUDENT QUESTION:**
Are these error significant or negligible? Should the model be revised? Discuss your reasoning.

**Answer:**

```python

```