```python
from ngames.evaluation.extensivegames import ExtensiveFormGame, plot_game,\
    subgame_perfect_equilibrium, DFS_equilibria_paths
```

# Market game

From S. Fatima, S. Kraus, M. Wooldridge, Principles of Automated Negotiation,
Cambridge University Press, 2014, see Figure 3.3.

```python
m = ExtensiveFormGame(title='Market game')
m.add_players('firm 1', 'firm 2')
m.add_node(1, 'chance', is_root=True)
m.add_node(2, 'firm 1')
m.add_node(3, 'firm 1')
for i in range(4, 8):
    m.add_node(i, 'firm 2')
for i in range(8, 16):
    m.add_node(i)

m.add_edge(1, 2, label='small')
m.add_edge(1, 3, label='large')
m.add_edge(2, 4, label='L')
m.add_edge(2, 5, label='H')
m.add_edge(3, 6, label='L')
m.add_edge(3, 7, label='H')
m.add_edge(4, 8, label='L')
m.add_edge(4, 9, label='H')
m.add_edge(5, 10, label='L')
m.add_edge(5, 11, label='H')
m.add_edge(6, 12, label='L')
m.add_edge(6, 13, label='H')
m.add_edge(7, 14, label='L')
m.add_edge(7, 15, label='H')

m.set_information_partition('firm 2', {4, 6}, {5, 7})
m.set_information_partition('firm 1', {2, 3})

m.set_uniform_probability_distribution(1)

m.set_utility(8, {'firm 1': 16, 'firm 2': 8})
m.set_utility(9, {'firm 1': 8, 'firm 2': 16})
m.set_utility(10, {'firm 1': 20, 'firm 2': 4})
m.set_utility(11, {'firm 1': 16, 'firm 2': 8})
m.set_utility(12, {'firm 1': 30, 'firm 2': 10})
m.set_utility(13, {'firm 1': 28, 'firm 2': 12})
m.set_utility(14, {'firm 1': 16, 'firm 2': 24})
m.set_utility(15, {'firm 1': 24, 'firm 2': 16})

position_colors = {'firm 1': 'cyan', 'firm 2': 'red'}

my_fig_kwargs = dict(figsize=(12, 12), frameon=False)
my_node_kwargs = dict(font_size=24, node_size=2000, edgecolors='k',
                      linewidths=3)
my_edge_kwargs = dict(arrowsize=25, width=5)
my_edge_labels_kwargs = dict(font_size=24)
my_patch_kwargs = dict(linewidth=3)
my_legend_kwargs = dict(fontsize=24, loc='upper right', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=20)
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)

fig = plot_game(m,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.075,
                info_sets_kwargs=my_info_sets_kwargs)
```

# Prisoner's Dilemma

Classical one-shot Prioner's Dilemma, but in extensive form.

```python
pd = ExtensiveFormGame(name='Prisoners Dilemma')
pd.add_players('player 1', 'player 2')
pd.add_node(1, 'player 1', is_root=True)
pd.add_node(2, 'player 2')
pd.add_node(3, 'player 2')
for i in range(4, 8):
    pd.add_node(i)

pd.add_edge(1, 2, 'cooperate')
pd.add_edge(1, 3, 'defect')
pd.add_edge(2, 4, 'cooperate')
pd.add_edge(2, 5, 'defect')
pd.add_edge(3, 6, 'cooperate')
pd.add_edge(3, 7, 'defect')

pd.set_information_partition('player 2', {2, 3})

pd.set_utility(4, {'player 1': 6, 'player 2': 6})
pd.set_utility(5, {'player 1': 0, 'player 2': 9})
pd.set_utility(6, {'player 1': 9, 'player 2': 0})
pd.set_utility(7, {'player 1': 3, 'player 2': 3})

position_colors = {'player 1': 'aquamarine', 'player 2': 'lightcoral'}
my_fig_kwargs = dict(figsize=(12, 12), tight_layout=True)
my_node_kwargs = dict(font_size=36, node_size=4500, edgecolors='k',
                      linewidths=3)
my_edge_kwargs = dict(arrowsize=50, width=5)
my_edge_labels_kwargs = dict(font_size=32)
my_patch_kwargs = dict(linewidth=3)
my_legend_kwargs = dict(fontsize=32, loc='upper left', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=28,
                               weight='bold')
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)
fig = plot_game(pd,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                decimals=0,
                utility_label_shift=0.06,
                info_sets_kwargs=my_info_sets_kwargs)
```

# Example of information rules

```python
example = ExtensiveFormGame()
example.add_players('position 1', 'position 2')
example.add_node(1, 'position 1', is_root=True)
example.add_node(2, 'position 2')
example.add_node(3, 'position 2')
example.add_edge(1, 2, label='do A')
example.add_edge(1, 3, label='do B')

example.set_information_partition('position 2', {2, 3})

position_colors = {'position 1': 'green', 'position 2': 'cyan'}
my_fig_kwargs['figsize'] = (6, 6)
my_legend_kwargs['loc'] = 'upper left'
my_legend_kwargs['fontsize'] = 20
my_patch_kwargs['linewidth'] = 2
fig = plot_game(example,
                position_colors,
                utility_label_shift=0.,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                info_sets_kwargs=my_info_sets_kwargs)

```

```python
example.set_information_partition('position 2', {2}, {3})
example.is_perfect_information = True
fig = plot_game(example,
                position_colors,
                utility_label_shift=0.,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                info_sets_kwargs=my_info_sets_kwargs)
```

# Ostrom's fishing game

```python
fishing_game = ExtensiveFormGame(title='Fishing game C1')

# add the two positions for the two fishers
fishing_game.add_players('fisher 1', 'fisher 2')

# add the nodes to the graph
fishing_game.add_node(1, player_turn='fisher 1', is_root=True)
fishing_game.add_node(2, player_turn='fisher 2')
fishing_game.add_node(3, player_turn='fisher 2')
fishing_game.add_node(4, player_turn='fisher 1')
fishing_game.add_node(5)
fishing_game.add_node(6)
fishing_game.add_node(7, player_turn='fisher 1')
fishing_game.add_node(8, player_turn='fisher 2')
fishing_game.add_node(9, player_turn='fisher 2')
fishing_game.add_node(10, player_turn='fisher 2')
fishing_game.add_node(11, player_turn='fisher 2')
fishing_game.add_node(12, player_turn='chance')
fishing_game.add_node(13)
fishing_game.add_node(14)
fishing_game.add_node(15, player_turn='chance')
fishing_game.add_node(16, player_turn='chance')
fishing_game.add_node(17)
fishing_game.add_node(18)
fishing_game.add_node(19, player_turn='chance')
for i in range(20, 28):
    fishing_game.add_node(i)

# add the edges to the graph
fishing_game.add_edge(1, 2, label='go to spot 1')
fishing_game.add_edge(1, 3, label='go to spot 2')
fishing_game.add_edge(2, 4, label='go to spot 1')
fishing_game.add_edge(2, 5, label='go to spot 2')
fishing_game.add_edge(3, 6, label='go to spot 1')
fishing_game.add_edge(3, 7, label='go to spot 2')
fishing_game.add_edge(4, 8, label='stay')
fishing_game.add_edge(4, 9, label='leave')
fishing_game.add_edge(7, 10, label='stay')
fishing_game.add_edge(7, 11, label='leave')

fishing_game.add_edge(8, 12, label='stay')
fishing_game.add_edge(8, 13, label='leave')
fishing_game.add_edge(9, 14, label='stay')
fishing_game.add_edge(9, 15, label='leave')
fishing_game.add_edge(10, 16, label='stay')
fishing_game.add_edge(10, 17, label='leave')
fishing_game.add_edge(11, 18, label='stay')
fishing_game.add_edge(11, 19, label='leave')

fishing_game.add_edge(12, 20, label='1 wins')
fishing_game.add_edge(12, 21, label='2 wins')
fishing_game.add_edge(15, 22, label='1 wins')
fishing_game.add_edge(15, 23, label='2 wins')
fishing_game.add_edge(16, 24, label='1 wins')
fishing_game.add_edge(16, 25, label='2 wins')
fishing_game.add_edge(19, 26, label='1 wins')
fishing_game.add_edge(19, 27, label='2 wins')

# add imperfect information, equivalent to having players take the actions
# simultaneously
fishing_game.set_information_partition('fisher 2', {2, 3}, {8, 9}, {10, 11})
```

## Rule configuration C1 (default)

See page 85 in E. Ostrom, R. Gardner, J. Walker, Rules, Games, and
Common-Pool Resources, The University of Michigan Press, Ann Arbor, 1994.
https://doi.org/10.3998/mpub.9739.

First, build the default situation as an extensive-form game. Two fishers
competing for two fishing spots, the first spot being better than the other.

The utilities and probability at chance nodes of the game are parametrized
with the following variables:

* $v_i$: economical value of the $i$-th spot. It is assumed that the first
spot is the better one, so $v_1>v_2$.

* $P$: the probability that fisher 1 wins the fight if one happens. It is
assumed that fisher 1 is the stronger one, so $P>0.5$. Then, the probability
that fisher 2 wins a fight is $(1-P)<0.5$.

* $c$: cost of travel between the two spots.

* $d$: damage incurred to the loser of the fight.

$w(j, i)$ denotes the *expected* value for fisher $j$ of having a fight at
spot $i$:

\begin{align}
    w(1,i) =& Pv_i+(1-P)(-d)\\
    w(2,i) =& (1-P)v_i+P(-d)
\end{align}

Start assigning utilities with made-up values.

```python
# parameters
v1, v2 = 5, 3
P = 0.6
c = 0.5
d = 2

def set_parameters(v1: float, v2: float, P: float, c: float, d: float,
                   game: ExtensiveFormGame):

    w11 = P*v1+(1-P)*(-d)
    w12 = P*v2+(1-P)*(-d)
    w21 = (1-P)*v1+P*(-d)
    w22 = (1-P)*v2+P*(-d)
    # set utility parameters and probabilities over outgoing edges at
    # chance nodes
    game.set_utility(5, {'fisher 1': v1, 'fisher 2': v2})
    game.set_utility(6, {'fisher 1': v2, 'fisher 2': v1})
    game.set_utility(13, {'fisher 1': v1, 'fisher 2': v2-c})
    game.set_utility(14, {'fisher 1': v2-c, 'fisher 2': v1})
    game.set_utility(17, {'fisher 1': v2, 'fisher 2': v1-c})
    game.set_utility(18, {'fisher 1': v2-c, 'fisher 2': v1})
    game.set_probability_distribution(12, {(12, 20): P, (12, 21): (1-P)})
    game.set_probability_distribution(15, {(15, 22): P, (15, 23): (1-P)})
    game.set_probability_distribution(16, {(16, 24): P, (16, 25): (1-P)})
    game.set_probability_distribution(19, {(19, 26): P, (19, 27): (1-P)})
    game.set_utility(20, {'fisher 1': v1, 'fisher 2': -d})
    game.set_utility(21, {'fisher 1': -d, 'fisher 2': v1})
    game.set_utility(22, {'fisher 1': v2-c, 'fisher 2': -c-d})
    game.set_utility(23, {'fisher 1': -c-d, 'fisher 2': v2-c})
    game.set_utility(24, {'fisher 1': v2, 'fisher 2': -d})
    game.set_utility(25, {'fisher 1': -d, 'fisher 2': v2})
    game.set_utility(26, {'fisher 1': v1-c, 'fisher 2': -c-d})
    game.set_utility(27, {'fisher 1': -c-d, 'fisher 2': v1-c})
    return (w11, w12), (w21, w22)

_ = set_parameters(v1, v2, P, c, d, fishing_game)

# default keywords for rendering the figure
my_fig_kwargs = dict(figsize=(45, 24), frameon=False)
my_node_kwargs = dict(font_size=30, node_size=2250, edgecolors='k',
                      linewidths=2)
my_edge_kwargs = dict(arrowsize=25, width=3)
my_edge_labels_kwargs = dict(font_size=20)
my_patch_kwargs = dict(linewidth=2)
my_legend_kwargs = dict(fontsize=24, loc='upper right', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=24)
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)

position_colors = {'fisher 1': 'aquamarine', 'fisher 2': 'greenyellow'}

fig = plot_game(fishing_game,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.07,
                info_sets_kwargs=my_info_sets_kwargs)
```

Compute the subgame perfect equilibria strategy:

```python
spe = subgame_perfect_equilibrium(fishing_game)
```

Compute the paths of play that arise from following the subgame perfect
equilibria strategy, alongside with the probability of being played. It is
necessary to include probabilities since there are chance nodes:

```python
path_store = []
DFS_equilibria_paths(fishing_game, fishing_game.game_tree.root, spe, [], 1,
                     path_store)
print("Path -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

    Path -- Probability
    -------------------
    [1, 'go to spot 1', 2, 'go to spot 2', 5] -- 1.00

In order to introduce the later changes to the game in the other rule
configurations, establish that the utility at the terminal nodes can be
computed as:

*Utility* = (*payoff of the outcome*) + (*cost of the path of actions to
get there*)

__Costs assigned to actions__:

| Action       | Cost     |
|--------------|----------|
| Go to spot 1 | 0        |
| Go to spot 2 | 0        |
| Stay         | 0        |
| Leave        | -c (c>0) |

__Costs assigned to payoffs__:

| Outcome | Payoff |
|---------|--------|
| Fish at spot 1 | $v_1$ |
| Fish at spot 2 | $v_2$ |
| Looser of fight* | -d (d>0) |

*: in more general terms, a fight can be encompassed under the term
"competition". This will make the transition from C1 to C2 easier.

To reproduce the analysis of the game by Ostrom in an automated way, I use
backward induction. This is not technically correct because the game is
imperfect information, but for the moment the focus is not so much on the
correctness of the solution concept.

The first three cases fulfill $$w(1, 1)>v_2-c$$ and $w(2, 1)>v_2-c$:

* Case 1: $w(1,1)>v_2;\;w(2,1)>v_2$

* Case 2: $w(1,1)>v_2;\;w(2,1)<v_2$

* Case 3: $w(1,1)<v_2;\;w(2,1)<v_2$

The other two cases are:

* Case 4: $w(1,1)>v_2-c;\;w(2,1)<v_2-c$

* Case 5: $w(1,1)<v_2-c;\;w(2,1)<v_2-c$

### Case C1-1

$w(1,1)>v_2;\;w(2,1)>v_2$

As predicted by Ostrom, in the equilibrium both fishers go to spot 1,
stay there and fight.

```python
v1, v2 = 10, 2
P = 0.55
c = 1
d = 2

(w11, w12), (w21, w22) = set_parameters(v1, v2, P, c, d, fishing_game)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2 = {:.1f}".format(v2))

spe = subgame_perfect_equilibrium(fishing_game)
path_store = []
DFS_equilibria_paths(fishing_game, fishing_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

    w(1,1) = 4.6
    w(2,1) = 3.4
    v2 = 2.0

    Path -- Probability
    -------------------
    [1, 'go to spot 1', 2, 'go to spot 1', 4, 'stay', 8, 'stay', 12, '1 wins', 20] -- 0.55
    [1, 'go to spot 1', 2, 'go to spot 1', 4, 'stay', 8, 'stay', 12, '2 wins', 21] -- 0.45

### Case C1-2

$w(1,1)>v_2;\;w(2,1)<v_2$

As predicted by Ostrom, in the equilibrium the stronger fisher 1 ($P>0.5$)
goes to spot 1, the weaker fisher 2 goes to spot 2.

```python
v1, v2 = 10, 4
P = 0.6
c = 1
d = 2

(w11, w12), (w21, w22) = set_parameters(v1, v2, P, c, d, fishing_game)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2 = {:.1f}".format(v2))

spe = subgame_perfect_equilibrium(fishing_game)
path_store = []
DFS_equilibria_paths(fishing_game, fishing_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

### Case C1-3

$w(1,1)<v_2;\;w(2,1)<v_2$

As predicted by Ostrom, again in the equilibrium the stronger fisher 1
($P>0.5$) goes to spot 1, the weaker fisher 2 goes to spot 2.

Ostrom points out that there is another pure strategy equilibrium in which
fisher 1 goes to the worse spot 2, and fisher 2 goes to the better spot 1.
However, that outcome is part of a Nash equilibrium that is not sequentially
rational (?).

```python
v1, v2 = 6, 4
P = 0.6
c = 1
d = 2

(w11, w12), (w21, w22) = set_parameters(v1, v2, P, c, d, fishing_game)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2 = {:.1f}".format(v2))

spe = subgame_perfect_equilibrium(fishing_game)
path_store = []
DFS_equilibria_paths(fishing_game, fishing_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

### Case C1-4

$w(1,1)>v_2-c;\;w(2,1)<v_2-c$

As predicted by Ostrom, again in the equilibrium the stronger fisher 1 goes
to spot 1, the weaker fisher 2 goes to spot 2.

```python
v1, v2 = 10, 5
P = 0.6
c = 1
d = 2

(w11, w12), (w21, w22) = set_parameters(v1, v2, P, c, d, fishing_game)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2-c = {:.1f}".format(v2-c))

spe = subgame_perfect_equilibrium(fishing_game)
path_store = []
DFS_equilibria_paths(fishing_game, fishing_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

### Case C1-5

$w(1,1)<v_2-c;\;w(2,1)<v_2-c$

As predicted by Ostrom, again in the equilibrium the stronger fisher 1 goes
to spot 1, the weaker fisher 2 goes to spot 2.

```python
v1, v2 = 7, 5
P = 0.6
c = 1
d = 2

(w11, w12), (w21, w22) = set_parameters(v1, v2, P, c, d, fishing_game)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2-c = {:.1f}".format(v2-c))

spe = subgame_perfect_equilibrium(fishing_game)
path_store = []
DFS_equilibria_paths(fishing_game, fishing_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

## Rule configuration C2

First fisher in arrival has the right to fish at that spot for the remaining
of the day.

```python
fishing_game_C2 = ExtensiveFormGame(title='Fishing game C2')

# add the two positions for the two fishers
fishing_game_C2.add_players('fisher 1', 'fisher 2')

# add the nodes to the graph
fishing_game_C2.add_node(1, player_turn='fisher 1', is_root=True)
fishing_game_C2.add_node(2, player_turn='fisher 2')
fishing_game_C2.add_node(3, player_turn='fisher 2')
fishing_game_C2.add_node(4, player_turn='chance')
fishing_game_C2.add_node(5)
fishing_game_C2.add_node(6)
fishing_game_C2.add_node(7, player_turn='chance')
fishing_game_C2.add_node(8, player_turn='fisher 2')
fishing_game_C2.add_node(9, player_turn='fisher 1')
fishing_game_C2.add_node(10, player_turn='fisher 2')
fishing_game_C2.add_node(11, player_turn='fisher 1')
for i in range(12, 15+1):
    fishing_game_C2.add_node(i)

# add the edges to the graph
fishing_game_C2.add_edge(1, 2, label='go to spot 1')
fishing_game_C2.add_edge(1, 3, label='go to spot 2')
fishing_game_C2.add_edge(2, 4, label='go to spot 1')
fishing_game_C2.add_edge(2, 5, label='go to spot 2')
fishing_game_C2.add_edge(3, 6, label='go to spot 1')
fishing_game_C2.add_edge(3, 7, label='go to spot 2')
fishing_game_C2.add_edge(4, 8, label='1 first')
fishing_game_C2.add_edge(4, 9, label='2 first')
fishing_game_C2.add_edge(7, 10, label='1 first')
fishing_game_C2.add_edge(7, 11, label='2 first')
fishing_game_C2.add_edge(8, 12, label='leave')
fishing_game_C2.add_edge(9, 13, label='leave')
fishing_game_C2.add_edge(10, 14, label='leave')
fishing_game_C2.add_edge(11, 15, label='leave')

# add imperfect information, equivalent to having players take the actions
# simultaneously
fishing_game_C2.set_information_partition('fisher 2', {2, 3}, {8}, {10})
```

The utilities and probability at chance nodes of the game are parametrized
with the following variables:

* $v_i$: economical value of the $i$-th spot. It is assumed that the first
spot is the better one, so $v_1>v_2$.

* $P$: the probability that fisher 1 gets to a spot first. It is assumed
that fisher 1 has better technology than fisher 2, and hence $P>0.5$.

* $c$: cost of travel between the two spots.

$w(j, i)$ denotes the *expected* value for fisher $j$ of spot $i$ if the
other fisher has gone there too:

\begin{align}
    w(1,i) =& Pv_i+(1-P)(v_{-i}-c)\\
    w(2,i) =& (1-P)v_i+P(v_{-i}-c)
\end{align}

```python
# parameters
v1, v2 = 5, 3
P = 0.6
c = 0.5

def set_parameters_C2(v1: float, v2: float, P: float, c: float,
                      game: ExtensiveFormGame):
    w11 = P*v1+(1-P)*(v2-c)
    w12 = P*v2+(1-P)*(v1-c)
    w21 = (1-P)*v1+P*(v2-c)
    w22 = (1-P)*v2+P*(v1-c)
    # set utility parameters and probabilities over outgoing edges at
    # chance nodes
    game.set_utility(5, {'fisher 1': v1, 'fisher 2': v2})
    game.set_utility(6, {'fisher 1': v2, 'fisher 2': v1})
    game.set_utility(12, {'fisher 1': v1, 'fisher 2': v2-c})
    game.set_utility(13, {'fisher 1': v2-c, 'fisher 2': v1})
    game.set_utility(14, {'fisher 1': v2, 'fisher 2': v1-c})
    game.set_utility(15, {'fisher 1': v1-c, 'fisher 2': v2})
    game.set_probability_distribution(4, {(4, 8): P, (4, 9): (1-P)})
    game.set_probability_distribution(7, {(7, 10): P, (7, 11): (1-P)})
    return (w11, w12), (w21, w22)

_ = set_parameters_C2(v1, v2, P, c, fishing_game_C2)

# default keywords for rendering the figure
my_fig_kwargs = dict(figsize=(30, 20), frameon=False)
my_node_kwargs = dict(font_size=30, node_size=2250, edgecolors='k',
                      linewidths=2)
my_edge_kwargs = dict(arrowsize=25, width=3)
my_edge_labels_kwargs = dict(font_size=20)
my_patch_kwargs = dict(linewidth=2)
my_legend_kwargs = dict(fontsize=24, loc='upper right', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=20)
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)

position_colors = {'fisher 1': 'aquamarine', 'fisher 2': 'greenyellow'}

fig = plot_game(fishing_game_C2,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.06,
                info_sets_kwargs=my_info_sets_kwargs)
```

**Changes to go from C1 to C2:**

* If fisher1.location == fisher2.location $\longrightarrow$ engage in a
"competition". In the game tree of C1: "shortcut edge" from node 4 to
node 12.

* Looser of fight from a final to an intermediate outcome (no longer a
terminal node).

* Change in payoff of outcome: $d>0\longrightarrow d=0$. Because fishers are
engaged in a different, non-violent type of competition, it is not costly to
lose the race. (this change might be redundant because being the looser is
now an intermediate instead of a final outcome).

* Additional edges: if fisher $i$ loses the competition $\longrightarrow$
fisher $i$ has to leave.

### Case C2-1

$w(1,1)>v_2;\;w(2,1)>v_2$

As predicted by Ostrom, in the equilibrium both fishers go to spot 1, and
let chance decide who was first.

```python
v1, v2 = 10, 2
P = 0.55
c = 1

(w11, w12), (w21, w22) = set_parameters_C2(v1, v2, P, c, fishing_game_C2)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2 = {:.1f}".format(v2))

spe = subgame_perfect_equilibrium(fishing_game_C2)
path_store = []
DFS_equilibria_paths(fishing_game_C2, fishing_game_C2.game_tree.root, spe, [],
                     1, path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

### Case C2-2

$w(1,1)>v_2;\;w(2,1)<v_2$

As predicted by Ostrom, in the equilibrium fisher 1 (the faster) goes to
spot 1, while fisher 2 (the slower) goes to spot 2.

```python
v1, v2 = 10, 5
P = 0.7
c = 3

(w11, w12), (w21, w22) = set_parameters_C2(v1, v2, P, c, fishing_game_C2)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2 = {:.1f}".format(v2))

spe = subgame_perfect_equilibrium(fishing_game_C2)
path_store = []
DFS_equilibria_paths(fishing_game_C2, fishing_game_C2.game_tree.root, spe, [],
                     1, path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

### Case C2-3

$w(1,1)<v_2;\;w(2,1)<v_2$

As predicted by Ostrom, in the equilibrium fisher 1 (the faster) goes to
spot 1, while fisher 2 (the slower) goes to spot 2.

As in case C1-3, there is another Nash equilibria in which the most
resourceful fisher 1 goes to the worse spot 2. However, that is not
predicted by backward induction, possibly because that Nash equilibria
is not sequentially rational.

```python
v1, v2 = 10, 8
P = 0.55
c = 3

(w11, w12), (w21, w22) = set_parameters_C2(v1, v2, P, c, fishing_game_C2)
print("w(1,1) = {:.1f}".format(w11))
print("w(2,1) = {:.1f}".format(w21))
print("v2 = {:.1f}".format(v2))

spe = subgame_perfect_equilibrium(fishing_game_C2)
path_store = []
DFS_equilibria_paths(fishing_game_C2, fishing_game_C2.game_tree.root, spe, [],
                     1, path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

## Rule configuration C3

Fisher 1 announces first, and he has the right to go fish wherever he wants.
If fisher 2 goes to the spot taken by fisher 1, he has to leave for the
other spot.

```python
fishing_game_C3 = ExtensiveFormGame(title='Fishing game C3')

# add the two positions for the two fishers
fishing_game_C3.add_players('fisher 1', 'fisher 2')

# add the nodes to the graph
fishing_game_C3.add_node(1, player_turn='fisher 1', is_root=True)
fishing_game_C3.add_node(2, player_turn='fisher 2')
fishing_game_C3.add_node(3, player_turn='fisher 2')
fishing_game_C3.add_node(4, player_turn='fisher 2')
fishing_game_C3.add_node(5)
fishing_game_C3.add_node(6)
fishing_game_C3.add_node(7, player_turn='fisher 2')
fishing_game_C3.add_node(8)
fishing_game_C3.add_node(9)

# add the edges to the graph
fishing_game_C3.add_edge(1, 2, label='go to spot 1')
fishing_game_C3.add_edge(1, 3, label='go to spot 2')
fishing_game_C3.add_edge(2, 4, label='go to spot 1')
fishing_game_C3.add_edge(2, 5, label='go to spot 2')
fishing_game_C3.add_edge(3, 6, label='go to spot 1')
fishing_game_C3.add_edge(3, 7, label='go to spot 2')
fishing_game_C3.add_edge(4, 8, label='leave')
fishing_game_C3.add_edge(7, 9, label='leave')
```

The utilities are parametrized with the following variables:

* $v_i$: economical value of the $i$-th spot. It is assumed that the first
spot is the better one, so $v_1>v_2$.

* $c$: cost of travel between the two spots.

```python
# parameters
v1, v2 = 5, 3
P = 0.6
c = 0.5

fishing_game_C3.set_utility(5, {'fisher 1': v1, 'fisher 2': v2})
fishing_game_C3.set_utility(6, {'fisher 1': v2, 'fisher 2': v1})
fishing_game_C3.set_utility(8, {'fisher 1': v1, 'fisher 2': v2-c})
fishing_game_C3.set_utility(9, {'fisher 1': v2, 'fisher 2': v1-c})

# default keywords for rendering the figure
my_fig_kwargs = dict(figsize=(30, 20))
my_node_kwargs = dict(font_size=30, node_size=2250, edgecolors='k',
                      linewidths=2)
my_edge_kwargs = dict(arrowsize=25, width=3)
my_edge_labels_kwargs = dict(font_size=20)
my_patch_kwargs = dict(linewidth=2)
my_legend_kwargs = dict(fontsize=24, loc='upper right', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=20)
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)

position_colors = {'fisher 1': 'aquamarine', 'fisher 2': 'greenyellow'}

fig = plot_game(fishing_game_C3,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.05,
                info_sets_kwargs=my_info_sets_kwargs)
```

**Changes to go from C1 to C3:**

* Fisher 1 announces: the information set {2,3} of fisher 2 gets split into
two information sets {2}, {3}

* If fisher$_i$.action == 'go to' fisher$_j$.location $\implies$
fisher$_i$.action $\leftarrow$ 'leave'. In terms of edges in the game tree
from C1: direct edge from 4$\longrightarrow$13 and 7$\longrightarrow$17

```python
spe = subgame_perfect_equilibrium(fishing_game_C3)
path_store = []
DFS_equilibria_paths(fishing_game_C3, fishing_game_C3.game_tree.root, spe, [],
                     1, path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

    Path -- Probability
    -------------------
    [1, 'go to spot 1', 2, 'go to spot 2', 5] -- 1.00

As predicted by Ostrom, in equilibrium the fisher who gets to announce first
(fisher 1) goes to the best spot 1, and the other fish goes to the other
spot. The second fisher will never go to the same spot that the first fisher
went to, because $v_2>v_2-c$.

## Rule configuration C4

This configuration consists of a prearranged rotation of the game in rule
configuration C3. It would consist of alternating between the game in C3 as
presented before, and the same game in C3 but with fisher 1 and fisher
swapped.

# Axelrod's norms game

```python
norms_game = ExtensiveFormGame(title='Axelrod norms game')

norms_game.add_players('i', 'j')
norms_game.add_node(1, player_turn='i', is_root=True)
norms_game.add_node(2, player_turn='chance')
norms_game.add_node(3)
norms_game.add_node(4, player_turn='j')
norms_game.add_node(5)
norms_game.add_node(6)
norms_game.add_node(7)

norms_game.add_edge(1, 2, 'defect')
norms_game.add_edge(1, 3, '~defect')
norms_game.add_edge(2, 4, 'j sees i')
norms_game.add_edge(2, 5, '~j sees i')
norms_game.add_edge(4, 6, 'punish')
norms_game.add_edge(4, 7, '~punish')

T = 3
H = -1
S = 0.6
P = -9
E = -2

norms_game.set_utility(3, {'i': 0, 'j': 0})
norms_game.set_utility(5, {'i': T, 'j': H})
norms_game.set_utility(6, {'i': P, 'j': E})
norms_game.set_utility(7, {'i': T, 'j': H})
norms_game.set_probability_distribution(2, {(2, 4): S, (2, 5): (1-S)})

# default keywords for rendering the figure
my_fig_kwargs = dict(figsize=(15, 15), frameon=False)
my_node_kwargs = dict(font_size=30, node_size=2250, edgecolors='k',
                      linewidths=2)
my_edge_kwargs = dict(arrowsize=25, width=3)
my_edge_labels_kwargs = dict(font_size=20)
my_patch_kwargs = dict(linewidth=2)
my_legend_kwargs = dict(fontsize=24, loc='upper right', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=20)
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)

position_colors = {'i': 'aquamarine', 'j': 'greenyellow'}

fig = plot_game(norms_game,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.06,
                info_sets_kwargs=my_info_sets_kwargs)
```

**CASE 1:** $E<H$
Theoretical prediction: $i$ will defect but $j$ will not punishes if she
detects $i$.

```python
print("E = {:.1f}".format(E))
print("H = {:.1f}".format(H))

spe = subgame_perfect_equilibrium(norms_game)
path_store = []
DFS_equilibria_paths(norms_game, norms_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

    E = -2.0
    H = -1.0

    Path -- Probability
    -------------------
    [1, 'defect', 2, 'j sees i', 4, '~punish', 7] -- 0.60
    [1, 'defect', 2, '~j sees i', 5] -- 0.40

**CASE 2:** $E>H$
Theoretical prediction: $i$ will not defect

```python
T = 3
H = -2
S = 0.6
P = -9
E = -1

norms_game.set_utility(3, {'i': 0, 'j': 0})
norms_game.set_utility(5, {'i': T, 'j': H})
norms_game.set_utility(6, {'i': P, 'j': E})
norms_game.set_utility(7, {'i': T, 'j': H})
norms_game.set_probability_distribution(2, {(2, 4): S, (2, 5): (1-S)})

print("E = {:.1f}".format(E))
print("H = {:.1f}".format(H))

spe = subgame_perfect_equilibrium(norms_game)
path_store = []
DFS_equilibria_paths(norms_game, norms_game.game_tree.root, spe, [], 1,
                     path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

    E = -1.0
    H = -2.0

    Path -- Probability
    -------------------
    [1, '~defect', 3] -- 1.00

## Metanorms game

```python
metanorms_game = ExtensiveFormGame(title='Axelrod metanorms game')

metanorms_game.add_players('i', 'j', 'k')
metanorms_game.add_node(1, player_turn='i', is_root=True)
metanorms_game.add_node(2, player_turn='chance')
metanorms_game.add_node(3)
metanorms_game.add_node(4, player_turn='j')
metanorms_game.add_node(5)
metanorms_game.add_node(6)
metanorms_game.add_node(7, player_turn='chance')
metanorms_game.add_node(8, player_turn='k')
metanorms_game.add_node(9)
metanorms_game.add_node(10)
metanorms_game.add_node(11)

metanorms_game.add_edge(1, 2, 'defect')
metanorms_game.add_edge(1, 3, '~defect')
metanorms_game.add_edge(2, 4, 'j sees i')
metanorms_game.add_edge(2, 5, '~j sees i')
metanorms_game.add_edge(4, 6, 'punish i')
metanorms_game.add_edge(4, 7, '~punish i')
metanorms_game.add_edge(7, 8, 'k sees j')
metanorms_game.add_edge(7, 9, '~k sees j')
metanorms_game.add_edge(8, 10, 'punish j')
metanorms_game.add_edge(8, 11, '~punish j')
```

```python
T = 3
H = -1
S = 0.6
P = -9
E = -2
P_prime = P
E_prime = E

metanorms_game.set_utility(3, {'i': 0, 'j': 0, 'k': 0})
metanorms_game.set_utility(5, {'i': T, 'j': H, 'k': H})
metanorms_game.set_utility(6, {'i': P, 'j': E, 'k': 0})
metanorms_game.set_utility(9, {'i': T, 'j': H, 'k': H})
metanorms_game.set_utility(10, {'i': T, 'j': P_prime, 'k': E_prime})
metanorms_game.set_utility(11, {'i': T, 'j': H, 'k': H})
metanorms_game.set_probability_distribution(2, {(2, 4): S, (2, 5): (1-S)})
metanorms_game.set_probability_distribution(7, {(7, 8): S, (7, 9): (1-S)})

# default keywords for rendering the figure
my_fig_kwargs = dict(figsize=(25, 25), frameon=False)
my_node_kwargs = dict(font_size=30, node_size=2250, edgecolors='k',
                      linewidths=2)
my_edge_kwargs = dict(arrowsize=25, width=3)
my_edge_labels_kwargs = dict(font_size=16)
my_patch_kwargs = dict(linewidth=2)
my_legend_kwargs = dict(fontsize=24, loc='upper right', edgecolor='white')
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=20)
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)

position_colors = {'i': 'aquamarine', 'j': 'greenyellow', 'k': 'violet'}

fig = plot_game(metanorms_game,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.09,
                info_sets_kwargs=my_info_sets_kwargs)
```

```python
spe = subgame_perfect_equilibrium(metanorms_game)
path_store = []
DFS_equilibria_paths(metanorms_game, metanorms_game.game_tree.root, spe, [],
                     1, path_store)
print("\nPath -- Probability")
print("-------------------")
for (path, prob) in path_store:
    print("{} -- {:.2f}".format(path, prob))
```

    Path -- Probability
    -------------------
    [1, 'defect', 2, 'j sees i', 4, '~punish i', 7, 'k sees j', 8, '~punish j', 11] -- 0.36
    [1, 'defect', 2, 'j sees i', 4, '~punish i', 7, '~k sees j', 9] -- 0.24
    [1, 'defect', 2, '~j sees i', 5] -- 0.40


```python
# As documented in the NRPy+ tutorial module
#   Tutorial_SEOBNR_Derivative_Routine.ipynb,
#   this module computes partial derivatives
#   of the SEOBNRv3 Hamiltonian with respect
#   to 12 dynamic variables

# Authors: Zachariah B. Etienne & Tyler Knowles
#         zachetie **at** gmail **dot* com

# Step 1.a: import all needed modules from Python/NRPy+:
import sympy as sp                # SymPy: The Python computer algebra package upon which NRPy+ depends
import sys, os                    # Standard Python modules for multiplatform OS-level functions

nrpy_dir_path = os.path.join("..") # TYLERK: Remove these three lines when moving to main NRPy+ directory
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

from outputC import superfast_uniq, lhrh      # Remove duplicate entries from a Python array; store left- and right-
                                              #   hand sides of mathematical expressions

# Step 1.b: Check for a sufficiently new version of SymPy (for validation)
# Ignore the rc's and b's for release candidates & betas.
sympy_version = sp.__version__.replace('rc', '...').replace('b', '...')
sympy_version_decimal = float(int(sympy_version.split(".")[0]) + int(sympy_version.split(".")[1])/10.0)
if sympy_version_decimal < 1.2:
    print('Error: NRPy+ does not support SymPy < 1.2')
    sys.exit(1)

# Step 1.c: Name of the directory containing the input file
inputdir = "SEOBNR"

# Supporting function to simplify derivative expressions by removing terms equal to 0
def simplify_deriv(lhss_deriv,rhss_deriv):
    # Copy expressions into another array
    lhss_deriv_simp = []
    rhss_deriv_simp = []
    for i in range(len(rhss_deriv)):
        lhss_deriv_simp.append(lhss_deriv[i])
        rhss_deriv_simp.append(rhss_deriv[i])
    # If a right-hand side is 0, substitute value 0 for the corresponding left-hand side in later terms
    for i in range(len(rhss_deriv_simp)):
        if rhss_deriv_simp[i] == 0:
            for j in range(i+1,len(rhss_deriv_simp)):
                for var in rhss_deriv_simp[j].free_symbols:
                    if str(var) == str(lhss_deriv_simp[i]):
                        rhss_deriv_simp[j] = rhss_deriv_simp[j].subs(var,0)
    zero_elements_to_remove = []
    # Create array of indices for expressions that are zero
    for i in range(len(rhss_deriv_simp)):
        if rhss_deriv_simp[i] == sp.sympify(0):
            zero_elements_to_remove.append(i)

    # When removing terms that are zero, we need to take into account their new index (after each removal)
    count = 0
    for i in range(len(zero_elements_to_remove)):
        del lhss_deriv_simp[zero_elements_to_remove[i]+count]
        del rhss_deriv_simp[zero_elements_to_remove[i]+count]
        count -= 1
    return lhss_deriv_simp,rhss_deriv_simp

# Supporing function to convert a generic partial derivative into a partial derivative with respect to a specific variable
def deriv_onevar(lhss_deriv,rhss_deriv,variable_list,index):
    # Denote each variable with prm
    variableprm_list = []
    for variable in variable_list:
        variableprm_list.append(str(variable)+"Prm")

    # Copy expressions into another array
    lhss_deriv_new = []
    rhss_deriv_new = []
    for i in range(len(rhss_deriv)):
        lhss_deriv_new.append(lhss_deriv[i])
        rhss_deriv_new.append(rhss_deriv[i])
    # For each free symbol's derivative, replace it with:
    #   1, if we are differentiating with respect to the variable, or
    #   0, if we are note differentiating with respect to that variable
    for i in range(len(rhss_deriv_new)):
        for var in variableprm_list:
            if variableprm_list.index(str(var))==index:
            #if var==(variable+"prm"):
                rhss_deriv_new[i] = rhss_deriv_new[i].subs(var,1)
            else:
                rhss_deriv_new[i] = rhss_deriv_new[i].subs(var,0)
    # Simplify derivative expressions again
    lhss_deriv_simp,rhss_deriv_simp = simplify_deriv(lhss_deriv_new,rhss_deriv_new)
    return lhss_deriv_simp,rhss_deriv_simp

# Step 2.a: Read in expressions as a (single) string
with open(os.path.join(inputdir,'second_derivatives_input.txt'), 'r') as file:
    expressions_as_lines = file.readlines()

# Step 2.b: Create and populate the "lr" array, which separates each line into left- and right-hand sides
#   Each entry is a string of the form lhrh(lhs='',rhs='')
lr = []

for i in range(len(expressions_as_lines)):
    # Ignore lines with 2 or fewer characters and those starting with #
    if len(expressions_as_lines[i]) > 2 and expressions_as_lines[i][0] != "#":
        # Split each line by its equals sign
        split_line = expressions_as_lines[i].split("=")
        # Append the line to "lr", removing spaces, "sp." prefixes, and replacing Lambda->Lamb
        #   (Lambda is a protected keyword):
        lr.append(lhrh(lhs=split_line[0].replace(" ","").replace("Lambda","Lamb"),
                           rhs=split_line[1].replace(" ","").replace("sp.","").replace("Lambda","Lamb")))

# Step 2.c: Separate and sympify right- and left-hand sides into separate arrays
lhss = []
rhss = []
for i in range(len(lr)):
    lhss.append(sp.sympify(lr[i].lhs))
    rhss.append(sp.sympify(lr[i].rhs))

# Step 3.a: Create `input_constants` array and populate with SymPy symbols
m1,m2,tortoise,eta,KK,k0,k1,EMgamma,d1v2,dheffSSv2 = sp.symbols('m1 m2 tortoise eta KK k0 k1 EMgamma d1v2 dheffSSv2',
                                                                    real=True)
s1x,s1y,s1z,s2x,s2y,s2z = sp.symbols('s1x s1y s1z s2x s2y s2z', real=True)
input_constants = [m1,m2,tortoise,eta,KK,k0,k1,EMgamma,d1v2,dheffSSv2,s1x,s1y,s1z,s2x,s2y,s2z]

# Step 3.b: Create `dynamic_variables` array and populate with SymPy symbols
x,y,z,px,py,pz = sp.symbols('x y z px py pz', real=True)
dynamic_variables = [x,y,z,px,py,pz]

# Step 4.a: Prepare array of "free symbols" in the right-hand side expressions
full_symbol_list_with_dups = []
for i in range(len(lr)):
    for variable in rhss[i].free_symbols:
        full_symbol_list_with_dups.append(variable)

# Step 4.b: Remove duplicate free symbols
full_symbol_list = superfast_uniq(full_symbol_list_with_dups)

# Step 4.c: Remove input constants from symbol list
for inputconst in input_constants:
    for symbol in full_symbol_list:
        if str(symbol) == str(inputconst):
            full_symbol_list.remove(symbol)

# Step 5.a: Convert each left-hand side to function notation
#   while separating and simplifying left- and right-hand sides
xx = sp.Symbol('xx')
func = []
for i in range(len(lr)):
    func.append(sp.sympify(sp.Function(lr[i].lhs,real=True)(xx)))

# Step 5.b: Mark each free variable as a function with argument xx
full_function_list = []
for symb in full_symbol_list:
    func = sp.sympify(sp.Function(str(symb),real=True)(xx))
    full_function_list.append(func)
    for i in range(len(rhss)):
        for var in rhss[i].free_symbols:
            if str(var) == str(symb):
                rhss[i] = rhss[i].subs(var,func)

# Step 6.a: Use SymPy's diff function to differentiate right-hand sides with respect to xx
#   and append "prm" notation to left-hand sides
lhss_deriv = []
rhss_deriv = []
for i in range(len(rhss)):
#        lhss_deriv.append(sp.sympify(str(lhss[i])+"prm"))
    lhss_deriv.append(sp.sympify(str(lhss[i])+"Prm"))
#        newrhs = sp.sympify(str(sp.diff(rhss[i],xx)).replace("(xx)","").replace(", xx","prm").replace("Derivative",""))
    newrhs = sp.sympify(str(sp.diff(rhss[i],xx)).replace("(xx)","").replace(", xx","Prm").replace("Derivative",""))
    rhss_deriv.append(newrhs)

# Step 7.b: Call the simplication function and then copy results
lhss_deriv_simp,rhss_deriv_simp = simplify_deriv(lhss_deriv,rhss_deriv)
lhss_deriv = lhss_deriv_simp
rhss_deriv = rhss_deriv_simp

# Step 8.b: Call the derivative function and populate dictionaries with the result
lhss_derivative = {}
rhss_derivative = {}
for index in range(len(dynamic_variables)):
    lhss_temp,rhss_temp = deriv_onevar(lhss_deriv,rhss_deriv,dynamic_variables,index)
    lhss_derivative[dynamic_variables[index]] = lhss_temp
    rhss_derivative[dynamic_variables[index]] = rhss_temp

# Step 9: Output original expression and each partial derivative expression in SymPy snytax
with open("second_partial_derivatives.txt", "w") as output:
    for i in range(len(lr)):
        right_side = lr[i].rhs
        right_side_in_sp = right_side.replace("sqrt(","sp.sqrt(").replace("log(","sp.log(").replace("pi",
                                                    "sp.pi").replace("sign(","sp.sign(").replace("Abs(",
                                                    "sp.Abs(").replace("Rational(","sp.Rational(")
        output.write(str(lr[i].lhs)+" = "+right_side_in_sp)
    for var in dynamic_variables:
        for i in range(len(lhss_derivative[var])):
            right_side = str(rhss_derivative[var][i])
            right_side_in_sp = right_side.replace("sqrt(","sp.sqrt(").replace("log(","sp.log(").replace("pi",
                                                        "sp.pi").replace("sign(","sp.sign(").replace("Abs(",
                                                        "sp.Abs(").replace("Rational(","sp.Rational(").replace("Prm",
                                                        "Prm_"+str(var))
            output.write(str(lhss_derivative[var][i]).replace("Prm","Prm_"+str(var))+" = "+right_side_in_sp+"\n")
```

```python
# Define a function to return a set of reasonable input parameters
# This function contains three distinct sets of input parameters, and index differentiates between them
def reset_values():
    values = {}
    # Each variable corresponds to a specific set of input parameters
    # -f 20 -M 23 -m 10 -X 0.01 -Y 0.02 -Z -0.03 -x 0.04 -y -0.05 -z 0.06
    values = {'m1': 2.300000000000000e+01, 'm2': 1.000000000000000e+01, 'eta': 2.112029384756657e-01,
              'x': 2.129380629915628e+01, 'y': 0.000000000000000e+00, 'z': 0.000000000000000e+00,
              'px': 0.000000000000000e+00, 'py': 2.335719349083314e-01, 'pz': 4.235761411850791e-22,
              's1x': 4.857667584940312e-03, 's1y': 9.715170052170581e-03, 's1z': -1.457311283194314e-02,
              's2x': 3.673094582185491e-03, 's2y': -4.591305801317518e-03, 's2z': 5.509693894690944e-03,
              'KK': 5.288229332346335e-01, 'k0': -9.985821843899071e-01, 'k1': -8.345846602989615e-01,
              'd1v2': -7.966696593617955e+01,'dheffSSv2':1.261873764525631e+01, 'tortoise': 1,
              'EMgamma': 5.772156649015329e-01}
    # Return the input values
    return values

# Numerically evaluate right-hand sides using input values
def evaluate_expression(left_sides,right_sides,input_values):
    new_right_sides = []
    for i in range(len(right_sides)):
        term = sp.sympify(str(right_sides[i]).replace("(xx)",""))
        # Only look for the free variables in each expression to reduce computation time
        free_vars = term.free_symbols
        for variable in free_vars:
            term = term.subs(variable, input_values[str(variable)])
        # Evaluate each term to reduce computation time
        new_right_sides.append(sp.sympify(term.evalf()))
        # Store each subexpression in values numerically
        input_values[str(left_sides[i])] = new_right_sides[i]
    # Return the input values dictionary with all numerical right-hand added
    return input_values

# Create array of trusted LALSuite derivative values
# Note that position in the array corresponds to the index of the corresponding input values
#-M 13 -m 11 -X 0.1 -Y -0.2 -Z 0.3 -x -0.3 -y 0.2 -z -0.1
LALSuite_validated_values = {'dHdx': 2.385809449654369e-03, 'dHdpy': 2.175267508848809e-01,
                             'dHdpz': -2.465237643652547e-06, 'dHdr': -2.432655706366936e-07}

# Call evaluation function
print("Done with this block...")
```

    Done with this block...

```python
values = reset_values()
values = evaluate_expression(lhss,rhss,values)
quagsire = evaluate_expression(lhss_derivative[py],rhss_derivative[py],values)

tru_dHdpy = LALSuite_validated_values['dHdpy']
tru_tol_m6_dHdpy = 2.175267702260915e-01
tru_tol_m2_dHdpy = 2.175267541277814e-01
new_dHdpy = quagsire['Hrealprm_py']/values['eta']

print("tru dHdpy = %.15e" % tru_dHdpy)
print("new dHdpy = %.15e" % new_dHdpy)
print("relative diff = %.15e" % ((tru_dHdpy-new_dHdpy)/tru_dHdpy))
print("rel dif tol m6 = %.15e" % ((tru_tol_m6_dHdpy-new_dHdpy)/tru_tol_m6_dHdpy))
print("rel dif tol m2 = %.15e" % ((tru_tol_m2_dHdpy-new_dHdpy)/tru_tol_m2_dHdpy))
```

    tru dHdpy = 2.175267508848809e-01
    new dHdpy = 2.175267535944173e-01
    relative diff = -1.245610674335776e-08
    rel dif tol m6 = 7.645805677948079e-08
    rel dif tol m2 = 2.451947013445752e-09

```python
values = reset_values()
values = evaluate_expression(lhss,rhss,values)
quagsire = evaluate_expression(lhss_derivative[pz],rhss_derivative[pz],values)

tru_dHdpz = LALSuite_validated_values['dHdpz']
tru_tol_m6_dHdpz = -2.465731322824164e-06
tru_tol_m2_dHdpz = -2.465233875460002e-06
new_dHdpz = quagsire['Hrealprm_pz']/values['eta']

print("tru dHdpz = %.15e" % tru_dHdpz)
print("new dHdpz = %.15e" % new_dHdpz)
print("relative diff = %.15e" % ((tru_dHdpz-new_dHdpz)/tru_dHdpz))
print("rel dif tol m6 = %.15e" % ((tru_tol_m6_dHdpz-new_dHdpz)/tru_tol_m6_dHdpz))
print("rel dif tol m2 = %.15e" % ((tru_tol_m2_dHdpz-new_dHdpz)/tru_tol_m2_dHdpz))
```

    tru dHdpz = -2.465237643652547e-06
    new dHdpz = -2.465235188355871e-06
    relative diff = 9.959675419908415e-07
    rel dif tol m6 = 2.012118934861622e-04
    rel dif tol m2 = -5.325644279845206e-07

```python
values = reset_values()
values = evaluate_expression(lhss,rhss,values)
quagsire = evaluate_expression(lhss_derivative[x],rhss_derivative[x],values)

tru_dHdx = LALSuite_validated_values['dHdx']
new_dHdx = quagsire['Hrealprm_x']/values['eta']

print("tru dHdx = %.15e" % tru_dHdx)
tru_tol_m6_dHdx = 2.385808641286588e-03
tru_tol_m2_dHdx = 2.385809159826322e-03
print("new dHdx = %.15e" % new_dHdx)
print("relative diff = %.15e" % ((tru_dHdx-new_dHdx)/tru_dHdx))
print("rel dif tol m6 = %.15e" % ((tru_tol_m6_dHdx-new_dHdx)/tru_tol_m6_dHdx))
print("rel dif tol m2 = %.15e" % ((tru_tol_m2_dHdx-new_dHdx)/tru_tol_m2_dHdx))
```

    tru dHdx = 2.385809449654369e-03
    new dHdx = 2.385784695044191e-03
    relative diff = 1.037577002692327e-05
    rel dif tol m6 = 1.003695014871812e-05
    rel dif tol m2 = 1.025429130822698e-05

```python
r = values['x']
ptheta = -r*values['pz']
pphi = r*values['py']

new_dHdr = new_dHdx - new_dHdpy*pphi/r/r + new_dHdpz*ptheta/r/r
tru_dHdr = LALSuite_validated_values['dHdr']

print("term 1 = %.15e" % new_dHdx)
print("term 2 = %.15e" % (new_dHdpy*pphi/r/r))
print("term 3 = %.15e" % (new_dHdpz*ptheta/r/r))

print("new dHdr = %.15e" % new_dHdr)
print("tru dHdr = %.15e" % tru_dHdr)
print("relative diff = %.15e" % ((tru_dHdr-new_dHdr)/tru_dHdr))
# Note that since dHdx agrees to about 5 significant digits and we lose about 3 from the
#    subtraction of dHdpy*pphi/r/r from dHdx, we only expect to agree to at most 2 significant digits
```

    term 1 = 2.385784695044191e-03
    term 2 = 2.386052733718586e-03
    term 3 = 4.903842899326205e-29
    new dHdr = -2.680386743947327e-07
    tru dHdr = -2.432655706366936e-07
    relative diff = -1.018356345832295e-01

```python

```


# Part1: Predicting Molecular Properties

## Deep learning on graphs:

In this hands-on exercise, we will guide you to design and train your first Graph Neural Network. We will focus here on a graph classification task. In this setting, you're given a set of graphs associated with a label. The goal is to model this set of graphs in order to extract relevant information from 1. the graph topology and 2. the node features to predict the associated label. In this example, the graphs are molecules where the nodes are the atoms and the edges the chemical bonds. More specifically, we are using the MUTAG dataset that consists of 188 chemical compounds divided into two
classes according to their mutagenic effect on a bacterium.

## Objectives:

- Familiarise yourself with the concept of graph.
- Introduce the Deep Graph Library (DGL) -- the de facto python library for learning on graphs
- Design from scratch an instance of GNN: the Graph Isomorphism Network (GIN)
- Train a graph classification task: predicting the mutagenicity of a molecule

## A.) Build a graph in DGL: the `DGLGraph`
- Declare a graph is as simple as `g = dgl.DGLGraph()`
- Add nodes with `add_nodes()`
- Add egdes with `add_edges()`
- Add node features using the `ndata` attribute

```python
import dgl
import numpy as np
import torch

# 1. declare a graph
graph = dgl.DGLGraph()

# 2. add 10 nodes
graph.add_nodes(10)

# 3. add 7 random edges
from_ = np.random.randint(0, 9, 7)
to_ = np.random.randint(0, 9, 7)
graph.add_edges(from_, to_)

# 4. add node features
graph.ndata['attr'] = torch.randn(10, 5)

def print_graph_properties(g):
  print('Graph has the following properties:')
  print('- {} nodes'.format(g.number_of_nodes()))
  for key, val in g.ndata.items():
    print('- "{}" with {} node features'.format(key, list(val.shape)))
  print('- {} edges'.format(g.number_of_edges()))

print_graph_properties(graph)
```

    Graph has the following properties:
    - 10 nodes
    - "attr" with [10, 5] node features
    - 7 edges

    /Users/gja/opt/anaconda3/lib/python3.7/site-packages/dgl/base.py:45: DGLWarning: Recommend creating graphs by `dgl.graph(data)` instead of `dgl.DGLGraph(data)`.
      return warnings.warn(message, category=category, stacklevel=1)

## B.) Dataloading with DGL:

### DGL built-in dataloader
- DGL provides a set of built-in dataloader for common datasets (eg `dgl.data.GINDataset`, `dgl.data.TUDataset`)
- This module is analogous to the `torchvision` library that provides an API to load popular computer vision datasets.

### DGL is built around `networkx` & PyTorch
- Allows to (partially) use the `networkx` API when dealing with the graph objects
- Conversion from `DGLGraph` to networkx `Graph` is straightforward
- Allows to use the PyTorch API when manipulating the node & edges features

### How to build a batch of graphs: The DGL `batch`
- For graph classification, as in image classification, we need to build a batch, ie a set of samples that are fed to the model.
- As opposed to image classification where each sample can be resized and padded to obtain the same size, adopting the same approach with graphs is not feasible.
- A DGL batch is built using the observation that a set of N graphs can be represented as one large disconnected graph made of N connected components.

```python
import dgl
import torch
from torch.utils.data import DataLoader
import random

# 1. load the data: graphs and labels
data = dgl.data.GINDataset('MUTAG', self_loop=False)

# 2. Inspect manually the data by printing one of the samples
g, label = data[0]
print_graph_properties(g)

# 3. Batchify and train/val split the data
data = list(zip(data.graphs, data.labels))
random.shuffle(data)
train_data = data[:int(len(data)* 0.7)]
val_data = data[int(len(data)*0.7):]
batch_size = 8

def collate(batch):
  g = dgl.batch([example[0] for example in batch])
  l = torch.LongTensor([example[1] for example in batch])
  return g, l

train_dataloader = DataLoader(train_data, batch_size, shuffle=True, collate_fn=collate)
val_dataloader = DataLoader(train_data, batch_size, collate_fn=collate)
```

    Graph has the following properties:
    - 23 nodes
    - "attr" with [23, 7] node features
    - "label" with [23] node features
    - 54 edges

## C.) Designing a DGL model

### Object oriented approach
- Define your Graph Neural Network layer as a python object
- Define a model object that is:
  - instantiating several GNN layers
  - implementing a global pooling operation, e.g., with a sum
  - projecting the graph embedding to the number of classes using an MLP

### The Graph Isomorphism Network
- The GIN proposes to update each node as:
\begin{equation}
h_v^{(k)} = \mbox{MLP}^{(k)} (h_v^{(k-1)} + \sum_{u \in N(v)} h_u^{(k-1)})
\end{equation}
- The graph-level embedding is then obtained using:
\begin{equation}
h_G = \sum_{v \in V} h_v^{(k)}
\end{equation}
- The DGL library also provides a high-level API to directly load Graph Neural Network layers in the `dgl.nn.pytorch.conv` module.

```python
import torch
import torch.nn as nn
import torch.nn.functional as F

class GINLayer(nn.Module):

    def __init__(self, node_dim, out_dim):
        """
        Implementation of a GIN (Graph Isomorphism Network) layer.

        Original paper:
          - How Powerful are Graph Neural Networks: https://arxiv.org/abs/1810.00826
          - Author's public implementation: https://github.com/weihua916/powerful-gnns

        :param node_dim: (int) input dimension of the node features
        :param out_dim: (int) output dimension of the node features
        """

        super(GINLayer, self).__init__()

        self.batchnorm_h = nn.BatchNorm1d(out_dim)

        self.mlp = nn.Sequential(
        nn.Linear(node_dim, out_dim),
        nn.ReLU(),
        nn.Linear(out_dim, out_dim),
      )

    def msg_fn(self, edges):
        """
        Message of each node
        """
        return {'msg': edges.src['h']}

    def reduce_fn(self, nodes):
        """
        For each node, aggregate the nodes using a reduce function.
        Current supported functions are sum and mean.
        """
        accum = torch.sum(nodes.mailbox['msg'], dim=1)
        return {'agg_msg': accum}

    def node_update_fn(self, nodes):
        """
        Node update function
        """
        h = nodes.data['h']
        h = self.mlp(h)
        h = F.relu(h)
        return {'h_out': h}

    def forward(self, g, h):
        """
        Forward-pass of a GIN layer.
        :param g: (DGLGraph) graph to process
        :param h: (FloatTensor) node features
        """

        # 1. set node features to g
        g.ndata['h'] = h

        # 2. message passing
        g.update_all(self.msg_fn, self.reduce_fn)
        g.ndata['h'] = g.ndata.pop('agg_msg') + g.ndata.pop('h')
        g.apply_nodes(func=self.node_update_fn)

        # 3. pop node features & apply batch norm
        h = g.ndata.pop('h_out')
        h = self.batchnorm_h(h)

        return h

class Model(nn.Module):

    def __init__(self, node_dim, out_dim, num_layers, num_classes):
      super(Model, self).__init__()

      # 1. define series of GNN layer
      self.layers = nn.ModuleList()
      for layer_id in range(num_layers):
        self.layers.append(GINLayer(node_dim if layer_id == 0 else out_dim, out_dim))

      # 2. define classifier
      self.classifier = nn.Sequential(
        nn.Linear(out_dim, out_dim),
        nn.ReLU(),
        nn.Linear(out_dim, num_classes)
      )

    def forward(self, g):

      # 1. loop over the GNN layers
      h = g.ndata['attr'].type('torch.FloatTensor')
      for layer in self.layers:
        h = layer(g, h)

      # 2. apply pooling to build fixed-size representation
      g.ndata['h'] = h
      g_emb = dgl.sum_nodes(g, 'h')

      # 3. apply classifier to get the logits
      logits = self.classifier(g_emb)

      return logits

```

## D.) Define the training and testing loop

### Use classic PyTorch training loop
- Define the model parameters (num layers, GNN dimensions)
- Define the training parameters (optimizer, learning rate, weight decay, number of epochs)

```python
import torch
from tqdm import trange

# declare model
model = Model(
  node_dim=g.ndata['attr'].shape[1],
  out_dim=32,
  num_layers=3,
  num_classes=2
)

# build optimizer
optimizer = torch.optim.Adam(
    model.parameters(),
    lr=10e-3,
    weight_decay=5e-4
)

# define loss function
loss_fn = torch.nn.CrossEntropyLoss()

# training
val_loss = 10e5
val_accuracy = 0.

with trange(50) as t:
  for epoch in t:
    t.set_description('Validation with loss={} | accuracy={}'.format(val_loss, val_accuracy))
    # A.) train for 1 epoch
    model.train()
    for graphs, labels in train_dataloader:

        # 1. forward pass
        labels = labels
        logits = model(graphs)

        # 2. backward pass
        loss = loss_fn(logits, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    # B.) validate
    model.eval()
    all_val_logits = []
    all_val_labels = []
    for graphs, labels in val_dataloader:
        with torch.no_grad():
            labels = labels
            logits = model(graphs)
        all_val_logits.append(logits)
        all_val_labels.append(labels)

    all_val_logits = torch.cat(all_val_logits).cpu()
    all_val_labels = torch.cat(all_val_labels).cpu()

    with torch.no_grad():
        val_loss = round(loss_fn(all_val_logits, all_val_labels).item(), 2)
        _, predictions = torch.max(all_val_logits, dim=1)
        correct = torch.sum(predictions.to(int) == all_val_labels.to(int))
        val_accuracy = round(correct.item() * 1.0 / len(all_val_labels), 2)

```

    Validation with loss=0.76 | accuracy=0.73: 100%|██████████| 50/50 [00:17<00:00,  2.83it/s]

```python

```

notebooks/Part1 - Predicting molecular properties.ipynb


# Neuromatch Academy: Week 1, Day 5, Tutorial 1
# Dimensionality Reduction: Geometric view of data

---

Tutorial objectives
In this notebook we'll explore how multivariate data can be represented in different orthonormal bases. This will help us build intuition that will be helpful in understanding PCA in the following tutorial.

Steps:
 1. Generate correlated multivariate data.
 2. Define an arbitrary orthonormal basis.
 3. Project data onto new basis.

---

```python
#@title Video: Geometric view of data
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="emLW0F-VUag", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video

```

    Video available at https://youtube.com/watch?v=emLW0F-VUag

# Setup
Run these cells to get the tutorial started.

```python
#library imports
import time                        # import time
import numpy as np                 # import numpy
import scipy as sp                 # import scipy
import math                        # import basic math functions
import random                      # import basic random number generator functions

import matplotlib.pyplot as plt    # import matplotlib
from IPython import display

```

```python
#@title Figure Settings
%matplotlib inline
fig_w, fig_h = (8, 8)
plt.rcParams.update({'figure.figsize': (fig_w, fig_h)})
plt.style.use('ggplot')
%config InlineBackend.figure_format = 'retina'
```

```python
#@title Helper functions

def get_data(cov_matrix):
  """
  Returns a matrix of 1000 samples from a bivariate, zero-mean Gaussian

  Note that samples are sorted in ascending order for the first random variable.

  Args:
    cov_matrix (numpy array of floats): desired covariance matrix

  Returns:
    (numpy array of floats) : samples from the bivariate Gaussian, with
                              each column corresponding to a different random variable
  """
  mean = np.array([0,0])
  X = np.random.multivariate_normal(mean,cov_matrix,size = 1000)
  indices_for_sorting = np.argsort(X[:,0])
  X = X[indices_for_sorting,:]
  return X

def plot_data(X):
  """
  Plots bivariate data. Includes a plot of each random variable, and a scatter
  plot of their joint activity. The title indicates the sample correlation
  calculated from the data.

  Args:
    X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable

  Returns:
    Nothing.
  """
  fig = plt.figure(figsize=[8,4])
  gs = fig.add_gridspec(2,2)
  ax1 = fig.add_subplot(gs[0,0])
  ax1.plot(X[:,0],color='k')
  plt.ylabel('Neuron 1')
  plt.title('Sample var 1: {:.1f}'.format(np.var(X[:,0])))
  ax1.set_xticklabels([])
  ax2 = fig.add_subplot(gs[1,0])
  ax2.plot(X[:,1],color='k')
  plt.xlabel('Sample Number')
  plt.ylabel('Neuron 2')
  plt.title('Sample var 2: {:.1f}'.format(np.var(X[:,1])))
  ax3 = fig.add_subplot(gs[:, 1])
  ax3.plot(X[:,0],X[:,1],'.',markerfacecolor=[.5,.5,.5], markeredgewidth=0)
  ax3.axis('equal')
  plt.xlabel('Neuron 1 activity')
  plt.ylabel('Neuron 2 activity')
  plt.title('Sample corr: {:.1f}'.format(np.corrcoef(X[:,0],X[:,1])[0,1]))

def plot_basis_vectors(X,W):
  """
  Plots bivariate data as well as new basis vectors.

  Args:
    X (numpy array of floats):   Data matrix
                                  each column corresponds to a different random variable
    W (numpy array of floats):   Square matrix representing new orthonormal basis
                                  each column represents a basis vector

  Returns:
    Nothing.
  """
  plt.figure(figsize=[4,4])
  plt.plot(X[:,0],X[:,1],'.',color=[.5,.5,.5],label='Data')
  plt.axis('equal')
  plt.xlabel('Neuron 1 activity')
  plt.ylabel('Neuron 2 activity')
  plt.plot([0,W[0,0]],[0,W[1,0]],color='r',linewidth=3,label = 'Basis vector 1')
  plt.plot([0,W[0,1]],[0,W[1,1]],color='b',linewidth=3,label = 'Basis vector 2')
  plt.legend()

def plot_data_new_basis(Y):
  """
  Plots bivariate data after transformation to new bases. Similar to plot_data but
  with colors corresponding to projections onto basis 1 (red) and basis 2 (blue).
  The title indicates the sample correlation calculated from the data.

  Note that samples are re-sorted in ascending order for the first random variable.

  Args:
    Y (numpy array of floats):   Data matrix in new basis
                                  each column corresponds to a different random variable

  Returns:
    Nothing.
  """
  fig = plt.figure(figsize=[8,4])
  gs = fig.add_gridspec(2,2)
  ax1 = fig.add_subplot(gs[0,0])
  ax1.plot(Y[:,0],'r')
  plt.xlabel
  plt.ylabel('Projection \n basis vector 1')
  plt.title('Sample var 1: {:.1f}'.format(np.var(Y[:,0])))
  ax1.set_xticklabels([])
  ax2 = fig.add_subplot(gs[1,0])
  ax2.plot(Y[:,1],'b')
  plt.xlabel('Sample number')
  plt.ylabel('Projection \n basis vector 2')
  plt.title('Sample var 2: {:.1f}'.format(np.var(Y[:,1])))
  ax3 = fig.add_subplot(gs[:, 1])
  ax3.plot(Y[:,0],Y[:,1],'.',color=[.5,.5,.5])
  ax3.axis('equal')
  plt.xlabel('Projection basis vector 1')
  plt.ylabel('Projection basis vector 2')
  plt.title('Sample corr: {:.1f}'.format(np.corrcoef(Y[:,0],Y[:,1])[0,1]))

```

# Generate correlated multivariate data

```python
#@title Video: Multivariate data
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="YOan2BQVzTQ", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=YOan2BQVzTQ

To study multivariate data, first we generate it. In this exercise we generate data from a *bivariate normal distribution*. This is an extension of the one-dimensional normal distribution to two dimensions, in which each $x_i$ is marginally normal with mean $\mu_i$ and variance $\sigma_i^2$:

\begin{align}
x_i \sim \mathcal{N}(\mu_i,\sigma_i^2)
\end{align}

Additionally, the joint distribution for $x_1$ and $x_2$ has a specified correlation coefficient $\rho$. Recall that the correlation coefficient is a normalized version of the covariance, and ranges between -1 and +1.

\begin{align}
\rho = \frac{\text{cov}(x_1,x_2)}{\sqrt{\sigma_1^2 \sigma_2^2}}
\end{align}

For simplicity, we will assume that the mean of each variable has already been subtracted, so that $\mu_i=0$. The remaining parameters can be summarized in the covariance matrix:
\begin{equation*}
{\bf \Sigma} =
\begin{pmatrix}
 \text{var}(x_1) & \text{cov}(x_1,x_2) \\
 \text{cov}(x_1,x_2) &\text{var}(x_2)
\end{pmatrix}
\end{equation*}
Note that this is a symmetric matrix with the variances $\text{var}(x_i) = \sigma_i^2$ on the diagonal, and the covariance on the off-diagonal.

### Exercise
We have provided code to draw random samples from a zero-mean bivariate normal distribution. These samples could be used to simulate changes in firing rates for two neurons. Fill in the function below to calculate the covariance matrix given the desired variances and correlation coefficient. The covariance can be found by rearranging the equation above:

\begin{align}
\text{cov}(x_1,x_2) = \rho \sqrt{\sigma_1^2 \sigma_2^2}
\end{align}

Use these functions to generate and plot data while varying the parameters. You should get a feel for how changing the correlation coefficient affects the geometry of the simulated data.

**Suggestions**
* Fill in the function `calculate_cov_matrix` to calculate the covariance.
* Generate and plot the data for $\sigma_1^2 =1$, $\sigma_1^2 =1$, and $\rho = .8$. Try plotting the data for different values of the correlation coefficent: $\rho = -1, -.5, 0, .5, 1$.

```python
help(plot_data)
help(get_data)
```

    Help on function plot_data in module __main__:

    plot_data(X)
        Plots bivariate data. Includes a plot of each random variable, and a scatter
        plot of their joint activity. The title indicates the sample correlation
        calculated from the data.

        Args:
          X (numpy array of floats):   Data matrix
                                        each column corresponds to a different random variable

        Returns:
          Nothing.

    Help on function get_data in module __main__:

    get_data(cov_matrix)
        Returns a matrix of 1000 samples from a bivariate, zero-mean Gaussian

        Note that samples are sorted in ascending order for the first random variable.

        Args:
          cov_matrix (numpy array of floats): desired covariance matrix

        Returns:
          (numpy array of floats) : samples from the bivariate Gaussian, with
                                    each column corresponding to a different random variable

```python

def calculate_cov_matrix(var_1,var_2,corr_coef):
  """
  Calculates the covariance matrix based on the variances and correlation coefficient.

  Args:
    var_1 (scalar):    variance of the first random variable
    var_2 (scalar):    variance of the second random variable
    corr_coef (scalar):      correlation coefficient

  Returns:
    (numpy array of floats) : covariance matrix
  """
  ###################################################################
  ## Insert your code here to:
  ##                calculate the covariance from the variances and correlation
  # cov = ...
  cov_matrix = np.array([[var_1,cov],[cov,var_2]])

  #uncomment once you've filled in the function
  raise NotImplementedError("Student excercise: calculate the covariance matrix!")
  ###################################################################
  return cov

###################################################################
## Insert your code here to:
##                generate and plot bivariate Gaussian data with variances of 1
##                      and a correlation coefficients of: 0.8
##                repeat while varying the correlation coefficient from -1 to 1
###################################################################
variance_1 = 1
variance_2 = 1
corr_coef = 0.8

#uncomment to test your code and plot
#cov_matrix = calculate_cov_matrix(variance_1,variance_2,corr_coef)
#X = get_data(cov_matrix)
#plot_data(X)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D5-DimensionalityReduction/solutions/W1D5_Tutorial1_Solution_62df7ae6.py)

*Example output:*

# Define a new orthonormal basis

```python
#@title Video: Orthonormal bases
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="dK526Nbn2Xo", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=dK526Nbn2Xo

Next, we will define a new orthonormal basis of vectors ${\bf u} = [u_1,u_2]$ and ${\bf w} = [w_1,w_2]$. As we learned in the video, two vectors are orthonormal if:

1.   They are orthogonal (i.e., their dot product is zero):
\begin{equation}
{\bf u\cdot w} = u_1 w_1 + u_2 w_2 = 0
\end{equation}
2.   They have unit length:
\begin{equation}
||{\bf u} || = ||{\bf w} || = 1
\end{equation}

In two dimensions, it is easy to make an arbitrary orthonormal basis. All we need is a random vector ${\bf u}$, which we have normalized. If we now define the second basis vector to be ${\bf w} = [-u_2,u_1]$, we can check that both conditions are satisfied:
\begin{equation}
{\bf u\cdot w} = - u_1 u_2 + u_2 u_1 = 0
\end{equation}
and
\begin{equation}
{|| {\bf w} ||} = \sqrt{(-u_2)^2 + u_1^2} = \sqrt{u_1^2 + u_2^2} = 1,
\end{equation}
where we used the fact that ${\bf u}$ is normalized. So, with an arbitrary input vector, we can define an orthonormal basis, which we will write in matrix by stacking the basis vectors horizontally:

\begin{equation}
{{\bf W} } =
\begin{pmatrix}
 u_1 & w_1 \\
 u_2 & w_2
\end{pmatrix}.
\end{equation}

### Exercise

In this exercise you will fill in the function below to define an orthonormal basis, given a single arbitrary 2-dimensional vector as an input.

**Suggestions**
* Modify the function `define_orthonormal_basis` to first normalize the first basis vector $\bf u$.
* Then complete the function by finding a basis vector $\bf w$ that is orthogonal to $\bf u$.
* Test the function using initial basis vector ${\bf u} = [3,1]$. Plot the resulting basis vectors on top of the data scatter plot using the function `plot_basis_vectors`. (For the data, use  $\sigma_1^2 =1$, $\sigma_1^2 =1$, and $\rho = .8$).

```python
help(plot_basis_vectors)
```

    Help on function plot_basis_vectors in module __main__:

    plot_basis_vectors(X, W)
        Plots bivariate data as well as new basis vectors.

        Args:
          X (numpy array of floats):   Data matrix
                                        each column corresponds to a different random variable
          W (numpy array of floats):   Square matrix representing new orthonormal basis
                                        each column represents a basis vector

        Returns:
          Nothing.

```python
def define_orthonormal_basis(u):
  """
  Calculates an orthonormal basis given an arbitrary vector u.

  Args:
    u (numpy array of floats):    arbitrary 2-dimensional vector used for new basis

  Returns:
    (numpy array of floats) : new orthonormal basis
                                columns correspond to basis vectors
  """
  ###################################################################
  ## Insert your code here to:
  ##                normalize vector u
  ##                calculate vector w that is orthogonal to w

  #u = ....
  #w = ...
  #W = np.column_stack((u,w))

  #comment this once you've filled the function
  raise NotImplementedError("Student excercise: implement the orthonormal basis function")
  ###################################################################
  return W

variance_1 = 1
variance_2 = 1
corr_coef = 0.8

cov_matrix = calculate_cov_matrix(variance_1,variance_2,corr_coef)
X = get_data(cov_matrix)
u = np.array([3,1])

#uncomment and run below to plot the basis vectors
##define_orthonormal_basis(u)
#plot_basis_vectors(X,W)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D5-DimensionalityReduction/solutions/W1D5_Tutorial1_Solution_c9ca4afa.py)

*Example output:*

# Project data onto new basis

```python
#@title Video: Change of basis
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="5MWSUtpbSt0", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=5MWSUtpbSt0

Finally, we will express our data in the new basis that we have just found. Since $\bf W$ is orthonormal, we can project the data into our new basis using simple matrix multiplication :

\begin{equation}
{\bf Y = X W}.
\end{equation}

We will explore the geometry of the transformed data $\bf Y$ as we vary the choice of basis.

#### Exercise
In this exercise you will fill in the function below to define an orthonormal basis, given a single arbitrary vector as an input.

**Suggestions**
* Complete the function `change_of_basis` to project the data onto the new basis.
* Plot the projected data using the function `plot_data_new_basis`.
* What happens to the correlation coefficient in the new basis? Does it increase or decrease?
* What happens to variance?

```python
def change_of_basis(X,W):
  """
  Projects data onto new basis W.

  Args:
    X (numpy array of floats) : Data matrix
                                each column corresponding to a different random variable
    W (numpy array of floats):  new orthonormal basis
                                columns correspond to basis vectors

  Returns:
    (numpy array of floats) : Data matrix expressed in new basis
  """
  ###################################################################
  ## Insert your code here to:
  ##                project data onto new basis described by W

  #Y = ...
  #comment this once you've filled the function
  raise NotImplementedError("Student excercise: implement change of basis")
  ###################################################################
  return Y

## Unomment below to transform the data by projecting it into the new basis
## Plot the projected data
# Y = change_of_basis(X,W)
# plot_data_new_basis(Y)
# disp(...)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D5-DimensionalityReduction/solutions/W1D5_Tutorial1_Solution_b434bc0d.py)

*Example output:*

#### Exercise
To see what happens to the correlation as we change the basis vectors, run the cell below. The parameter $\theta$ controls the angle of $\bf u$ in degrees. Use the slider to rotate the basis vectors.

**Questions**
* What happens to the projected data as you rotate the basis?
* How does the correlation coefficient change? How does the variance of the projection onto each basis vector change?
* Are you able to find a basis in which the projected data is uncorrelated?

```python
###### MAKE SURE TO RUN THIS CELL VIA THE PLAY BUTTON TO ENABLE SLIDERS ########

import ipywidgets as widgets

def refresh(theta = 0):
  u = [1,np.tan(theta * np.pi/180.)]
  W = define_orthonormal_basis(u)
  Y = change_of_basis(X,W)
  plot_basis_vectors(X,W)
  plot_data_new_basis(Y)

_ = widgets.interact(refresh,
  theta = (0, 90, 5))
```


```python
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import scipy
from sklearn.model_selection import ParameterGrid
from sklearn.manifold import Isomap
import time
from tqdm import tqdm

import librosa
from librosa import cqt
from librosa.core import amplitude_to_db
from librosa.display import specshow

import os
import glob
import h5py

from utilities.isomapEmbedding import isomapEmbedding
```

```python
hop_size= 512
q= 24
```

```python
with h5py.File("TinySOL.h5", "r") as f:
    features_dict = {key:f[key][()] for key in f.keys()}
```

```python
grid = {
    'Q': [24],
    'k': [3],
    'comp': ['log'],
    'instr': ['all'],
    'dyn': ['all']
}

settings = list(ParameterGrid(grid))

for setting in settings:

    if setting["instr"] == 'all':
        setting['instr'] = ''

    if setting['dyn'] == 'all':
        setting['dyn'] = ''
```

```python
# Isomap embedding computation

batch_str = []

for setting in settings:

    q = setting['Q']
    # Batch process and store in a folder
    batch_str = [setting['instr'], setting['dyn']]

    batch_features = []
    for feature_key in features_dict.keys():
        # Get features that match setting

        if all(x in feature_key for x in batch_str):
            batch_features.append(features_dict[feature_key])

    batch_features = np.stack(batch_features, axis=1)
    print(batch_features.shape)

    isomap, freqs, rho_std = isomapEmbedding(batch_features)
    coords = isomap.embedding_
```

    (168, 2913)

```python
curr_fig= plt.figure(figsize=(5.5, 2.75))
ax= curr_fig.add_subplot(121)
ax.axis('off')

import colorcet as cc
subsampled_color_ids = np.floor(np.linspace(0, 256, q, endpoint=False)).astype('int')
color_list= [cc.cyclic_mygbm_30_95_c78[i] for i in subsampled_color_ids]

# Plot embedding with color
for i in range(coords.shape[0]):
    plt.scatter(coords[i, 0], coords[i, 1], color= color_list[i%q], s=30.0)

plt.plot(coords[:, 0], coords[:, 1], color='black', linewidth=0.2)

# Plot Pearson correlation matrix
rho_frequencies = freqs

freq_ticklabels = ['A1', 'A2', 'A3']
freq_ticks = librosa.core.note_to_hz(freq_ticklabels)

tick_bins = []
tick_labels= []
for i,freq_tick in enumerate(freq_ticks):
    tick_bin = np.argmin(np.abs(rho_frequencies-freq_tick))
    tick_bins.append(tick_bin)
    tick_labels.append(freq_ticklabels[i])

plt.figure(figsize=(2.5,2.5))
plt.imshow(np.abs(rho_std), cmap='magma_r')
plt.xticks(tick_bins)
plt.gca().set_xticklabels(freq_ticklabels)

plt.yticks(tick_bins)
plt.gca().set_yticklabels(freq_ticklabels)
plt.gca().invert_yaxis()

plt.clim(0, 1)

```

### Circle projection

```python
from utilities.circle_fit import circle_fit

A = np.transpose(coords[:,:-1])
x, r, circle_residual = circle_fit(A)
```

```python
matplotlib.rc('font', family='serif')

fig, axes = plt.subplots()
plt.scatter(A[0,:],A[1,:])
plt.plot(x[0],x[1],'rx')

circle = plt.Circle(x, radius=r, fill=False, linestyle='-.')

axes.set_aspect(1)
axes.add_artist(circle)

plt.title('Circle fit: TinySOL all instr', pad=10.0)
plt.show()

print(np.sqrt(circle_residual)/72)
```

```python
import sympy

from sympy.geometry import Circle, Point, Line
from utilities.d_squared import d_squared

center = Point(x, evaluate=False)
c = Circle(center, r, evaluate=False)

l = Line(Point(coords[0,:-1]), center, evaluate=False)
points = [tuple(p) for p in l.points]

xy_prime = []

# TODO: Optimize to a more pythonic manner
for a,b in coords[:,:2]:

    intersections = c.intersection(Line(Point(a,b), center, evaluate=False))

    if d_squared((a,b),intersections[0]) < d_squared((a,b), intersections[1]):
        xy_prime.append([float(p) for p in intersections[0]])
    else:
        xy_prime.append([float(p) for p in intersections[1]])

```

```python
fig, axes = plt.subplots()
plt.scatter(np.array(xy_prime)[:,0],np.array(xy_prime)[:,1], s=10,
            label='projected points')
plt.scatter(A[0,:],A[1,:], s=0.5, label='isomap embedding points (2D)')
plt.plot(center[0],center[1],'rx')

circle = plt.Circle([float(p) for p in center], radius=r, fill=False,
                    linestyle='--', label='estimated circle fit')

axes.set_aspect(1)
# axes.add_artist(circle)

plt.title('Projected points on circle', pad=10.0)
plt.legend(bbox_to_anchor=(1,1))
plt.show()

```

```python
vectors = coords[:,:-1] - x.T
azimuths = np.arctan2(vectors[:, 1], (1e-6 + vectors[:, 0]))
xy_new = np.asarray([x[0] + r * np.cos(azimuths), x[1] + r * np.sin(azimuths)]).T

min(xy_new[:,1])
```

    -4.986045019041322

### Line projection

```python
z = np.arange(len(coords[:,2]))
z_fit = scipy.stats.linregress(z, coords[:,2])
print(z_fit.stderr)
```

    0.008383389057128201

```python
plt.figure()
plt.title('Line fit: TinySOL all instr')
plt.scatter(np.arange(len(coords[:,2])), coords[:,2])

plt.plot(z_fit.intercept + z_fit.slope*z, 'b')
```

```python
# New line coordinates
z_prime = [i * z_fit.slope + z_fit.intercept for i,_ in enumerate(coords[:,2])]
```

```python
coords_prime = np.append(np.array(xy_prime), np.expand_dims(np.array(z_prime), axis=1), axis=1)
coords_length = coords_prime.shape[0]
```

### Distance matrices

```python
from utilities.distance import self_distance
```

```python
# Projected helix self-distance matrix
D_proj = self_distance(coords_prime)

# Isomap embedding self-distance matrix
D_isomap = self_distance(coords)
```

```python
# Geodesic self-distance matrix
D_geodesic = isomap.dist_matrix_

# Convert to upper triangular sparse matrix
for i in range(coords_length):
    for j in range(i):
        D_geodesic[i,j] = 0
```

```python
from utilities.matrix_operations import centered
from utilities.matrix_operations import frobenius_distance
```

```python
loss_isomap = frobenius_distance(centered(D_geodesic), centered(D_isomap))/coords_length
loss_total = frobenius_distance(centered(D_geodesic), centered(D_proj))/coords_length
loss_proj = frobenius_distance(centered(D_isomap), centered(D_proj))/coords_length
```

```python
print(f"Isomap loss= {loss_isomap}")
print(f"Projection loss= {loss_proj}")
print(f"Total loss= {loss_total}")
```

    Isomap loss= 11.192415210749315
    Projection loss= 5.263174367367614
    Total loss= 11.78717458565251

```python
(loss_total) - (loss_isomap + loss_proj) < 0
```

    True


$$
\huge\text{Latex Essentials}\\
\text{Andrew Ribeiro}\\
\text{2017}
$$

LaTex is a robust typesetting system which is used ubiquitously in scientific publications and mathematical documents. With a working knowledge of LaTex, you can make beautiful scientific documents and express your mathematical constructions easily. Jupyter notebooks allow us embed LaTex in markdown cells using the double dollar sign block ```$$ Your latex here $$```, so we can use LaTex to document our mathematical notebooks easily! This guide will show you the basics of using LaTex in Jupyter notebooks.

** Double click any cell to reveal the LaTex code. **

Resources and Tools
* https://www.sharelatex.com/learn/
* https://www.latex4technics.com/
* https://math.meta.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference

## Basic Formatting and Embedding Latex

We can use single dollar signs to embedd latex in a markdown sentence. Let's say we want to talk about a function $f(x)$ in some text and refer to $x$ as well. This is how you'd go about it.

If you want to put a snippet of LaTex on its own dedicated line, use double dollar signs.

$$ \text{This is a dedicated LaTex line}$$

Within some latex block, you can write multiple lines as such:

$$ \text{Line 1} \\ \text{Line 2} $$

You can adjust the size of each line as such:

$$ \huge \text{This is a big line} \\
\large \text{This is a smaller, but still big } \\
\text{This is a normal size} \\
\small \text{This is a smaller size}
$$

For more complex statements you must use the begin and end commands:

```$$\begin{align} your latex \end{align}$$```

The following snippets use this.

You can also give each line a tag if you'd like to refer to them later.

$$
\begin{align}
\text{Line 1} \tag 1 \\
\text{Line 2} \tag 2
\end{align}
$$

You can arrange things in columns and rows using the double ampersand.

$$
\begin{align}
\text{This} && \text{Is} && \text{sparta} \\
\text{no} && \text{this} && \text{is} && \text{columns} \\
&& \text{and} && \text{rows}
\end{align}
$$

You can  keep equations aligned by using the single ampersand.:

$$
\begin{align}
f(x) &= \frac{x(x+1)}{2} \\
& = equiv \\
& = equiv
\end{align}
$$

## Essentials

#### Fractions
$$\frac{2}{\frac{2}{3}+5}+\frac{1}{2}$$

#### Matrices
$$
\large\begin{bmatrix}
n && n-1 && n-2 && \dots && 0 \\
0 && 1 && 2 && \dots&& n
\end{bmatrix}^\intercal
$$

#### Sums
$$ \large\sum_{i=1}^{n}{i} = \frac{n(n+1)}{2} $$

#### Products
$$
\large\prod_{i=1}^{n}{i}
$$

#### Calculus
$$\int_{a}^{b} x^2 dx$$

$$\lim_{x\to\infty} f(x)$$

## Examples

$$
\large\sum_{i=1}^{n}{i} = \frac{n(n+1)}{2} =
\frac{
\begin{bmatrix}
1\\
1
\end{bmatrix}^\intercal
\begin{bmatrix}
n && n-1 && n-2 && \dots && 0 \\
0 && 1 && 2 && \dots&& n
\end{bmatrix}
\begin{bmatrix}
1&&1&&\dots&& n
\end{bmatrix}^\intercal}{2}
$$

From Mike

$$
\begin{align}
\frac{\partial{f}}{\partial{v_{0}}} & = \frac{1}{2} \frac{\partial}{\partial{v_{0}}} (( A_{00}v_{0} + A_{01}v_{1} - b_{0} )^{2}) + \frac{1}{2} \frac{\partial}{\partial{v_{0}}} (( A_{10}v_{0} + A_{11}v_{1} - b_{1} )^{2}) && \tag 6\\
& = \frac{1}{2} 2 ( A_{00}v_{0} + A_{01}v_{1} - b_{0} ) \frac{\partial}{\partial{v_{0}}} ( A_{00}v_{0} + A_{01}v_{1} - b_{0} ) + \frac{1}{2} 2 ( A_{10}v_{0} + A_{11}v_{1} - b_{1} ) \frac{\partial}{\partial{v_{0}}} ( A_{10}v_{0} + A_{11}v_{1} - b_{1} ) && \tag 7\\
& = A_{00}( A_{00}v_{0} + A_{01}v_{1} - b_{0} ) + A_{10} (A_{10}v_{0} + A_{11}v_{1} - b_{1}) && \tag 8\\
\end{align}
$$

$$
f(\mathbf{x}) = \frac{1}{2} \lVert \mathbf{A}\mathbf{x} - \mathbf{b} \rVert^{2}_{2}
$$

$$
\begin{align}
\nabla_{\mathbf{v}}f(\mathbf{v}) & = \begin{bmatrix}A_{00}&A_{10}\\A_{01}&A_{11}\\ \end{bmatrix} \begin{bmatrix}A_{00}v_{0} + A_{01}v_{1} - b_{0}\\A_{10}v_{0} + A_{11}v_{1} - b_{1}\\ \end{bmatrix} && \tag{11}\\
& = \mathbf{A}^T(\mathbf{A}\mathbf{x} - \mathbf{b}) && \tag{qed}\\
\end{align}
$$

$$
\begin{align}
Pr(x,y)Pr(z|w,x,y)Pr(w|x,y)
& = Pr(z|w,x,y)Pr(w|x,y)Pr(x,y) &&\text{rearrange} \tag 1\\
& = Pr(z|w,x,y)Pr(w,x,y) &&\text{prince 2.5} \tag 2\\
& = \frac{Pr(w,x,y,z)}{Pr(w,x,y)}Pr(w,x,y) &&\text{bayes rule on left factor of (2)} \tag 3\\
& = Pr(w,x,y,z) &&\text{terms cancel} \tag 4\\
\end{align}
$$


Coupled cavity broadening device

```python
from simba.core import StateSpace
from simba.graph import nodes_from_network
from sympy import symbols, Matrix, sqrt
```

```python
alpha, beta = symbols('alpha beta', positive=True)

a = Matrix([[0, 0, 1, 0], [0, 0, 0, 1], [-1, 0, alpha, 0], [0, -1, 0, alpha]])
b = Matrix([[0, 0], [0, 0], [0, beta], [beta, 0]])
c = Matrix([[0, 0, 0, beta], [0, 0, beta, 0]])
d = Matrix([[1, 0], [0, 1]])

ss = StateSpace(a, b, c, d, paired_operator_form=True)
```

```python
ss.to_slh()
```

$$\displaystyle \left(I_{2\times2}, \left[\begin{matrix}0 & 0 & 0 & \beta\end{matrix}\right] \left[\begin{matrix}a_{1}\\\overline{a_{1}}\\a_{2}\\\overline{a_{2}}\end{matrix}\right], \left[\begin{matrix}\overline{a_{1}} & a_{1} & \overline{a_{2}} & a_{2}\end{matrix}\right] \left[\begin{matrix}0 & 0 & \frac{i}{2} & 0\\0 & 0 & 0 & - \frac{i}{2}\\- \frac{i}{2} & 0 & 0 & 0\\0 & \frac{i}{2} & 0 & 0\end{matrix}\right] \left[\begin{matrix}a_{1}\\\overline{a_{1}}\\a_{2}\\\overline{a_{2}}\end{matrix}\right]\right)$$

```python
split_network = ss.to_slh().split()
nodes = nodes_from_network(split_network)
g = nodes.as_graphviz_agraph()
g.layout(prog='dot')
g.draw("coupled-cavity-broadener.png")
g.draw("coupled-cavity-broadener.pdf")
```

```python
split_network.h_d
```

$\displaystyle \left[\begin{matrix}0 & 0 & i & 0\\0 & 0 & 0 & - i\\0 & 0 & 0 & 0\\0 & 0 & 0 & 0\end{matrix}\right]$

```python
split_network.gs
```

    [(S = Matrix([
     [1, 0],
     [0, 1]]), K = Matrix([[0, 0]]), R = Matrix([
     [0, 0],
     [0, 0]])), (S = Matrix([
     [1, 0],
     [0, 1]]), K = Matrix([[0, beta]]), R = Matrix([
     [0, 0],
     [0, 0]]))]

```python
split_network.dynamical_matrix.states.states
```

$\displaystyle \left[\begin{matrix}a_{1}\\\overline{a_{1}}\\a_{2}\\\overline{a_{2}}\\a'_{1}\\\overline{a'_{1}}\\a'_{2}\\\overline{a'_{2}}\\ain_{1}\\\overline{ain_{1}}\\aout_{1}\\\overline{aout_{1}}\\ain_{2}\\\overline{ain_{2}}\\aout_{2}\\\overline{aout_{2}}\end{matrix}\right]$

```python
split_network.dynamical_matrix.matrix
```

$\displaystyle \left[\begin{array}{cccccccccccccccc}0 & 0 & - \frac{1}{s} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & - \frac{1}{s} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\frac{1}{s} & 0 & 0 & 0 & 0 & 0 & 0 & - \frac{\sqrt{2} \beta \sqrt{\gamma_{2}}}{2 s} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & \frac{1}{s} & 0 & 0 & 0 & 0 & - \frac{\sqrt{2} \beta \sqrt{\gamma_{2}}}{2 s} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & - \frac{\sqrt{2} \beta \sqrt{\gamma_{2}}}{2 s} & 0 & 0 & \frac{\gamma_{2}}{s} & 0 & 0 & - \frac{\sqrt{2} \sqrt{\gamma_{2}}}{s} & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & - \frac{\sqrt{2} \beta \sqrt{\gamma_{2}}}{2 s} & 0 & 0 & 0 & 0 & \frac{\gamma_{2}}{s} & 0 & 0 & - \frac{\sqrt{2} \sqrt{\gamma_{2}}}{s} & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & - \sqrt{2} \sqrt{\gamma_{2}} & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0\\0 & 0 & 0 & 0 & 0 & 0 & 0 & - \sqrt{2} \sqrt{\gamma_{2}} & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0\end{array}\right]$

```python
split_network.dynamical_matrix.states
```

$$\displaystyle \left[\begin{matrix}a_{1}\\\overline{a_{1}}\\a_{2}\\\overline{a_{2}}\\a'_{1}\\\overline{a'_{1}}\\a'_{2}\\\overline{a'_{2}}\\ain_{1}\\\overline{ain_{1}}\\aout_{1}\\\overline{aout_{1}}\\ain_{2}\\\overline{ain_{2}}\\aout_{2}\\\overline{aout_{2}}\end{matrix}\right]$$

```python

```


## 다변수 연속확률변수
- 연속확률분포에서는 확률변수 내 단일 사건에 대한 확률이 아닌 구간의 분포를 나타냄.
- 그래서 cdf를 먼저 정의한 후, 이를 미분하여 pdf를 정의하는 방식으로 흘러감.

##### 1. Joint cdf

```python
import sympy
sympy.init_printing(use_latex='mathjax')
```

$$ F_{XY}(x, y) = P(\{ X < x \} \cap \{ Y < y \}) = P(\{X < x, Y < y\}) $$

$$ F_{XY}(\infty, \infty)=1 $$
$$ F_{XY}(-\infty, y)=F_{XY}(x,-\infty)=0 $$

##### 2. Joint pdf
- 확률변수 두개에 대해 각각 편미분

$$ f_{XY} = \dfrac{\partial^2 F_{XY}(x, y)}{\partial x \partial y} $$

```python
# 확률변수 X : 몸무게, Y : 키 라고 봤을 때
# 이를 2차원 graph로 표현하면
mu = [70, 170]
cov = [[150, 140], [140, 300]]
rv = sp.stats.multivariate_normal(mu, cov)

xx = np.linspace(20, 120, 100)
yy = np.linspace(100, 250, 100)
XX, YY = np.meshgrid(xx, yy)
ZZ = rv.pdf(np.dstack([XX, YY]))
plt.contour(XX, YY, ZZ)
plt.xlabel("x")
plt.ylabel("y")
plt.title("Joint Probability Density p(x,y)")
plt.show()
```

```python
# 위 밀도함수를 3차원으로 나타내면
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)
ax.contour(XX, YY, ZZ)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_xlim(20, 120)
ax.set_ylim(100, 250)
ax.view_init(50, -50)
plt.show()
```

```python
# 고정된 y값에 대해 확률 밀도함수의 단면을 표시하면
from matplotlib.collections import PolyCollection
from matplotlib import colors as mcolors

xx = np.linspace(20, 120, 100)
yy = np.linspace(100, 250, 16)
XX, YY = np.meshgrid(xx, yy)
ZZ = rv.pdf(np.dstack([XX, YY]))

for i, j in enumerate(range(9, 3, -1)):
    ax = plt.subplot(6, 1, i + 1)
    plt.plot(xx, ZZ[j, :])
    plt.ylim(0, 0.0012)
    if i < 5:
        ax.xaxis.set_ticklabels([])
    plt.ylabel("p(x, y={:.0f})".format(yy[j]), rotation=0, labelpad=40)
plt.xlabel("x")
plt.tight_layout()
plt.show()
```

- 확률 변수의 값을 모른다고 할 때 어떤 값이 나올 가능성이 가장 높은가. 170 , 70 정도?
- 만약 확률 변수 Y 의 값이 170이 나왔다면 확률 변수 X 의 값은 어떤 값이 나올 가능성이 가장 높은가. 약 70
- 만약 확률 변수 Y 의 값이 150이 나왔다면 확률 변수 X 의 값은 어떤 값이 나올 가능성이 가장 높은가. 약 60

##### 3. Marginal pdf
- 이때는 pdf판 전체확률의 법칙이 적용된다.
- Y가 70일때 X의 밀도값과 같이 Y가 변화할때 X의 밀도값을 합하여 분포로 나타낸 것(?)

$$
\begin{align}%\label{}
\nonumber  f_X(x) = \int_{-\infty}^{\infty} f_{XY}(x,y)dy \\
\nonumber  f_Y(y) = \int_{-\infty}^{\infty} f_{XY}(x,y)dx
\end{align}
$$

```python
from matplotlib.ticker import NullFormatter
from matplotlib import transforms
from scipy.integrate import simps # 심슨법칙(Simpson's rule)을 사용한 적분 계산

xx = np.linspace(20, 120, 100)
yy = np.linspace(100, 250, 100)
XX, YY = np.meshgrid(xx, yy)
ZZ = rv.pdf(np.dstack([XX, YY]))
fx = [simps(Z, yy) for Z in ZZ.T]
fy = [simps(Z, xx) for Z in ZZ]

plt.figure(figsize=(6,6))

left, width = 0.1, 0.65
bottom, height = 0.1, 0.65
bottom_h = left_h = left + width + 0.05

rect1 = [left, bottom, width, height]
rect2 = [left, bottom_h, width, 0.2]
rect3 = [left_h, bottom, 0.2, height]

ax1 = plt.axes(rect1)
ax2 = plt.axes(rect2)
ax3 = plt.axes(rect3)

ax2.xaxis.set_major_formatter(NullFormatter())
ax3.yaxis.set_major_formatter(NullFormatter())

ax1.contour(XX, YY, ZZ)
ax2.plot(xx, fx)
base = ax3.transData
rot = transforms.Affine2D().rotate_deg(-90)
plt.plot(-yy, fy, transform=rot + base)

ax1.set_xlim(38, 102)
ax1.set_ylim(120, 220)
ax2.set_xlim(38, 102)
ax3.set_xlim(0, 0.025)
ax3.set_ylim(120, 220)
plt.show()
```

- marginal pdf(X)는 위에서 아래 또는 아래에서 위쪽으로 찌부시킨 것.
- marginal pdf(Y)는 왼쪽에서 오른쪽으로 찌부시킨것이라 보면 된다.

##### 4. Conditional pdf
- 이도 pmf와 마찬가지로 확률변수 중 하나가 특정값이라는 사실이 밝혀졌을 때
- 나머지 확률변수의 조건부분포를 나타낸다.
- 이때도 역시 scaling의 개념이 들어가기 때문에 조건부분포의 각 위치에서의 높이가 다르더라도 같은 그래프로 본다.(scaling해주면 같아지기 때문에)

$$ f_{X \mid Y}(x \mid y) = \dfrac{f_{XY}(x, y)}{f_{Y}(y)} $$

$$ f_{Y \mid X}(y \mid x) = \dfrac{f_{XY}(x, y)}{f_{X}(x)} $$

```python
from scipy.integrate import simps # 심슨법칙(Simpson's rule)을 사용한 적분 계산

xx = np.linspace(20, 120, 100)
yy = np.linspace(100, 250, 16)
XX, YY = np.meshgrid(xx, yy)
ZZ = rv.pdf(np.dstack([XX, YY]))

plt.figure(figsize=(6,10))

for i, j in enumerate(range(9, 4, -1)):
    ax = plt.subplot(5, 1, i + 1)
    plt.plot(xx, ZZ[j, :], 'b:', label="joint")
    marginal = simps(ZZ[j, :], xx)
    plt.plot(xx, ZZ[j, :] / marginal, 'r-', label="conditional")
    plt.ylim(0, 0.05)
    ax.xaxis.set_ticklabels([])
    plt.ylabel("p(x, y={:.0f})".format(yy[j]), rotation=0, labelpad=40)
plt.xlabel("x")
plt.tight_layout()
plt.legend()
plt.show()
```