__Method__

Soccer is played by 11 players on each side, therefore, the best method for capturing the pattern of teams from optical tracking data seems to use players’ individual attributes. Given the data for every player, we initialize the players to some canonical order and the order remains fixed throughout the match. For $N = 22$ players on the pitch, the data representation at a frame t can be set as $X_t = [x1, y1, x2, y2, x3, y3, ..., x22, y22]^T$. Directly using players’ individual jersey numbers for the analysis seems to be a good approach. However, it lacks important contextual concepts such as when a substitution occurs, players receive medical treatment outside of the pitch, a player is sent away, or when analyzing different teams (i.e., different jersey numbers or identities). To deal with all these problems and players’ positional interchanges, a role based analysis is conducted to make dynamic representation possible while initializing the players into the order that is based on their roles. This approach allows each player to be assigned to different roles, only role per frame while changing their positions throughout the match. After grouping players to particular roles, we use players’ $(x, y)$ coordinates and speed attributes to generate features, such as average coordinates and the fastest player attributes in the group. Soccer has its own well-established game strategies such as the tendency of teams to keep their formation structure when opposition team owns the ball. To carry out a role assignment, a cumulative average position of a team is calculated at the frames when the rival team has the ball. A cumulative average position is calculated for the window of the previous 15 minutes and updated at each minute. For each frame, player’s average $(x, y)$ positions is scaled  based on the dispersion of the players’ distribution.

__How we assign role to each player?__
1. Define which player's in the game.
2. Generate players' average positions based on their position in last 15 minutes.
3. Each minute update their average position.
4. Scale average positions to the range $\left [\delta^{1} + \frac{1}{N} \sum_{i}^{N} \left ( p_i-\bar{p} \right )^{2}, \delta^{2} - \frac{1}{N} \sum_{i}^{N} \left ( p_i-\bar{p} \right )^{2} \right ]$ based on their standard deviation of averge position data $\eqref{eq:pythagoras}$.
5. Seperate pitch into different role-based segments.
6. Assign role to player based on given pitch segments.

$$
\begin{equation}
p^{'}_{k} = \delta_{2}- \frac{\left( \left( \delta_{2}-\frac{1}{N} \sum_{i}^{N} \left ( p_i-\bar{p} \right )^{2} \right) - \left(\delta_{1}+\frac{1}{N} \sum_{i}^{N} \left ( p_i-\bar{p} \right )^{2}  \right)\right)\left(\alpha-p_{k}\right) }{\alpha  - \beta}
\label{eq:pythagoras}
\tag{1}
\end{equation}
$$ where $\delta^{1}$ are $\delta^{2}$ respectively start and end points of a segment for a given axis, $\alpha = \underset{1\leq i \leq N }{\max p_i}$, and $\beta = \underset{1\leq i \leq N }{\min p_i}$.
7. Calculate the features for each player

***note:*** *Above image depicts the role assigment to players after sclaing their avg pos for the span of last 15 min.*

Detailed info on features and their calculations can be found on classes.FeatureBuilder. Target data is derived from a player who owns the ball. On raw data, we have no information on ball location or its states. We are only able to determine the ball location if a specific player who has the ball at the moment. Then we set player's location as a ball location. Therefore, we have no information on a significant amount of data frames when the ball passes from one player to another and when game stops. We only going to use data frames when some player has the ball.

**Raw data-dict structure for a single frame** (structure of per_sec_data_id.json):

{

   **'distance'**: 3.1426183970482264,

   **'half'**: 1,

   **'hasballJerseyNumber'**: 6,

   **'hasballTeamId'**: 101,

   **'id'**: 280999,

   **'jerseyNumber'**: 6,

   **'matchId'**: 60568,

   **'minute'**: 19,

   **'second'**: 19,

   **'speed'**: 3.139151535835957,

   **'teamId'**: 101,

   **'xpos'**: 34.366599999999686,

   **'ypos'**: 23.508333333333262

 }

```python
import json
import pickle
import pandas as pd
import numpy as np
import csv
import math
from scipy.ndimage.interpolation import shift
import operator
import scipy as sc

# our common models and utility functions
from classes.DataModel import *
from classes.PlayersRoleSet import *
from classes.FeatureBuilder import *
from classes.Utils import load_json_content

pd.options.display.max_columns = None
pd.set_option('compute.use_bottleneck', True)
pd.set_option('compute.use_numexpr', True)
```

### Use different segment division and roles assignment

```python
# first we should decide what kind of segment division and role assignment we're going to employ.
# Then based on role group, we will calculate all features combinations in next steps.

# BELOW IS AN TO EXAMPLE TO HOW TO OVERRIDE FUNCTIONS OF PLAYERSROLESET: YOU CAN TRY YOUR OWN STUFF
# Here you define segment coordinates and override find_segments functions below to seperate them.

# new_role_segment_coords = {'LB':[-1,  40, -1, 19],    # Left Back
#                       'CB':[-1,  35,  19, 49],   # Center Back
#                       'RB':[-1,  40,  49, 69],   # Right Back
#                       'LW':[ 40, 106, -1, 19],  # Left Winger
#                       'CM':[ 35,  75,  19, 49],  # Center Midfielder
#                       'RW':[ 40, 106,  49, 69], # Right Winger
#                       'CF':[ 75, 106,  19, 49]}  # Center Forward/Attacking Middle

# we need to ovverride our find_segments function in order to do 2nd role assigmnet
# class PlayersRoleSet_New(PlayersRoleSet):
#     def find_segments(self, x, y):

#         for role, segment in self.role_segment_coords.items():
#             if (x>=segment[0] and x<=segment[1] and y>=segment[2] and y<=segment[3]):
#                 return role

#         return None

# playersRoleSet_New = PlayersRoleSet_New()
# playersRoleSet.set_role_segment_coords(new_role_segment_coords)
```

```python
# INIT PlayersRoleSet class
playersRoleSet = PlayersRoleSet()
```

```python
# runing this you can see the list of all feature combinations we're going to use
featureBuilder = FeatureBuilder(list(playersRoleSet.get_role_segment_coords().keys()))
feature_list = featureBuilder.get_feature_labels()

feature_list[:10] # print the first 10 features
```

    ['home_LB_all_avrg_x',
     'home_LB_all_avrg_y',
     'home_LB_all_avrg_speed',
     'home_LB_all_avrg_direction_x',
     'home_LB_all_avrg_direction_y',
     'home_LB_all_inner_dis_to_avrg_pos',
     'home_LB_slow_avrg_x',
     'home_LB_slow_avrg_y',
     'home_LB_slow_avrg_speed',
     'home_LB_hir_avrg_x']

#### More detailed infor on features list

group features (home&away) | Seperate Team features (home&away) | Teams features (all together) | Refree features
--- | --- | --- | ---
avrg x,y | $dbscan^{5*}$ avrg x,y | avrg x,y | x,y
avrg speed | dbscan avrg speed | avrg speed | speed
avrg speed direction on x,y | inner distance to dbscan | inner distance to avrg pos | direction on x,y
avrg pos | gk x,y | dbscan avrg x,y |
$inner distance^{1*}$ to avrg pos | gk speed | dbscan avrg speed |
$slow^{2*}$ players' avrg x,y | gk direction on x,y | inner distance to dbscan pos |
slow players' avrg x,y | min x,y | |
$hir^{3*}$ players' avrg x,y | min (x,y)'s speed | |
hir players' avrg x,y | min speed | |
$sprint^{4*}$ players' avrg x,y | min speed's x,y | |
sprint players' avrg x,y | max x,y | |
 | max (x,y)' speed  | |
 | max speed  | |
 | max speed's x,y | |

$^{1*}$Inner distance shows the sum of distance of players' from the average position of the traget cluster.

$^{*2, *3, *4}$Analyzing the strategic context of soccer, three speed groups were selected and their attributes were calculated. These groups are Slow, Hir (high-intensity run) and Sprint, where
$$
\left\{\begin{matrix}
\vec{V}_{slow}\leq1.5\frac{m}{s},\\
1.5\frac{m}{s}<\vec{V}_{hir}\leq 3\frac{m}{s},\\
3\frac{m}{s}<\vec{V}_{sprint}.
\end{matrix}\right.
$$

Each speed category of any group shows different characters in their distance to the ball's coordinates.

The average, max, min (x, y) coordinates of players were used to recognize
the moments when same team players were approaching each
other, and eventually approaching to the ball as well, to gain
more control of the ball. However, the average position for
any role group did not give the desired values when some
players in that group were nested and the remaining players
were relatively remote.

For this reason, clusters were found by
applying density-based clustering (DBSCAN)$^{*5}$ method on
all role groups. For both axes, we set the minimum number of
samples in a neighborhood required to form a cluster to k (we set this number to 7 meters when calculating teams-altogether features, to 4 when calculating separate team features ) and
the maximum distance between two samples in a neighborhood
to 20 meters. Consequently, the average (x, y) coordinates of
each role group were calculated for the given clusters.

### Calculate features

```python
# CONTANTS
match_start_minute={1:0, 2:45} # indicate starting minutes of matches for each half
error_list = [60706, 60795, 60798, 60845, 116001326] # match IDs with data error

# MATCH INFO LABELS
match_info =  {'match_id':None, 'half':None, 'minute':None, 'second':None, 'game_state':None, 'x':None, 'y':None}
```

```python
# GET GENERAL INFO ABOUT MATCHES
matches = load_json_content('../data/general/matches_2017_2018_v1.json')
```

```python
# FEATURE EXTRACTION

for match_data in matches:

    match_id = int(match_data['id'])

    if match_id not in error_list:

        match_info['match_id'] = match_id # set match id

        # get roaster data
        match_squad_info = load_json_content('../data/match_' + str(match_id) + '/roster_data_' + str(match_id) + '.json')
        # get player data
        match_palyer_info = load_json_content('../data/match_' + str(match_id) + '/players_data_' + str(match_id) + '.json')
        # get per second data
        data_persec = load_json_content('../data/match_' + str(match_id) + '/per_sec_data_' + str(match_id) + '.json')

        # INIT CLASSES

        # INIT DataModel class
        dataModel = DataModel(minutes_step=15)

        # init players info
        dataModel.init_players_info(match_data, match_squad_info, match_palyer_info)

        # INIT FeatureBuilder class
        featureBuilder = FeatureBuilder(list(playersRoleSet.get_role_segment_coords().keys())) # get rollist and init feature builder

        del match_squad_info, match_palyer_info

        # list to store features
        features_set = []

        # reset ball position info: x-pos, y-pos, game state to define if game stopped or not
        ball_position = {'x':0, 'y':0, 'game_state':False}

        # var for define half, second
        match_start_half = 0
        prev_second = 0

        # ITERATE THROUGH EACH SECOND
        for data_t in data_persec:

            # AT THE BEGNING OF EACH HALF GET PLAYER INFO FOR SELECTED TIMEWINDOW FOR ROLE ASSIGNING, THEN START ITERATION
            if data_t['half']!=match_start_half:

                dataModel.reset_positional_info()

                match_start_half=data_t['half']

                dataModel.calculate_fist_time_step_data(data_persec=data_persec, match_half=match_start_half, threshold_min=5)

                #----------  assign role  -------
                # get scaled values
                scale_values=playersRoleSet.get_scaled_values(dataModel=dataModel, posCounIndex=-2)

                # iterate through each player
                for team, _ in dataModel.player_data.items():
                    for ID, playerdata in dataModel.player_data[team].items():

                        # set role to player if sum of rolePosCount and last min count of posCount is equal is nonzero
                        dataModel.player_data[team][ID]['role'] = playersRoleSet.set_role(playerdata=playerdata, posCounIndex=-2, scale_values=scale_values, team=team)
                #----------  assign role  -------

                prev_minute = match_start_minute[match_start_half]

            # EACH MINUTE ASSIGN ROLE TO PLAYERS
            if prev_minute!=data_t['minute']:

                prev_minute=data_t['minute']

                #----------  assign role  -------
                # get scaled values
                scale_values=playersRoleSet.get_scaled_values(dataModel=dataModel, posCounIndex=-1)

                # iterate through each player
                for team, _ in dataModel.player_data.items():
                    for ID, playerdata in dataModel.player_data[team].items():

                        # set role to player if sum of rolePosCount and last min count of posCount is equal is nonzero
                        dataModel.player_data[team][ID]['role'] = playersRoleSet.set_role(playerdata=playerdata, posCounIndex=-1, scale_values=scale_values, team=team)
                #----------  assign role  -------

                # shif time-step data to right after role assignment
                dataModel.shift_tms_data_right()

            # EACH SECOND CALCULATE FEATURES HERE
            # after geting all players features for a second calculate all at the end of second

            if prev_second!=data_t['second']:

                prev_second=data_t['second']

                # make sure we have more than six players on the pitch for each team at the time
                if len(featureBuilder.role_data['home']['Team']['all_x'])>6 and len(featureBuilder.role_data['away']['Team']['all_x'])>6:

                    # get match info and Y
                    match_info['half'] = data_t['half']
                    match_info['minute'] = data_t['minute']-1 if (data_t['second']==0) else data_t['minute']
                    match_info['second'] = 59 if (data_t['second']==0) else data_t['second']-1
                    match_info['game_state'] = 1 if ball_position['game_state']==True else 0
                    match_info['x'] = -1 if (ball_position['game_state']==False and ball_position['x']==0) else ball_position['x']
                    match_info['y'] = -1 if (ball_position['game_state']==False and ball_position['y']==0) else ball_position['y']

                    # get features and match info then add to main list
                    features_set.append(list(match_info.values()) + featureBuilder.calculate_features())

                # at the end, reset current_data dict-array
                featureBuilder.reset_referee_data()
                featureBuilder.reset_goalkeeper_data()
                featureBuilder.reset_role_data()

                # reset ball position info
                ball_position = {'x':0, 'y':0, 'game_state':False}

            if (data_t['xpos']>-1 and data_t['xpos']<106) and (data_t['ypos']>-1 and data_t['ypos']<69):

                # for role assignment get players data when the rival team has the ball
                dataModel.add_player_activity(data_t['teamId'], data_t['hasballTeamId'], data_t['jerseyNumber'], data_t['xpos'], data_t['ypos'])

                # round data to 2 in order
                xpos = round(data_t['xpos'], 2)
                ypos = round(data_t['ypos'], 2)
                speed = round(data_t['speed'], 2)

                if (data_t['hasballTeamId']==data_t['teamId'] and data_t['hasballJerseyNumber']==data_t['jerseyNumber']
                    and data_t['teamId']!=0 and data_t['jerseyNumber']!=0):
                    ball_position['x'] = xpos
                    ball_position['y'] = ypos
                    ball_position['game_state'] = True

                if data_t['teamId']!=dataModel.referee_id:

                    team = dataModel.team_id_dict[data_t['teamId']]
                    role = dataModel.player_data[team][data_t['jerseyNumber']]['role']

                    if (role != None and np.sum(dataModel.player_data[team][data_t['jerseyNumber']]['rolePosCount'])!=0):

                        if role!='GK':

                            # define speed categorgy
                            speed_prefix='sprint' if speed>3 else 'slow' if speed<1.5 else 'hir'

                            for tmp_role in [role, 'Team']:
                                for tmp_speed in [speed_prefix, 'all']:

                                    # add player values
                                    featureBuilder.role_data[team][tmp_role][tmp_speed+'_x'].append(xpos)
                                    featureBuilder.role_data[team][tmp_role][tmp_speed+'_y'].append(ypos)
                                    featureBuilder.role_data[team][tmp_role][tmp_speed+'_speed'].append(speed)

                        else:

                            # add goalkeeper values
                            featureBuilder.goalkeeper_data[team]['x'].append(xpos)
                            featureBuilder.goalkeeper_data[team]['y'].append(ypos)
                            featureBuilder.goalkeeper_data[team]['speed'].append(speed)

                else:

                    # add referee values
                    featureBuilder.referee_data['x'].append(xpos)
                    featureBuilder.referee_data['y'].append(ypos)
                    featureBuilder.referee_data['speed'].append(speed)

        # SAVE FEATURES SET
        with open('../data/general/feature-set/match_{}.csv'.format(match_info['match_id']),'w') as file:
            writer = csv.writer(file)
            writer.writerow(list(match_info.keys())+featureBuilder.get_feature_labels())
            writer.writerows(features_set)

        file.close()

        del dataModel
        del featureBuilder
        del data_persec
```


Sascha Spors,
Professorship Signal Theory and Digital Signal Processing,
Institute of Communications Engineering (INT),
Faculty of Computer Science and Electrical Engineering (IEF),
University of Rostock,
Germany

# Tutorial Digital Signal Processing

**Stable IIR Biquad Filter -> Coefficient's Value Range**,
Winter Semester 2021/22 (Master Course #24505)

- lecture: https://github.com/spatialaudio/digital-signal-processing-lecture
- tutorial: https://github.com/spatialaudio/digital-signal-processing-exercises

Feel free to contact lecturer frank.schultz@uni-rostock.de

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.markers import MarkerStyle
from matplotlib.patches import Circle
from scipy import signal

def zplane_plot(ax, z, p, k):
    """Plot pole/zero/gain plot of discrete-time, linear-time-invariant system.

    Note that the for-loop handling might be not very efficient
    for very long FIRs

    z...array of zeros in z-plane
    p...array of poles in z-zplane
    k...gain factor

    taken from own work
    URL = ('https://github.com/spatialaudio/signals-and-systems-exercises/'
           'blob/master/sig_sys_tools.py')

    currently we don't use the ax input parameter, we rather just plot
    in hope for getting an appropriate place for it from the calling function
    """
    # draw unit circle
    Nf = 2**7
    Om = np.arange(Nf) * 2*np.pi/Nf
    plt.plot(np.cos(Om), np.sin(Om), 'C7')

    try:  # TBD: check if this pole is compensated by a zero
        circle = Circle((0, 0), radius=np.max(np.abs(p)),
                        color='C7', alpha=0.15)
        plt.gcf().gca().add_artist(circle)
    except ValueError:
        print('no pole at all, ROC is whole z-plane')

    zu, zc = np.unique(z, return_counts=True)  # find and count unique zeros
    for zui, zci in zip(zu, zc):  # plot them individually
        plt.plot(np.real(zui), np.imag(zui), ms=8,
                 color='C0', marker='o', fillstyle='none')
        if zci > 1:  # if multiple zeros exist then indicate the count
            plt.text(np.real(zui), np.imag(zui), zci)

    pu, pc = np.unique(p, return_counts=True)  # find and count unique poles
    for pui, pci in zip(pu, pc):  # plot them individually
        plt.plot(np.real(pui), np.imag(pui), ms=8,
                 color='C0', marker='x')
        if pci > 1:  # if multiple poles exist then indicate the count
            plt.text(np.real(pui), np.imag(pui), pci)

    plt.text(0, +1, 'k={0:f}'.format(k))
    plt.text(0, -1, 'ROC for causal: white')
    plt.axis('square')
    plt.xlabel(r'$\Re\{z\}$')
    plt.ylabel(r'$\Im\{z\}$')
    plt.grid(True, which="both", axis="both",
             linestyle="-", linewidth=0.5, color='C7')
```

## Biquad
A biquadratic or - abbreviated - **biquad** filter is a **recursive filter of second
order** that is commonly implemented in digital signal processing. The biquad
filter is expressed in most textbooks with $a_0=1$ as

\begin{equation}
H(z)=\frac{b_0z^0+b_1z^{-1}+b_2z^{-2}}{1\cdot z^0+a_1z^{-1}+a_2z^{-2}}
=\frac{b_0z^2+b_1z+b_2}{z^2+a_1z+a_2}.
\label{eq:biquad}
\end{equation}

- The notation in form of the **first fraction** in above equation is suitable for identifying the structure of the filter (the necessary delay elements for the input (numerator) and the output (denominator) can directly be seen) and for creating block diagrams (direct form I/II, transposed direct forms, ...) from the difference equation.
- The form of the **second fraction** is more convenient for calculating poles, zeros and gain of the filter.

## Poles of Biquad
The value range of the coefficients $a_{1,2}$ is important as they determine the
poles and therefore the stability of the filter. The poles of the biquad in general

\begin{equation}
z_{\infty,1,2}=\frac{-a_1}{2}\pm\frac{\sqrt{a_1^2-4a_2}}{2}.
\end{equation}

We must aim at $a_1,a_2\in\mathbb{R}$ since **only real coefficients** ensure that the system $H(z)$ produces **real valued output signals** from real valued input signals.
This can be achieved if the two poles are **either real or complex conjugates**.
Furthermore, we aim at a **stable** system, which requires that both poles are **within the unit circle**.

## Real Poles

For **real poles** (i.e. poles on the real-axis in the z-plane), the expression under the square root has to be positive

\begin{equation}
z_{\infty,1,2}=\frac{-a_1}{2}\pm\frac{1}{2}\sqrt{\underbrace{a_1^2-4a_2}_{\geq0}}
\end{equation}

and the **two conditions**

\begin{align}
a_1\leq-2\sqrt{a_2} \vee a_1\geq2\sqrt{a_2}\\
a_2>|a_1|-1
\end{align}

must hold (try to figure out these conditions by yourself) to achieve stable IIR system with real valued coefficients $a_{1,2}$.

## Complex Conjugate Poles

For the case of **complex conjugate poles**, one can write

\begin{equation}
z_{\infty,1,2}=\frac{-a_1}{2}\pm\frac{\mathrm{j}}{2}\sqrt{\underbrace{4a_2-a_1^2}_{>0}}.
\label{eq:complexconjugatepoles}
\end{equation}

which holds if the two conditions

\begin{align}
-2\sqrt{a_2}<a_1<2\sqrt{a_2}\\
0\leq a_2<1
\end{align}

are met (try to figure out these conditions by yourself).

The figure below visualizes the range of values of the coefficients $a_1,a_2$ as being inside the blue/orange contour for a pair of real poles (blue) and a pair of complex conjugate poles (orange).

```python
# real pole
a1r = np.arange(-2,2.1,0.1)
a2r = np.abs(a1r)-1
# complex conjugate
a1cc = np.arange(-2,2.1,0.1)
a2cc = a1cc**2/4

plt.figure(figsize=(5,5))
plt.plot(a1r, a2r,'C0', lw=3, label='for real poles')
plt.plot([-2,2], [1,1], 'C0', lw=3)
plt.plot(a1cc, a2cc,'C1', label='for complex conjugate pole')
plt.plot([-2,2], [1,1], 'C1')
plt.axis('equal')
plt.xlabel(r'$a_1$')
plt.ylabel(r'$a_2$')
plt.title(r'Stable biquad if $a_1$/$a_2$ within the blue/orange contour')
plt.legend()
plt.grid(True)
```

```python
# check for two real poles
b = (1, 0, 0)
a = (1, -1, 0.25)
z, p, k = signal.tf2zpk(b, a)
ax = plt.figure(figsize=(5, 5))
zplane_plot(ax, z, p, k)

# conditions
print(a[1] <= -2*np.sqrt(a[2]) or a[1] >= +2*np.sqrt(a[2]))
print(a[2] > np.abs(a[1])-1)
```

```python
# check for complex conjugate pole
b = (1, 0, 0)
a = (1, -1, 0.5)
z, p, k = signal.tf2zpk(b, a)
ax = plt.figure(figsize=(5, 5))
zplane_plot(ax, z, p, k)

print(-2*np.sqrt(a[2]) < a[1] < +2*np.sqrt(a[2]))
print(0 <= a[2] < 1)
```

# **Copyright**

The notebooks are provided as [Open Educational Resources](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebooks for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Frank Schultz, Digital Signal Processing - A Tutorial Featuring Computational Examples* with the URL https://github.com/spatialaudio/digital-signal-processing-exercises


# Modeling and Simulation in Python

Chapter 10 Example: Springy Pendulum

Copyright 2017 Allen Downey

License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0)

```python
# If you want the figures to appear in the notebook,
# and you want to interact with them, use
# %matplotlib notebook

# If you want the figures to appear in the notebook,
# and you don't want to interact with them, use
# %matplotlib inline

# If you want the figures to appear in separate windows, use
# %matplotlib qt5

# to switch from one to another, you have to select Kernel->Restart

%matplotlib notebook

from modsim import *
```

### Pendulum

This notebook solves the Spider-Man problem from spiderman.ipynb, demonstrating a different development process for physical simulations.

In pendulum_sympy, we derive the equations of motion for a springy pendulum without drag, yielding:

$ \ddot{x} = \frac{k length_{0} x}{m \sqrt{x^{2} + y^{2}}} - \frac{k x}{m} $

$ \ddot{y} = - g + \frac{k length_{0} y}{m \sqrt{x^{2} + y^{2}}} - \frac{k y}{m} $

We'll use the same conditions we saw in `spiderman.ipynb`

```python
condition = Condition(g = 9.8,
                      m = 75,
                      area = 1,
                      rho = 1.2,
                      v_term = 60,
                      duration = 30,
                      length0 = 100,
                      angle = (270 - 45),
                      k = 20)
```

Now here's a version of `make_system` that takes a `Condition` object as a parameter.

`make_system` uses the given value of `v_term` to compute the drag coefficient `C_d`.

```python
def make_system(condition):
    """Makes a System object for the given conditions.

    condition: Condition with height, g, m, diameter,
               rho, v_term, and duration

    returns: System with init, g, m, rho, C_d, area, and ts
    """
    unpack(condition)

    theta = np.deg2rad(angle)
    x, y = pol2cart(theta, length0)
    P = Vector(x, y)
    V = Vector(0, 0)

    init = State(x=P.x, y=P.y, vx=V.x, vy=V.y)
    C_d = 2 * m * g / (rho * area * v_term**2)
    ts = linspace(0, duration, 501)

    return System(init=init, g=g, m=m, rho=rho,
                  C_d=C_d, area=area, length0=length0,
                  k=k, ts=ts)
```

Let's make a `System`

```python
system = make_system(condition)
system
```

      value

      init
      x     -70.71067811865477 dimensionless
y     -...

      g
      9.8

      m
      75

      rho
      1.2

      C_d
      0.340278

      area
      1

      length0
      100

      k
      20

      ts
      [0.0, 0.06, 0.12, 0.18, 0.24, 0.3, 0.36, 0.42,...

```python
system.init
```

      value

      x
      -70.71067811865477 dimensionless

      y
      -70.71067811865474 dimensionless

      vx
      0 dimensionless

      vy
      0 dimensionless

To write the slope function, we can get the expressions for `ax` and `ay` directly from SymPy and plug them in.

```python
def slope_func(state, t, system):
    """Computes derivatives of the state variables.

    state: State (x, y, x velocity, y velocity)
    t: time
    system: System object with length0, m, k

    returns: sequence (vx, vy, ax, ay)
    """
    x, y, vx, vy = state
    unpack(system)

    ax = k*length0*x/(m*sqrt(x**2 + y**2)) - k*x/m
    ay = -g + k*length0*y/(m*sqrt(x**2 + y**2)) - k*y/m

    return vx, vy, ax, ay
```

As always, let's test the slope function with the initial conditions.

```python
slope_func(system.init, 0, system)
```

    (<Quantity(0, 'dimensionless')>,
     <Quantity(0, 'dimensionless')>,
     <Quantity(0.0, 'dimensionless')>,
     <Quantity(-9.8, 'dimensionless')>)

And then run the simulation.

```python
%time run_odeint(system, slope_func)
```

    CPU times: user 84 ms, sys: 0 ns, total: 84 ms
    Wall time: 83.6 ms

### Visualizing the results

We can extract the x and y components as `Series` objects.

```python
xs = system.results.x
ys = system.results.y
```

The simplest way to visualize the results is to plot x and y as functions of time.

```python
newfig()
plot(xs, label='x')
plot(ys, label='y')

decorate(xlabel='Time (s)',
         ylabel='Position (m)')
```

    <IPython.core.display.Javascript object>

We can plot the velocities the same way.

```python
vxs = system.results.vx
vys = system.results.vy
```

```python
newfig()
plot(vxs, label='vx')
plot(vys, label='vy')

decorate(xlabel='Time (s)',
         ylabel='Velocity (m/s)')
```

    <IPython.core.display.Javascript object>

Another way to visualize the results is to plot y versus x.  The result is the trajectory through the plane of motion.

```python
newfig()
plot(xs, ys, label='trajectory')

decorate(xlabel='x position (m)',
         ylabel='y position (m)')
```

    <IPython.core.display.Javascript object>

We can also animate the trajectory.  If there's an error in the simulation, we can sometimes spot it by looking at animations.

```python
newfig()
decorate(xlabel='x position (m)',
         ylabel='y position (m)',
         xlim=[-100, 100],
         ylim=[-200, -50],
         legend=False)

for x, y in zip(xs, ys):
    plot(x, y, 'bo', update=True)
    sleep(0.01)
```

    <IPython.core.display.Javascript object>

Here's a function that encapsulates that code and runs the animation in (approximately) real time.

```python
def animate2d(xs, ys, speedup=1):
    """Animate the results of a projectile simulation.

    xs: x position as a function of time
    ys: y position as a function of time

    speedup: how much to divide `dt` by
    """
    # get the time intervals between elements
    ts = xs.index
    dts = np.diff(ts)
    dts = np.append(dts, 0)

    # decorate the plot
    newfig()
    decorate(xlabel='x position (m)',
             ylabel='y position (m)',
             xlim=[xs.min(), xs.max()],
             ylim=[ys.min(), ys.max()],
             legend=False)

    # loop through the values
    for x, y, dt in zip(xs, ys, dts):
        plot(x, y, 'bo', update=True)
        sleep(dt / speedup)
```

```python
animate2d(system.results.x, system.results.y)
```

    <IPython.core.display.Javascript object>

```python

```


# Dropout and maxout
In this lab we will explore the methods of [dropout](https://www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf), a regularisation method which stochastically drops out activations from the model during training, and [maxout](http://www.jmlr.org/proceedings/papers/v28/goodfellow13.pdf), another non-linear transformation that can be used in multiple layer models. This is based on material covered in the [fifth lecture slides](http://www.inf.ed.ac.uk/teaching/courses/mlp/2016/mlp05-hid.pdf).

## Exercise 1: Implementing a dropout layer

During training the forward propagation through a dropout layer produces outputs where a subset of the input dimensions are set to zero ('dropped out'). The dimensions to be dropped out are randomly sampled for each new batch, with each dimension having a probability $p$ of being included and the inclusion (or not) of each dimension independent of all the others. If the inputs to a dropout layer are $D$ dimensional vectors then we can represent the dropout operation by an elementwise multiplication by a $D$ dimensional *binary mask* vector $\boldsymbol{m} = \left[m_1 ~ m_2 ~\dots~ m_D\right]^{\rm T}$ where $m_d \sim \text{Bernoulli}(p) ~~\forall d \in \lbrace 1 \dots D\rbrace$.

As a first step implement a `random_binary_mask` function in the cell below to generate a binary mask array of a specified shape, where each value in the outputted array is either a one with probablity `prob_1` or zero with probability `1 - prob_1` and all values are sampled independently.

```python
def random_binary_mask(prob_1, shape, rng):
    """Generates a random binary mask array of a given shape.

    Each value in the outputted array should be an indepedently sampled
    binary value i.e in {0, 1} with the probability of each value
    being 1 being equal to `prob_1`.

    Args:
        prob_1: Scalar value in [0, 1] specifying probability each
            entry in output array is equal to one.
        shape: Shape of returned mask array.
        rng (RandomState): Seeded random number generator object.

    Returns:
        Random binary mask array of specified shape.
    """
    return rng.uniform(size=shape) < prob_1
```

Test your `random_binary_mask` function using the cell below (if your implementation is incorrect you will get an `AssertionError` - look at what the assert statement is checking for a clue as to what is wrong).

```python
import numpy as np
test_shapes = [(1, 1000), (10, 10, 10)]
test_probs = [0.1, 0.5, 0.7]
for i in range(10):
    for shape in test_shapes:
        for prob in test_probs:
            output = random_binary_mask(prob, shape, np.random)
            # Check generating correct shape output
            assert output.shape == shape
            # Check all outputs are binary values
            assert np.all((output == 1.) | (output == 0.))
            # Check proportion equal to one plausible
            # This will be noisy so there is a chance this will error
            # even for a correct implementation
            assert np.abs(output.mean() - prob) < 0.1
```

Given a randomly sampled binary mask $\boldsymbol{m}$, the outputs $\lbrace \boldsymbol{y}^{(b)} \rbrace_{b=1}^B$ of the stochastic forward propagation through a dropout layer given a batch of inputs $\lbrace \boldsymbol{x}^{(b)} \rbrace_{b=1}^B$ can be calculated by simply performing an elementwise multiplication of the inputs with the mask

\begin{equation}
  y^{(b)}_d = m_k x^{(b)}_d \qquad \forall d \in \lbrace 1 \dots D \rbrace
\end{equation}

The corresponding partial derivatives required for implementing back-propagation through a dropout layer are

\begin{equation}
  \frac{\partial y^{(b)}_k}{\partial x^{(b)}_d} =
  \begin{cases}
      m_k & \quad k = d \\
      0   & \quad k \neq d
  \end{cases}
  \qquad \forall k,\,d \in \lbrace 1 \dots D \rbrace
\end{equation}

As discussed in the lecture slides, when using a model trained with dropout at test time dimensions are no longer stochastically dropped out and instead all activations are deterministically fed forward through the model. So that the expected (mean) outputs of each layer are the same at test and training we scale the forward propagated inputs during testing by $p$ the probability of each dimension being included in the output. If we denote the deterministically forward-propagated batch of outputs of a dropout layer at test time as $\lbrace \boldsymbol{z}^{(b)} \rbrace_{b=1}^B$ then we have

\begin{equation}
  z^{(b)}_d =
  \mathbb{E}\left[ y^{(b)}_d \right] =
  \sum_{m_d \in \lbrace 0,1 \rbrace} \left( \mathbb{P}\left[\mathrm{m}_d = m_d\right] m_d x^{(b)}_d \right) =
  (p) (1) x^{(b)}_d  + (1-p) (0) x^{(b)}_d =
  p x^{(b)}_d  \qquad \forall d \in \lbrace 1 \dots D \rbrace
\end{equation}

To allow switching between this stochastic training time behaviour and deterministic test time behaviour, a new abstract `StochasticLayer` class has been defined in the `mlp.layers` module. This acts similarly to the layer objects we have already encountered other than adding an extra boolean argument `stochastic` to the `fprop` method interface. When `stochastic = True` (the default) a stochastic forward propagation should be caculated, for dropout this corresponding to $\boldsymbol{x}^{(b)} \to \boldsymbol{y}^{(b)}$ above. When `stochastic = False` a deterministic forward-propagation corresponding to the expected output of the stochastic forward-propagation should be calculated, for dropout this corresponding to $\boldsymbol{x}^{(b)} \to \boldsymbol{z}^{(b)}$ above.

Using the skeleton `DropoutLayer` class definition below, implement the `fprop` and `bprop` methods. You may wish to store the binary mask used in the forward propagation as an attribute of the class for use in back-propagation - it is fine to assume that the `fprop` and `bprop` will always be called in sync.

```python
from mlp.layers import StochasticLayer

class DropoutLayer(StochasticLayer):
    """Layer which stochastically drops input dimensions in its output."""

    def __init__(self, rng=None, incl_prob=0.5, share_across_batch=True):
        """Construct a new dropout layer.

        Args:
            rng (RandomState): Seeded random number generator.
            incl_prob: Scalar value in (0, 1] specifying the probability of
                each input dimension being included in the output.
            share_across_batch: Whether to use same dropout mask across
                all inputs in a batch or use per input masks.
        """
        super(DropoutLayer, self).__init__(rng)
        assert incl_prob > 0. and incl_prob <= 1.
        self.incl_prob = incl_prob
        self.share_across_batch = share_across_batch

    def fprop(self, inputs, stochastic=True):
        """Forward propagates activations through the layer transformation.

        Args:
            inputs: Array of layer inputs of shape (batch_size, input_dim).
            stochastic: Flag allowing different deterministic
                forward-propagation mode in addition to default stochastic
                forward-propagation e.g. for use at test time. If False
                a deterministic forward-propagation transformation
                corresponding to the expected output of the stochastic
                forward-propagation is applied.

        Returns:
            outputs: Array of layer outputs of shape (batch_size, output_dim).
        """
        if stochastic:
            mask_shape = (1,) + inputs.shape[1:] if self.share_across_batch else inputs.shape
            self._mask = (rng.uniform(size=mask_shape) < self.incl_prob)
            return inputs * self._mask
        else:
            return inputs * self.incl_prob

    def bprop(self, inputs, outputs, grads_wrt_outputs):
        """Back propagates gradients through a layer.

        Given gradients with respect to the outputs of the layer calculates the
        gradients with respect to the layer inputs. This should correspond to
        default stochastic forward-propagation.

        Args:
            inputs: Array of layer inputs of shape (batch_size, input_dim).
            outputs: Array of layer outputs calculated in forward pass of
                shape (batch_size, output_dim).
            grads_wrt_outputs: Array of gradients with respect to the layer
                outputs of shape (batch_size, output_dim).

        Returns:
            Array of gradients with respect to the layer inputs of shape
            (batch_size, input_dim).
        """
        return grads_wrt_outputs * self._mask

    def __repr__(self):
        return 'DropoutLayer(incl_prob={0:.1f})'.format(self.incl_prob)
```

Test your implementation by running the cell below (if your implementation is incorrect you will get an `AssertionError` - look at what the assert statement is checking for a clue as to what is wrong).

```python
seed = 31102016
rng = np.random.RandomState(seed)
test_incl_probs = [0.1, 0.5, 0.7]
input_shape = (5, 10)
for incl_prob in test_incl_probs:
    layer = DropoutLayer(rng, incl_prob)
    inputs = rng.normal(size=input_shape)
    grads_wrt_outputs = rng.normal(size=input_shape)
    for t in range(100):
        outputs = layer.fprop(inputs, stochastic=True)
        # Check outputted array correct shape
        assert outputs.shape == inputs.shape
        # Check all outputs are either equal to inputs or zero
        assert np.all((outputs == inputs) | (outputs == 0))
        grads_wrt_inputs = layer.bprop(inputs, outputs, grads_wrt_outputs)
        # Check back-propagated gradients only non-zero for non-zero outputs
        assert np.all((outputs != 0) == (grads_wrt_inputs != 0))
        assert np.all(grads_wrt_outputs[outputs != 0] == grads_wrt_inputs[outputs != 0])
    det_outputs = layer.fprop(inputs, stochastic=False)
    # Check deterministic fprop outputs are correct shape
    assert det_outputs.shape == inputs.shape
    # Check deterministic fprop outputs scaled correctly
    assert np.allclose(det_outputs, incl_prob * inputs)
```

### Optional extension

Above we assumed the same dropout mask was applied to each input in a batch, as specified in the lecture slides. In practice sometimes a different mask is sampled for each input. As an extension you could try implementing this per-input form of dropout either by defining a new layer or adding an extra argument to the constructor of the above layer which allows you to switch between the two forms.

## Exercise 2: Training with dropout

Experiment with training models with dropout layers to classify MNIST digits. Code has been provided below as a starting point for setting up the model objects though feel free to use any additional adaptive learning rules or learning rule schedulers you wrote during the coursework instead. You may also wish to change the model architecture to use a larger model with more parameters in which the regularisation provided by dropout is likely to have a more pronounced effect. You will probably also find that models with dropout generally need to be trained over more epochs than those without (can you suggest why this might be?).

You should training with a few different `incl_prob` settings for the dropout layers and try to establish how the values chosen affect the training performance. You may wish to experiment with using a different dropout probability at the input than for the intermediate layers (why?).

You may wish to start reading through and implementing exercise 3 while waiting for training runs to complete.

```python
import numpy as np
import logging
from mlp.data_providers import MNISTDataProvider
from mlp.models import MultipleLayerModel
from mlp.layers import ReluLayer, AffineLayer
from mlp.errors import CrossEntropySoftmaxError
from mlp.initialisers import GlorotUniformInit, ConstantInit
from mlp.learning_rules import MomentumLearningRule
from mlp.optimisers import Optimiser
import matplotlib.pyplot as plt
%matplotlib inline

# Seed a random number generator
seed = 31102016
rng = np.random.RandomState(seed)

# Set up a logger object to print info about the training run to stdout
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.handlers = [logging.StreamHandler()]

# Create data provider objects for the MNIST data set
train_data = MNISTDataProvider('train', batch_size=50, rng=rng)
valid_data = MNISTDataProvider('valid', batch_size=50, rng=rng)
```

```python
# Probability of input being included in output in dropout layer
incl_prob = 0.5

input_dim, output_dim, hidden_dim = 784, 10, 125

# Use Glorot initialisation scheme for weights and zero biases
weights_init = GlorotUniformInit(rng=rng, gain=2.**0.5)
biases_init = ConstantInit(0.)

# Create three affine layer model with rectified linear non-linearities
# and dropout layers before every affine layer
model = MultipleLayerModel([
    DropoutLayer(rng, incl_prob),
    AffineLayer(input_dim, hidden_dim, weights_init, biases_init),
    ReluLayer(),
    DropoutLayer(rng, incl_prob),
    AffineLayer(hidden_dim, hidden_dim, weights_init, biases_init),
    ReluLayer(),
    DropoutLayer(rng, incl_prob),
    AffineLayer(hidden_dim, output_dim, weights_init, biases_init)
])

# Multiclass classification therefore use cross-entropy + softmax error
error = CrossEntropySoftmaxError()

# Use a momentum learning rule - you could use an adaptive learning rule
# implemented for the coursework here instead
learning_rule = MomentumLearningRule(0.02, 0.9)

# Monitor classification accuracy during training
data_monitors={'acc': lambda y, t: (y.argmax(-1) == t.argmax(-1)).mean()}

optimiser = Optimiser(
    model, error, learning_rule, train_data, valid_data, data_monitors)

num_epochs = 100
stats_interval = 5

stats, keys, run_time = optimiser.train(num_epochs=num_epochs, stats_interval=stats_interval)

# Plot the change in the validation and training set error over training.
fig_1 = plt.figure(figsize=(8, 4))
ax_1 = fig_1.add_subplot(111)
for k in ['error(train)', 'error(valid)']:
    ax_1.plot(np.arange(1, stats.shape[0]) * stats_interval,
              stats[1:, keys[k]], label=k)
ax_1.legend(loc=0)
ax_1.set_xlabel('Epoch number')

# Plot the change in the validation and training set accuracy over training.
fig_2 = plt.figure(figsize=(8, 4))
ax_2 = fig_2.add_subplot(111)
for k in ['acc(train)', 'acc(valid)']:
    ax_2.plot(np.arange(1, stats.shape[0]) * stats_interval,
              stats[1:, keys[k]], label=k)
ax_2.legend(loc=0)
ax_2.set_xlabel('Epoch number')
```

## Exercise 3: Implementing maxout

[Maxout](http://www.jmlr.org/proceedings/papers/v28/goodfellow13.pdf) can be considered a generalisation of the rectified linear transformation implemented in the previous lab.

For a rectified linear (`Relu`) layer the forward propagation corresponds to

\begin{equation}
    y^{(b)}_k =
    \max\left\lbrace 0,\,x^{(b)}_k \right\rbrace
\end{equation}

i.e. each output corresponds to an pairwise maximum of a constant (0) and the input.

Instead of taking the maximum of the input and a constant, we could instead consider taking the maximum over sets of inputs of a fixed size $s$.

\begin{equation}
    y^{(b)}_k =
    \max\left\lbrace x^{(b)}_{(k-1)s + 1},\, x^{(b)}_{(k-1)s + 2},\, \dots ,\, x^{(b)}_{ks} \right\rbrace
\end{equation}

If these inputs $x^{(b)}_d$ are themselves the outputs of an affine layer, then this corresponds to taking the maximum of a series of affine functions of the previous layer outputs. Like a rectified linear layer this leads to piecewise linear input-output relationships (which have well-behaved gradients which do not suffer from the saturation problems of logistic sigmoid / hyperbolic tangent transformations) but unlike the rectified linear case we do not end force a portion of the outputs to be zero.

Experimentally this form of transformation has been found to give good performance, with the name *maxout* chosen because the *out*put is the *max*imum of a set of inputs. Maxout is also commonly used with dropout layers however note they are not directly related - maxout defines a deterministic non-linear transformation which can help improve the representational capacity and trainability of models; dropout defines a stochastic transformation which is mainly aimed at regularising a model to reduce overfitting.

Using layers which take the maximum of fixed sized sets of inputs is also a common technique in models with convolutional layers which we will cover later in the course, with here the layer commonly being termed a *max-pooling* layer (with there being natural generalisation to other choices of reduction functions over pools such as the mean). We will adopt this terminology here for a layer implementing the transformation described above and we will be able to reuse our code implementing this maximum operation when experimenting with convolutional models.

The partial derivatives of this max-pooling transformation are sparse (lots of values are zero), with only the partial derivative of the output of a pool with respect to the maximum input in the pool non-zero. This can be expressed as

\begin{equation}
    \frac{\partial y^{(b)}_k}{\partial x^{(b)}_d} =
    \begin{cases}
      1 & \quad (k-1)s + 1 \leq d  \leq ks \quad\textrm{and} &x^{(b)}_d = \max\left\lbrace x^{(b)}_{(k-1)s + 1},\, x^{(b)}_{(k-1)s + 2},\, \dots ,\, x^{(b)}_{ks} \right\rbrace \\
      0 & \quad \textrm{otherwise}
    \end{cases}.
\end{equation}

Using these definitions implement the `fprop` and `bprop` methods of the skeleton `MaxPoolingLayer` class below.

Some hints

  * One way of organising the inputs into non-overlapping pools is using the `numpy.reshape` function.
  * The `numpy.max` function has an `axis` argument which allows you specify the axis (dimension) of the input array to take the maximum over.
  * It may help to construct a binary mask corresponding to the definitions of the partial derivatives above to allow you to implement the `bprop` method.
  * As with the `DropoutLayer` it is fine to temporarily store values calculated in the `fprop` method as attributes of the object (e.g. `self.val = val`) to use in the `bprop` method (although you don't necessarily need to do this).

```python
from mlp.layers import Layer

class MaxPoolingLayer(Layer):

    def __init__(self, pool_size=2):
        """Construct a new max-pooling layer.

        Args:
            pool_size: Positive integer specifying size of pools over
               which to take maximum value. The outputs of the layer
               feeding in to this layer must have a dimension which
               is a multiple of this pool size such that the outputs
               can be split in to pools with no dimensions left over.
        """
        self.pool_size = pool_size

    def fprop(self, inputs):
        """Forward propagates activations through the layer transformation.

        This corresponds to taking the maximum over non-overlapping pools of
        inputs of a fixed size `pool_size`.

        Args:
            inputs: Array of layer inputs of shape (batch_size, input_dim).

        Returns:
            outputs: Array of layer outputs of shape (batch_size, output_dim).
        """
        assert inputs.shape[-1] % self.pool_size == 0, (
            'Last dimension of inputs must be multiple of pool size')
        pooled_inputs = inputs.reshape(
            inputs.shape[:-1] +
            (inputs.shape[-1] // self.pool_size, self.pool_size))
        pool_maxes = pooled_inputs.max(-1)
        self._mask = pooled_inputs == pool_maxes[..., None]
        return pool_maxes

    def bprop(self, inputs, outputs, grads_wrt_outputs):
        """Back propagates gradients through a layer.

        Given gradients with respect to the outputs of the layer calculates the
        gradients with respect to the layer inputs.

        Args:
            inputs: Array of layer inputs of shape (batch_size, input_dim).
            outputs: Array of layer outputs calculated in forward pass of
                shape (batch_size, output_dim).
            grads_wrt_outputs: Array of gradients with respect to the layer
                outputs of shape (batch_size, output_dim).

        Returns:
            Array of gradients with respect to the layer inputs of shape
            (batch_size, input_dim).
        """
        return (self._mask * grads_wrt_outputs[..., None]).reshape(inputs.shape)

    def __repr__(self):
        return 'MaxPoolingLayer(pool_size={0})'.format(self.pool_size)
```

Test your implementation by running the cell below.

```python
test_inputs = np.array([[-3, -4, 5, 8], [0, -2, 3, -8], [1, 5, 3, 2]])
test_outputs_1 = np.array([[8], [3], [5]])
test_grads_wrt_outputs_1 = np.array([[10], [5], [-3]])
test_grads_wrt_inputs_1 = np.array([[0, 0, 0, 10], [0, 0, 5, 0], [0, -3, 0, 0]])
test_outputs_2 = np.array([[-3, 8], [0, 3], [5, 3]])
test_grads_wrt_outputs_2 = np.array([[3, -1], [2, 5], [5, 3]])
test_grads_wrt_inputs_2 = np.array([[3, 0, 0, -1], [2, 0, 5, 0], [0, 5, 3, 0]])
layer_1 = MaxPoolingLayer(4)
layer_2 = MaxPoolingLayer(2)
# Check fprop with pool_size = 4
assert np.allclose(layer_1.fprop(test_inputs), test_outputs_1)
# Check bprop with pool_size = 4
assert np.allclose(
    layer_1.bprop(test_inputs, test_outputs_1, test_grads_wrt_outputs_1),
    test_grads_wrt_inputs_1
)
# Check fprop with pool_size = 2
assert np.allclose(layer_2.fprop(test_inputs), test_outputs_2)
# Check bprop with pool_size = 2
assert np.allclose(
    layer_2.bprop(test_inputs, test_outputs_2, test_grads_wrt_outputs_2),
    test_grads_wrt_inputs_2
)
```

## Exercise 4: Training with maxout

Use your `MaxPoolingLayer` implementation in a multiple layer models to experiment with how well maxout networks are able to classify MNIST digits. As with the dropout training exercise, code has been provided below as a starting point for setting up the model objects, but again feel free to substitute any components.

If you have time you may wish to experiment with training a model using a combination of maxout and dropout or another regularisation method covered in the last lab notebook.

```python
import numpy as np
import logging
from mlp.data_providers import MNISTDataProvider
from mlp.models import MultipleLayerModel
from mlp.layers import AffineLayer
from mlp.errors import CrossEntropySoftmaxError
from mlp.initialisers import GlorotUniformInit, ConstantInit
from mlp.learning_rules import MomentumLearningRule
from mlp.optimisers import Optimiser
import matplotlib.pyplot as plt
%matplotlib inline

# Seed a random number generator
seed = 31102016
rng = np.random.RandomState(seed)

# Set up a logger object to print info about the training run to stdout
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.handlers = [logging.StreamHandler()]

# Create data provider objects for the MNIST data set
train_data = MNISTDataProvider('train', batch_size=50, rng=rng)
valid_data = MNISTDataProvider('valid', batch_size=50, rng=rng)
```

```python
# Size of pools to take maximum over
pool_size = 2

input_dim, output_dim, hidden_dim = 784, 10, 100

# Use Glorot initialisation scheme for weights and zero biases
weights_init = GlorotUniformInit(rng=rng)
biases_init = ConstantInit(0.)

# Create three affine layer model interleaved with max-pooling layers
model = MultipleLayerModel([
    AffineLayer(input_dim, hidden_dim * pool_size, weights_init, biases_init),
    MaxPoolingLayer(pool_size),
    AffineLayer(hidden_dim, hidden_dim * pool_size, weights_init, biases_init),
    MaxPoolingLayer(pool_size),
    AffineLayer(hidden_dim, output_dim, weights_init, biases_init)
])

# Multiclass classification therefore use cross-entropy + softmax error
error = CrossEntropySoftmaxError()

# Use a momentum learning rule - you could use an adaptive learning rule
# implemented for the coursework here instead
learning_rule = MomentumLearningRule(0.02, 0.9)

# Monitor classification accuracy during training
data_monitors={'acc': lambda y, t: (y.argmax(-1) == t.argmax(-1)).mean()}

optimiser = Optimiser(
    model, error, learning_rule, train_data, valid_data, data_monitors)

num_epochs = 100
stats_interval = 5

stats, keys, run_time = optimiser.train(num_epochs=num_epochs, stats_interval=stats_interval)

# Plot the change in the validation and training set error over training.
fig_1 = plt.figure(figsize=(8, 4))
ax_1 = fig_1.add_subplot(111)
for k in ['error(train)', 'error(valid)']:
    ax_1.plot(np.arange(1, stats.shape[0]) * stats_interval,
              stats[1:, keys[k]], label=k)
ax_1.legend(loc=0)
ax_1.set_xlabel('Epoch number')

# Plot the change in the validation and training set accuracy over training.
fig_2 = plt.figure(figsize=(8, 4))
ax_2 = fig_2.add_subplot(111)
for k in ['acc(train)', 'acc(valid)']:
    ax_2.plot(np.arange(1, stats.shape[0]) * stats_interval,
              stats[1:, keys[k]], label=k)
ax_2.legend(loc=0)
ax_2.set_xlabel('Epoch number')
```


# Economic Planning

## Objective and Prerequisites

In this example, you’ll discover how mathematical optimization can be used to address a  macroeconomic planning problem that a country may face. We’ll show you how to model and solve an input-output problem encompassing the interrelationships between the different sectors of a country’s economy.

This model is example 9 from the fifth edition of Model Building in Mathematical Programming by H. Paul Williams on pages 263-264 and 316-317.

This modeling example is at the intermediate level, where we assume that you know Python and are familiar with the Gurobi Python API. In addition, you should have some knowledge about building mathematical optimization models.

**Download the Repository**

You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip).

**Gurobi License**

In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-MUI-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Economic_Plannng_COM_EVAL_GitHub&utm_term=Economic%20Planning&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-MUI-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Economic_Plannng_COM_EVAL_GitHub&utm_term=Economic%20Planning&utm_content=C_JPM) as an *academic user*.

## Problem Description

In this problem, we assume that we have an economy with three types of industries:

* Coal
* Steel
* Transport

Part of the outputs from these industries are needed as inputs to others. For example, coal is needed to fire the blast furnaces that produce steel, steel is needed in the machinery for extracting coal, etc. We measure all units of production in monetary terms.  That is, each dollar of production by one of the industries requires inputs (in dollars) from possibly its own industry as well as other industries. The required inputs as well as the labor requirements (also measured in dollars) are shown in the following table.

| input (t) /
 output (t+1) | Coal | Steel | Transport |
| --- | --- | --- | --- |
| Coal | 0.1 | 0.5 | 0.4 |
| Steel | 0.1 | 0.1 | 0.2 |
| Transport | 0.2 | 0.1 | 0.2 |
| Labor | 0.6 | 0.3 | 0.2 |
|**Total**  | **1** | **1** | **1**|

There is a time lag in the economy so that the output in year t +1 requires an input in year t.  For example, to produce one-dollar worth of coal requires 0.1 dollars of coal (to provide the necessary power), 0.1 dollars of steel (the steel ‘used up’ in the ‘wear and tear’ on the machinery) and 0.2 dollars of transport (for moving the coal from the mine). In addition, 0.6 dollars of manpower is required. Similarly, the other columns of the table give the inputs required (in dollars) for each dollar of steel and each dollar of transport (trucks, cars, trains, etc.). Notice that the value of each unit of output is exactly matched by the sum of the values of its inputs.

Output from an industry may also be used to build productive capacity for itself or other industries in future years. The inputs required to give unit increases (capacity in dollars’ worth of extra production) in productive capacity are given in the following table. Input from an industry in year t results in a (permanent) increase in productive capacity in year t +2.

| input (t) /
 output (t+2) | Coal | Steel | Transport |
| --- | --- | --- | --- |
| Coal | 0.1 | 0.7 | 0.9 |
| Steel | 0.1 | 0.1 | 0.2 |
| Transport | 0.2 | 0.1 | 0.2 |
| Labor | 0.4 | 0.2 | 0.1 |

Stocks of goods may be held from year to year. At present (the beginning of year 1), the stocks and productive capacities (per year) are given in the following table (in millions of dollars).

| Present | Stocks | Productive Capacity |
| --- | --- | --- |
| Coal | 150 | 300 |
| Steel | 80 | 350 |
| Transport | 100 | 280 |

We want to maximize the total manpower utilization i.e. employment, over the planning horizon while meeting an exogenous consumption requirement in every year:

* 60 million dollars of coal.
* 60 million dollars of steel.
* 30 million dollars  of transport.

## Model Formulation
A widely used type of national economic model is the input–output model representing the interrelationships between the different sectors of a country’s economy. Such models are often referred to as dynamic Leontief models after their originator, who built such a model of the American economy. A similar model has been considered by Wagner (1957).

### Sets and indices

$i,j \in \text{Industries}=\{\text{coal}, \text{steel}, \text{transport}\}$

$ t \in \text{Horizon} = \{\text{year1}, \text{year2}, \text{year3}, \text{year4}, \text{year5}, \text{year6} \}$

$ t \in \text{fiveYears} = \{\text{year1}, \text{year2}, \text{year3}, \text{year4}, \text{year5} \}$

$t \in H_{2,4} = \{year2, year3, year4 \} $

### Parameters

$\text{demand}_{j} \in \mathbb{R}^+$: Exogenous demand of goods in industry $j$.

$\text{initial_stock}_{j} \in \mathbb{R}^+$: Stocks of goods in industry $j$ available at the beginning of year 1.

$\text{in_out_prod}_{i,j} \in \mathbb{R}^+$: Input of good $i$ required to produce one unit of good $j$ in the following year.

$\text{in_out_cap}_{i,j} \in \mathbb{R}^+$: Input of good $i$ in current year results in a permanent increase in productive capacity of good $j$  two years later.

$\text{industry_cap}_{j} \in \mathbb{R}^+$: Industry $j$  productive capacity at the beginning of year 1.

$\text{labor_prod}_{j} \in \mathbb{R}^+$: Labor requirements for the production of goods in industry  $j$.

$\text{labor_extra_cap}_{j} \in \mathbb{R}^+$: Labor requirements to permanently increase capacity for goods in industry $j$.

### Decision Variables

$\text{production}_{j,t} \in \mathbb{R}^+$: Production of goods in industry $j$ available at year $t$, in millions of dollars.
Note: goods of industry j, in millions of dollars, available in year t but produced in previous year.

$\text{stock}_{j,t} \in \mathbb{R}^+$: Stock level of industry $j$ at the end of year $t$, in millions of dollars.

$\text{extra_cap}_{j,t} \in \mathbb{R}^+$: Extra capacity for industry   $j$ becoming effective at the beginning of year $t$, in millions of dollars.

In order to build a realistic model, we need to think beyond year 5. Therefore, we make the following assumptions:

* External demand is constant up to year 5 and beyond.
* Stock level remains constant at year 5 and beyond.
* Capacity cannot be increased after year 5.

Consequently, for year 6 and beyond, we can assume a static model of the economy. For this static model, the production of goods in each industry $i \in \text{Industries}$ can be computed as follows.

\begin{equation}
x_{i} = \text{demand}_{i} + \sum_{j \in \text{Industries} } \text{in_out_prod}_{i,j} * x_{j}
\end{equation}

Where $x_{i}$ is the production of goods in industry $i$.

Solving this system of equations gives lower bounds for the production of goods in years beyond year 5. Then, the production of goods of  industry $j \in \text{Industries}$ in year 6 and beyond can be defined by the following constraints.

$$
\text{production}_{j,t} \geq x_{j} \quad \forall j \in \text{Industries}, \; t=6.
$$

$$
\text{extra_cap}_{j,t} = 0 \quad \forall j \in \text{Industries}, \; t=6.
$$

### Constraints

**Balance equation year 1**: The initial stocks in industry $i$ should be equal to the total demand (internal and external), plus the extra capacity to build, plus the stock level at the end of year 1 of the goods in that industry.

\begin{equation}
\text{initial_stock}_{i} = \sum_{j \in \text{Industries} } \text{in_out_prod}_{i,j}*\text{production}_{j,2}
+ \text{demand}_{i} + \sum_{j \in \text{Industries} } \text{in_out_cap}_{i,j}*\text{extra_cap}_{j,3} +
 \text{stock}_{i,1}
\end{equation}

**Balance equation for year 2, year 3, year 4**: The production of goods in industry $i$ at year t plus the stocks at the end of year t-1 should be equal to the total demand (internal and external), plus the extra capacity to build, plus the stock level at the end of year t of the goods in that industry.

\begin{equation}
\text{production}_{j,t} + \text{stock}_{i,t-1} =
\sum_{j \in \text{Industries} } \text{in_out_prod}_{i,j}*\text{production}_{j,t+1} + \text{demand}_{i} +
\sum_{j \in \text{Industries} } \text{in_out_cap}_{i,j}*\text{extra_cap}_{j,t+2}
+ \text{stock}_{i,t} \quad \forall t \in H_{2,4}
\end{equation}

**Balance equation year 5**: The production of goods in industry $i$ plus the stocks at the end of year 4 should be equal to the total demand (internal and external), plus the stock level at the end of year 5 of the goods in that industry.

\begin{equation}
\text{production}_{j,5} + \text{stock}_{i,4} =
\sum_{j \in \text{Industries} } \text{in_out_prod}_{i,j}*\text{production}_{j,6}  +
\text{demand}_{i} + \text{stock}_{i,5}
\end{equation}

**End of horizon constraints**:

$$
\text{production}_{j,t} \geq x_{j} \quad \forall j \in \text{Industries}, \; t=6
$$

$$
\text{extra_cap}_{j,t} = 0 \quad \forall j \in \text{Industries}, \; t=6
$$

Where $x_{j}$ is the solution of the static model.

**Productive capacity constraints**: These constraints ensure that the production of goods for each industry $j$ during the planning horizon do not exceed the total production capacity at that year.

\begin{equation}
\text{production}_{j,t} \leq \text{base_cap}_{j} + \sum_{\tau \leq t} \text{extra_cap}_{j,\tau} \quad \forall t \in \text{Horizon}
\end{equation}

### Objective Function

**Labor utilization**: Maximize labor employment.

\begin{equation}
\text{Maximize} \quad Z =
\sum_{t \in \text{fiveYears} } \sum_{j \in \text{Industries} } \text{labor_prod}_{j}*\text{production}_{j,t} +
\sum_{t \in \text{fiveYears} } \sum_{j \in \text{Industries} } \text{labor_extra_cap}_{j}*\text{extra_cap}_{j,t}
\end{equation}

---
## Python Implementation

We import the Gurobi Python Module and other Python libraries.

```python
import numpy as np
import pandas as pd
from itertools import product

import gurobipy as gp
from gurobipy import GRB

# tested with Python 3.7.0 & Gurobi 9.1.0
```

## Input Data
We define all the input data for the model.

```python
# input-output matrix for the production of goods of each industry

arcs, inout_prod = gp.multidict({
    ('coal', 'coal'): 0.1,
    ('coal', 'steel'): 0.5,
    ('coal', 'transport'): 0.4,
    ('steel', 'coal'): 0.1,
    ('steel', 'steel'): 0.1,
    ('steel', 'transport'): 0.2,
    ('transport', 'coal'): 0.2,
    ('transport', 'steel'): 0.1,
    ('transport', 'transport'): 0.2
})

# Labor requirements for the production of goods of each industry
labor_prod = dict({'coal': 0.6,
                   'steel': 0.3,
                   'transport': 0.2})

# input-output matrix to create extra capacity for each industry

arcs, inout_cap = gp.multidict({
    ('coal', 'coal'): 0.1,
    ('coal', 'steel'): 0.7,
    ('coal', 'transport'): 0.9,
    ('steel', 'coal'): 0.1,
    ('steel', 'steel'): 0.1,
    ('steel', 'transport'): 0.2,
    ('transport', 'coal'): 0.2,
    ('transport', 'steel'): 0.1,
    ('transport', 'transport'): 0.2
})

# Labor requirements to increase capacity for each industry
labor_extra_cap = dict({'coal': 0.4,
                  'steel': 0.2,
                  'transport': 0.1})

# Initial stock, initial capacity, and demand of goods for each industry

industries, stock0, capacity0, demand = gp.multidict({
    'coal': [250,300,60],
    'steel': [180,350,60],
    'transport': [200,280,30]
})

# Time Horizons
horizon = [1,2,3,4,5,6]
fiveYears = [1,2,3,4,5]
years2_4 = [2,3,4]

# Computed parameters
i2h = set(product(industries, horizon))
i2f = set(product(industries, fiveYears))
```

## Preprocessing

We assume a static model of the economy, for year 6 and beyond. This static model is defined by a system of equations. The decision variables are the production of goods of each industry for the static model of the economy. We formulate and solve this model.

```python
static = gp.Model('StaticModel')

static_prod = static.addVars(industries, name="static_prod")

# Static model balance equations

static_balance = static.addConstrs(
    (static_prod[i] - gp.quicksum(inout_prod[i,j]*static_prod[j] for j in industries)
     == demand[i] for i in industries  ), name='static_balance' )

# we define a constant objective function to solve the system of balance equations for the static model.

static.setObjective(0)

# Verify model formulation

static.write("StaticModel.lp")

# Run optimization engine

static.optimize()

# Print solution of static model

print("\n\n_________________________________________________________________________________")
print(f"The production of goods by industry for the static model of the economy is:")
print("_________________________________________________________________________________")
for i in industries:
    if (static_prod[i].x > 1e-6):
        dollars_static_prod = '${:,.2f}'.format(static_prod[i].x)
        print(f"Generate {dollars_static_prod} million dollars of {i} ")
```

    Using license file c:\gurobi\gurobi.lic
    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)
    Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
    Optimize a model with 3 rows, 3 columns and 9 nonzeros
    Model fingerprint: 0x5b989667
    Coefficient statistics:
      Matrix range     [1e-01, 9e-01]
      Objective range  [0e+00, 0e+00]
      Bounds range     [0e+00, 0e+00]
      RHS range        [3e+01, 6e+01]
    Presolve removed 3 rows and 3 columns
    Presolve time: 0.05s
    Presolve: All rows and columns removed
    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    0.0000000e+00   0.000000e+00   0.000000e+00      0s

    Solved in 0 iterations and 0.05 seconds
    Optimal objective  0.000000000e+00

    _________________________________________________________________________________
    The production of goods by industry for the static model of the economy is:
    _________________________________________________________________________________
    Generate $166.40 million dollars of coal
    Generate $105.67 million dollars of steel
    Generate $92.31 million dollars of transport

## Model Deployment

We create a model and the variables. For each year in the planning horizon and industry, the variables capture the production of goods, the stock level of goods, and the increase in production capacity.

```python
model = gp.Model('EconomicPlanning')

# Decision variables
production = model.addVars(i2h, ub=capacity0, name="production")

# There is no production at year 1
model.setAttr('ub', production.select('*',1), 0)

stock = model.addVars(i2f, name="stock")
extra_cap = model.addVars(i2h, name="extra_cap")

# No extra capacity can be generated at years 1 and 2
for j,t in i2h:
    if t < 3:
        extra_cap[j,t].ub = 0
```

The first constraint is a balance equation for year 1 of the planning horizon. The initial stocks in industry  $i$  should be equal to the total demand (internal and external), plus the extra capacity to build, plus the stock level at the end of year 1 of the goods in that industry.

```python
# Year 1 balance equations

balance1 = model.addConstrs( ( stock0[i] == gp.quicksum(inout_prod[i,j]*production[j,2] for j in industries)
                              + gp.quicksum(inout_cap[i,j]*extra_cap[j,3] for j in industries )
                              + demand[i] + stock[i,1] for i in industries ), name='balance1' )
```

The following constraints are balance equations for years 2,3, and 4. The production of goods in industry  i  at year t plus the stocks at the end of year t-1 should be equal to the total demand (internal and external), plus the extra capacity to build, plus the stock level at the end of year t of the goods in that industry.

```python
# Balance equations for years 2,3, and 4

balance_t = model.addConstrs(( production[i, year] + stock[i,year-1]  ==
                              gp.quicksum(inout_prod[i,j]*production[j, year + 1] for j in industries)
                              + gp.quicksum(inout_cap[i,j]*extra_cap[j, year + 2] for j in industries )
                              + demand[i] + stock[i, year] for i in industries for year in years2_4 ), name='balance_t' )
```

The constraint for year 5 ensures that the production of goods in industry  i  plus the stocks at the end of year 4 should be equal to the total demand (internal and external), plus the stock level at the end of year 5 of the goods in that industry.

```python
# Balance equations for year 5

balance5 = model.addConstrs( (production[i, 5] + stock[i,4]  ==
                              gp.quicksum(inout_prod[i,j]*production[j,6] for j in industries)
                              + demand[i] + stock[i,5] for i in industries ), name='balance5' )
```

We need to enforce the constraints of a static model of the economy for year 6 and beyond.
* Production of goods at each industry should be larger or equal to the industry production of the static model of the economy.

* There is no increased capacity for year 6 and beyond.

```python
# Steady state production for year 6 and beyond

steadyProduction = model.addConstrs((production[j,6] - static_prod[j].x >= 0 for j in industries ), name='steadyProduction')

# Zero increased capacity for year 6 and beyond
for j,t in i2h:
    if t == 6:
        extra_cap[j,t].ub = 0
```

The productive capacity constraints ensure that the production of goods for each industry during the planning horizon do not exceed the total production capacity at that year.

```python
# Productive capacity constraints

capacityConstr = model.addConstrs(
    (production[industry, year] - gp.quicksum(extra_cap[industry,t] for t in fiveYears if t <= year)
     <= capacity0[industry] for industry,year in i2f ), name='capacityConstr' )
```

The objective is to maximize labor employment.

```python
# Maximize employment
model.setObjective(
    (gp.quicksum(labor_prod[j]*production[j,t] for j in industries for t in fiveYears)
     + gp.quicksum(labor_extra_cap[j]*extra_cap[j,t] for j in industries for t in fiveYears) ), GRB.MAXIMIZE)
```

```python
# Verify model formulation

model.write('DynamicModel.lp')

# Run optimization engine

model.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)
    Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
    Optimize a model with 33 rows, 51 columns and 183 nonzeros
    Model fingerprint: 0x194e5768
    Coefficient statistics:
      Matrix range     [1e-01, 1e+00]
      Objective range  [1e-01, 6e-01]
      Bounds range     [0e+00, 0e+00]
      RHS range        [3e+01, 4e+02]
    Presolve removed 9 rows and 18 columns
    Presolve time: 0.01s
    Presolved: 24 rows, 33 columns, 126 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    2.5258351e+04   7.671320e+03   0.000000e+00      0s
          25    1.9022169e+03   0.000000e+00   0.000000e+00      0s

    Solved in 25 iterations and 0.01 seconds
    Optimal objective  1.902216910e+03

---
## Analysis

The objective of maximizing employment results in a total manpower utilization of $\$1,902.22 $ millions. With this objective, the coal industry is boosted in view of its heavy labor requirements.

```python
# Output reports

print("_______________________________________________________________________________________________")
print(f"The production of goods by industry and year for the dynamic Leontief model of the economy is:")
print("_______________________________________________________________________________________________")

goods = {}

for i in industries:
    my_list = []
    for t in fiveYears:
        my_list.append('${:,.2f}'.format( production[i,t ].x ) )
    goods[i] = my_list

goods_production = pd.DataFrame(goods, index=["Year1", "Year2", "Year3", "Year4", "Year5"])
goods_production
```

    _______________________________________________________________________________________________
    The production of goods by industry and year for the dynamic Leontief model of the economy is:
    _______________________________________________________________________________________________

      coal
      steel
      transport

      Year1
      $0.00
      $0.00
      $0.00

      Year2
      $300.00
      $136.78
      $211.30

      Year3
      $370.87
      $231.91
      $217.39

      Year4
      $368.03
      $209.57
      $275.67

      Year5
      $961.06
      $350.00
      $92.31

```python
print("_______________________________________________________________________________________________")
print(f"The productive capacity by industry and year for the dynamic Leontief model of the economy is:")
print("_______________________________________________________________________________________________")

# Compute cummulative capacity
totalCap = {}

for i in industries:
    amount = capacity0[i]
    my_list = []
    for t in fiveYears:
        amount += extra_cap[i,t].x
        my_list.append(round(amount))
    totalCap[i] = my_list

extra_capacity = pd.DataFrame(totalCap, index=["Year1", "Year2", "Year3", "Year4", "Year5"])
extra_capacity
```

    _______________________________________________________________________________________________
    The productive capacity by industry and year for the dynamic Leontief model of the economy is:
    _______________________________________________________________________________________________

      coal
      steel
      transport

      Year1
      300
      350
      280

      Year2
      300
      350
      280

      Year3
      371
      350
      280

      Year4
      371
      350
      280

      Year5
      961
      350
      280

```python
print("____________________________________________________________________________________")
print(f"Stock level by industry at the end of year for the dynamic Leontif model of the economy is:")
print("____________________________________________________________________________________")

inv = {}

for i in industries:
    my_list = []
    for t in fiveYears:
        my_list.append('${:,.2f}'.format( stock[i,t ].x ) )
    inv[i] = my_list

stock_level = pd.DataFrame(inv, index=["Year1", "Year2", "Year3", "Year4", "Year5"])
stock_level
```

    ____________________________________________________________________________________
    Stock level by industry at the end of year for the dynamic Leontif model of the economy is:
    ____________________________________________________________________________________

      coal
      steel
      transport

      Year1
      $0.00
      $26.97
      $39.89

      Year2
      $0.00
      $0.00
      $80.34

      Year3
      $0.00
      $0.00
      $0.00

      Year4
      $0.00
      $0.00
      $0.00

      Year5
      $794.66
      $244.33
      $0.00

## References

H. Paul Williams, Model Building in Mathematical Programming, fifth edition.

Wagner, H.M. (1957) A linear programming solution to dynamic Leontief type models. Management Science, 3, 234–254.

Copyright © 2020 Gurobi Optimization, LLC