# Quantum Game Theory

__Note: The answers to the exercises in this notebook do not show up when looking at this notebook on GitHub. In order to see them, please open up this notebook using Jupyter.__

```python
# Run this cell once to load all the relevant packages and settings
%load_ext autoreload
%autoreload 2

from qiskit import QuantumCircuit
from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt
from qiskit.quantum_info import Operator
from qiskit.extensions import XGate, HGate
from QuantumGameTheory import Game
import pandas as pd
import numpy as np
```

## What you will learn in this tutorial
* __Game Theory__ - What a "game theory" game is.
* __Quantum Game Theory__ - What a "quantum game theory" game is and how you can study these types of games using Qiskit.
* __Classical vs Quantum Games__ - How the way we think about strategies change when you go from the classical to the quantum regime.

## Game Theory
Game Theory is defined as "the study of mathematical models of strategic interaction among rational decision-makers" [1], which is a very formal way of saying "it's the study of how games are played!". Although modern game theory was in fact first constructed using a [formal mathematical framework](https://link.springer.com/article/10.1007/BF01448847), we believe that the best way to get introduced to game theory is to start playing a game, so let's dive straight in!

### The Prisoner's Dilemma
Probably the most famous game theoretical game is the [Prisoner's Dilemma](https://en.wikipedia.org/wiki/Prisoner%27s_dilemma). There are many variants to this game each with their own story, so here's our story for the game:

Both you and I team up to break into a zoo and steal a panda (just for a little bit, we are of course going to give it back!). Unfortunately we both get caught, but not quite red handed, so it isn't such an open-and-shut case for the police. In order to try and get a confession out of both of us, the police separate us into different rooms and give each of us two options:
* To remain silent and take our chances - we shall call this strategy '0'
* To confess and maybe receive a reduced sentence - we shall call this strategy '1'

So together, there are 4 possible outcomes that could happen: '00' (where we both remain silent), '01' (where you remain silent and I confess), '10' (where you confess and I remain silent) and '11' (where we both confess).
We are told that if we both remain silent (i.e. end up with the '00' outcome), the police still have enough evidence sentence us both to 1 month in prison. However, if one of us confesses (i.e. the outcome is either '01' or '10') the player who chose the '1' strategy goes free and the other player (who chose the strategy '0') receives a 3 month prison sentence. Finally, if we both confess, (i.e. the outcome is '11') then we each get a 2 month prison sentence. These strategies are represented below in a payoff table.

Throughout this notebook, we will be using the 'QuantumGameTheory' package. So without further ado, let us create the prisoner's dilemma game object by simply calling it:

```python
prisoner_game = Game(game_name='prisoner', protocol="Classical")
```

The 'game_name' indicates which game to load and for now we will be using the 'Classical' protocol. Now before we play the game, let's have a look at the possible payoffs:

```python
prisoner_game.display_payoffs()
```

    Game: prisoner
    Payoffs:

      outcome
      payoffs

      0
      00
      (-1, -1)

      1
      01
      (-3, 0)

      2
      10
      (0, -3)

      3
      11
      (-2, -2)

This shows us the outcomes represented in bits and the payoff for each outcome (this table is the same as the above payoff table). Notice that the payoff representing a 3 month prison sentence is -3 and not 3. This is because players are always trying to maximize their payoff, so the more months in prison will give you more negative results.

Now let's play! For this game there are two players, P1 and P2; you and me.  To play the game, we can simply input our chosen strategy (0 or 1) into P1_strategy and P2_strategy to see who the winner is.

```python
P1_strategy = # pick your strategy
P2_strategy = 1 # I pick strategy '1'
combined_strategies = [P1_strategy, P2_strategy]
outcomes, results = prisoner_game.play_game(combined_strategies)
results
```

__Exercise 1__: What strategy would you choose if you didn't know what strategy I was going to choose? (This can be a bit tricky but it is worth taking the time to think about it before looking at the answer!)

Click here for the answer to Exercise 1
No matter what strategy I choose, the optimal strategy for you is 1 (i.e. to confess). To understand this, let's look at the options starting with my strategies:

First option: I choose strategy 0. If you also choose 0, your payoff is -1 whereas if you choose 1, your payoff is 0. Therefore it is better for you to choose 1 in this case.

Second option: I choose strategy 1. If you choose 0, your payoff is -3 whereas if you also choose 1, your payoff is -2. Therefore it is also better for you to choose 1 in this case.

So it is always better for you to choose 1. By symmetry, this logic works for both of us. Therefore, rationally speaking, we should both always choose option 1. This leads to the concept of the 'dominant strategy' - the strategy that we will choose regardless of what the other person chooses (in this case 1 for the both of us). We say that this game has a 'dominant strategy equilibrium state' with the outcome 11 and payoffs [-2, -2].

### Nash Equilibria

In this section, we investigate the most popular type of equilibrium that games can have - the Nash equilibrium. This concept was discovered by the famous mathematician John Nash (portrayed by Russel Crowe in the [Beautiful Mind](https://en.wikipedia.org/wiki/A_Beautiful_Mind_(film)) \). As a complete aside, if you haven't yet seen the [incredible recommendation letter](https://www.reddit.com/r/pics/comments/38nmzm/princeton_university_just_posted_john_nashs/) he received when applying for the Princeton PhD program, I would highly recommend reading it (it's only a few lines)!

Now back to game theory. The best way to understand Nash equilibria is to see them using a game. In particular, we will look at a game called "Bach or Stravinsky" [2]. The game is as follows: Two people (Alya and Bai) want to go out to a concert. Alya prefers Bach and Bai, Stravinksy. However, they both would prefer to go together rather than to either concert alone. Each of them individually has the option of going to the Bach concert - strategy '0' - or going to the Stravinsky concert - strategy '1'. To see the outcomes and the payoffs, let's look at the payoff table.

```python
BoS_game = Game(game_name='BoS',protocol="Classical")
BoS_game.display_payoffs()
```

    Game: BoS
    Payoffs:

      outcome
      payoffs

      0
      00
      (3, 2)

      1
      01
      (0, 0)

      2
      10
      (0, 0)

      3
      11
      (2, 3)

To start with, we see that there is no dominant strategy equilibrium - neither player has a strategy that will give them the best outcome regardless of the other player's strategy. On the other hand, we know that there are some strategies that are better than others _if a player knows what the other player is going to do_. In this example, say Bai knew that Alya was going to the Bach concert (strategy 0), then he will also choose to go to the Bach concert (because he would rather get a payoff of 2 than 0).

The question now becomes: are there pairs of strategies for which no player would change if the other one was fixed? I.e. is there a combined strategy 'a' and 'b' such that:
1. If Bai knows that Alya chose strategy 'a', then he will choose strategy 'b' _and_
2. If Alya knows that Bai chose strategy 'b', then she will choose strategy 'a'?

If this is the case, we say that Alya and Bai are in a Nash equilibrium.

__Exercise 2__: What are the Nash equilibria in the Bach or Stravinsky game?

Click here for the answer to Exercise 2
The two strategies that Alya and Bai can choose that lead to Nash equilibria are '00' or '11'. Let's look at the first one from each of the players' points of view:

 If Alya knew that Bai was going to pick 0 (i.e. go to the Bach concert), then she would also pick 0 to maximize her payoff.

 If Bai knew that Alya was going to pick 0, then he would also pick 0 to maximize his payoff.

Therefore, neither player will deviate from their strategies. The same applies for the '11' strategy

__Exercise 3__: Are dominant strategy equilibria Nash equilibria? What about vice-versa? (If you're not sure what dominant strategy equilibria are, have a look at the answer to Exercise 1)

Click here for the answer to Exercise 3
    Dominant strategy equilibria are also always Nash equilibria - if the players each choose the dominant strategy, they also will not change their strategies if they know what the other player is going to do, which by definition puts them in a Nash equilibrium. On the other hand, not all Nash equilibria are dominant strategy equilibria as can be seen using the Bach or Stravinsky game above.

### General 2x2 Games

The prisoner's dilemma and Bach or Stravinksy game are known as a "2x2 games" - games in which there are 2 players and they each have the option between 2 strategies. So immediately we can see how to make any 2x2 game in principle. Just fill out the payoff table below and you have a game you can then play by filling in the P1_choice and P2_choice.

```python
my_game_name = '' # fill in game name
my_payoff_table = {'00': [#, #],
                   '01': [#, #],
                   '10': [#, #],
                   '11': [#, #]} # fill in payoffs
my_game = Game(my_game_name, protocol="Classical", payoff_table=my_payoff_table)
my_game.display_payoffs()
```

```python
P1_choice = # fill in here
P2_choice = # fill in here
choices = [P1_choice, P2_choice]
outcomes, results = my_game.play_game(choices)
results
```

Here ends our basic introduction to Game Theory. There are [many](https://www.youtube.com/watch?v=3Y1WpytiHKE), [many](http://web.mit.edu/rgibbons/www/Gibbons_An%20Introduction%20to%20Applicable%20Game%20Theory.pdf) [good](http://www.rasmusen.org/GI/download.htm) [resources](https://econweb.ucsd.edu/~jsobel/200Cs09/Kartik_Notes.pdf) out there to learn more about classical game theory. As far as this tutorial is concerned, we will end our discussion on classical game theory here. Now let's go quantum!

## Quantum Game Theory

Quantum Game Theory is a field that was first introduced by D. Meyer [3] in 1999 but it wasn't until later that year that a formal quantum game theory protocol was invented by Eisert, Wilkens & Lewenstein [4] and hence is called the "EWL quantization protocol". Since then there have been many contributions to the field, for which we refer the reader to a [comprehensive review of the field by Khan et al.](https://link.springer.com/article/10.1007/s11128-018-2082-8) But for now, let us start by looking at the EWL quantization protocol.

### The EWL Quantization Protocol

Just as reminder, here is the protocol for a classical 2x2 game:
1. Initialization - we start with two '0' bits
2. Strategy - each player receives a bit and decides to either keep it as a 0 or flip it to a 1
3. Outcome - the outcome is the combination of the two bits
4. Payoff - the payoff table is used to find the payoff for each player given the outcome

Notice that for all 2x2 games, the choice of strategies and the final payoffs are the only things that depend on the game - the the steps to take and the order to take them remain the same.

When dealing with _quantum_ game theory, we require the quantum protocol to follow two rules: __Rule 1:__ It has to distribute maximally entangled qubits to the players and __Rule 2:__ it has to generalize the classical version of the game. We will first explain the recipe to turn a classical game theory game into a quantum game and then walk through how to implement it in Qiskit. Let's look at how to turn a 2x2 classical game into a quantum game.

__1. Initialization__ - We start with two $|0\rangle$ qubits and two classical bits that we will use to store measurement results. Ignoring the classical bits for now, this gives us $$|00\rangle $$
__2. Entanglement__ - We want the qubits to be maximally entangled to each other by an operator before we hand them off to the players. For now we will call this entangling operator $J$ (and explain it further later on). This gives us the state $$J|00\rangle $$
__3. Strategy__ - Each player is then given a qubit and they each apply a single-qubit unitary operator (or 'strategy') of their choice to their qubit (as opposed to the classical version where they can only choose between keeping it or flipping it). Let's call player 1's strategy $U_1$ and player 2's strategy $U_2$. We then end up with $$U_1U_2J|00\rangle $$
__4. 'Disentanglement'__- The qubits are then put through a 'disentangling' gate $J^{\dagger}$ (we will get to why later on). We now have $$J^{\dagger}U_1U_2J|00\rangle $$
__5. Measurement__ - The qubits are measured in the computational basis and the measurements are stored in the two classical bits

__6. Outcome__ - The outcome is the combination of the two (classical) bits

__7. Payoff__ - The payoff table is used to find the payoff for each player given the outcome

Similar to the 2x2 classical games, only the strategies taken and payoff tables differ depending on the specific game - the steps to be taken and the order that they are taken in remain same independent of the specific 2x2 game.

#### The Entangling Operator $J$

The entangling and 'disentangling' operators ($J$ and $J^{\dagger}$) are introduced to satisfy the two rules mentioned above. Here is a more detailed description of the rules:

__Rule 1__ : We need to pick an operator that leads to a maximally entangled state before distributing the qubits to each individual player

__Rule 2__ : When playing a quantum game, if both players' strategies are just combinations of either 'keeping it the same' i.e $I$, or 'flipping it' i.e. $X$, then the outcome of the game should be the same as the classical version of the game. This ensures that the classical game is contained within the quantum game.

This leaves us with many possibilities for the $J$ operator. We use the operator chosen by Benjamin & Hayden [5]: $$J = \frac{1}{\sqrt{2}} (I^{\otimes 2} + iX^{\otimes 2})$$ where $I$ is the identity operator and $X$ is the Pauli-X gate. The reason for going with Benjamin & Hayden's choice of the $J$ operator is that it is equivalent to the original one by Eisert, Wilkens and Lewenstein (they both satisfy __Rule 1__ and __Rule 2__) but is also more intuitive and easier to implement.

Applying this operator to the state $|00\rangle$ results in the state $$\frac{1}{\sqrt{2}}\left(|00\rangle + i|11\rangle\right)$$ which is a maximally entangled 2 qubit state.

__Exercise 4__: Show that adding the $J^{\dagger}$ operator at step 4 satisfies __Rule 2__. Hint: start by showing that  $J^{\dagger}$ commutes with $I^{\otimes 2}$ and $X{^\otimes 2}$.

Click here for the answer to Exercise 4
First we show the commutation relations in the hint.

$\begin{align}
    [J^{\dagger}, I^{\otimes 2}] &= J^{\dagger}I^{\otimes 2} - I^{\otimes 2}J^{\dagger} \\
    &= J^{\dagger} - J^{\dagger} \\
    &= 0 \\
    [J^{\dagger}, X^{\otimes 2}] &= J^{\dagger}X^{\otimes 2} - X^{\otimes 2}J^{\dagger} \\
    &= \frac{1}{\sqrt{2}} (I - iX^{\otimes 2})X^{\otimes 2} - X^{\otimes 2}\frac{1}{\sqrt{2}} (I^{\otimes 2} - iX^{\otimes 2}) \\
    &= \frac{1}{\sqrt{2}} (X^{\otimes 2} - iI^{\otimes 2}) - \frac{1}{\sqrt{2}} (X^{\otimes 2} - iI^{\otimes 2}) \\
    &= 0
\end{align}$

And since $X$ and $I$ commute, this means that $J^{\dagger}$ commutes with any combination of $I$ and $X$ (i.e. $I^{\otimes 2}$, $IX$, $XI$ and $X^{\otimes 2}$).
Now if we look back at step 4 of the protocol $$J^{\dagger}U_1U_2J|00\rangle $$ and impose that $U_1$ and $U_2$ are a combination of $I$ and $X$ gates, we can then move move then interchange the $J^{\dagger}$ operator with the $U_1$ and $U_2$ operators (since they all commute) which gives us $$U_1U_2J^{\dagger}J|00\rangle = U_1U_2|00\rangle.$$ This results in the final state being qubits that have either been 'kept the same' or 'flipped' without any entanglement or superposition states, which is exactly the same as what would happen in the classical version. This is why step for is called 'disentanglement' in quotation marks - the $J^{\dagger}$ gate disentangles the qubits in the case when only the $I$ or $X$ operators are used as strategies.

### Building a 2x2 Quantum Game

It's finally Qiskit time! In this section, we will be implementing steps 1 to 5 (above). The code has been written in a way that each cell corresponding to the individual steps also includes all the code to of the previous steps such that it can be run on its own.

#### 1. Initialization
Initialize a circuit with two '0' qubits and 2 classical bits that we will use to store measurement results. This gives a starting state $|00\rangle$.

```python
circ = QuantumCircuit(2,2)
circ.draw(output='mpl')
```

#### 2. Entanglement
We maximally entangle the qubits by inserting the $J$ operator (defined as $J = \frac{1}{\sqrt{2}} (I^{\otimes 2} + iX^{\otimes 2})$) which results in the state $J|00\rangle = \frac{1}{\sqrt{2}}\left(|00\rangle + i|11\rangle\right)$.

```python
I_2 = np.identity(4)

X_2 = np.matrix([[0,0,0,1],
                   [0,0,1,0],
                   [0,1,0,0],
                   [1,0,0,0]])
J_2 = Operator(1 / np.sqrt(2) * (I_2 + 1j *X_2))

circ = QuantumCircuit(2,2)
circ.append(J_2, [0,1])
circ.draw(output='mpl')
```

__Exercise 5__: Calculate the 4x4 matrix for the $J$ operator and show that $J$ is a unitary operator

Click here for the answer to Exercise 5
Computing $J$:
$$I^{\otimes 2} =  \left(\begin{array}{cccc} 1&0&0&0\\
                          0&1&0&0\\
                          0&0&1&0\\
                          0&0&0&1\end{array}\right)$$
$$iX^{\otimes 2} =  \left(\begin{array}{cccc} 0&0&0&i\\
                          0&0&i&0\\
                          0&i&0&0\\
                          i&0&0&0\end{array}\right)$$
Now substituting these in to the equation for the J operator:
$$J = \frac{1}{\sqrt{2}} (I^{\otimes 2} + iX^{\otimes 2}) = \frac{1}{\sqrt{2}}\left(\begin{array}{cccc} 1&0&0&i\\
                                                                                0&1&i&0\\
                                                                                0&i&1&0\\
                                                                                i&0&0&1\end{array}\right)$$

Showing J is unitary:

$\begin{align}J^{\dagger}J &= \frac{1}{\sqrt{2}} (I^{\otimes 2} - iX^{\otimes 2}) \frac{1}{\sqrt{2}} (I^{\otimes 2} + iX^{\otimes 2}) \\
&= \frac{1}{2}(I^{\otimes 2} - iX^{\otimes 2} + iX^{\otimes 2} + I^{\otimes 2}) \\
&= I
\end{align}
$

#### 3. Strategy
Each person can perform a unitary operator on their individual qubit, e.g. player 1 chooses the $X$ and player 2 chooses the $S$ gate (the $S$ gate changes $|1\rangle \rightarrow i|1\rangle$ and does not do anything to $|0\rangle$).

```python
circ = QuantumCircuit(2,2)
circ.append(J_2, [0,1])
circ.x(0) # assigns the X gate to player 1
circ.s(1) # assigns the S gate to player 2
circ.draw(output='mpl')
```

#### 4. 'Disentanglement'
We now apply the $J^{\dagger}$ gate to satisfy __Rule 2__.

```python
J_2_dg = Operator(1 / np.sqrt(2) * (I_2 - 1j *X_2))

circ = QuantumCircuit(2,2)
circ.append(J_2, [0,1])
circ.x(0) # assigns the X gate to player 1
circ.s(1) # assigns the S gate to player 2
circ.append(J_2_dg, [0,1])
circ.draw(output='mpl')
```

__Exercise 6__: What is the state of the qubits at this stage of the circuit?

Click here for the answer to Exercise 6
$\begin{align}
J^{\dagger}J &= J^{\dagger}X_1S_2J|00\rangle \\
&= J^{\dagger}X_1S_2\frac{1}{\sqrt{2}}\left(|00\rangle + i|11\rangle\right) \\
&= J^{\dagger}\frac{1}{\sqrt{2}}\left(|10\rangle - |01\rangle\right) \\
&= \frac{1}{\sqrt{2}} (I^{\otimes 2} - iX^{\otimes 2})\frac{1}{\sqrt{2}}\left(|10\rangle - |01\rangle\right) \\
&= \frac{1}{2}\left(|10\rangle - |01\rangle - i|01\rangle + i|10\rangle\right) \\
&= \frac{1+i}{2}(|10\rangle -|01\rangle)
\end{align}$

So even though player 1 used the $X$ operator to flip their bit, they finally ended in a state that could lead to their final outcome being 0 or 1 with a 50% probability!

#### 5. 'Measurement'
And we then measure both qubits in the computational basis, saving the answers into the classical registers.

```python
circ = QuantumCircuit(2,2)
circ.append(J_2, [0,1])
circ.x(0) # assigns the X gate to player 1
circ.s(1) # assigns the S gate to player 2
circ.append(J_2_dg, [0,1])
circ.measure([0,1], [0,1])
circ.draw(output='mpl')
```

Since the last two steps (outcome and payoff) do not involve any quantum processes we are done with building the quantum circuit for a 2x2 quantum game!

### Quantum Prisoner's Dilemma

Now that we have gone through how to write the circuit ourselves, we can use the inbuilt QuantumGameTheory package from now on to simplify the coding going forward, which basically implements the above for you. Let's spin up a quantum version of prisoner's dilemma.

```python
prisoner_game_quantum = Game(game_name='prisoner', protocol="EWL")
prisoner_game_quantum.display_payoffs()
```

    Game: prisoner
    Payoffs:

      outcome
      payoffs

      0
      00
      (-1, -1)

      1
      01
      (-3, 0)

      2
      10
      (0, -3)

      3
      11
      (-2, -2)

Notice that the payoff table is identical to the classical version (as expected given the protocol described above). Now let us run the same strategies as above in this game.

```python
P1_strategy = 'X'
P2_strategy = 'S'
combined_strategies = [P1_strategy, P2_strategy]
outcomes, results = prisoner_game_quantum.play_game(combined_strategies)
prisoner_game_quantum._quantum_game.circ.draw(output='mpl')
```

```python
results
```

      outcome
      payoffs
      winners
      num_times
      backend

      0
      10
      (0, -3)
      P1
      1
      qasm_simulator

This gives us the outcome of a single game. Let us play the game many times over (say 100) and see the results.

```python
outcomes_sim, results_sim = prisoner_game_quantum.play_game(combined_strategies, n_times = 100)
results_sim
```

      outcome
      payoffs
      winners
      num_times
      backend

      0
      01
      (-3, 0)
      P2
      45
      qasm_simulator

      1
      10
      (0, -3)
      P1
      55
      qasm_simulator

'num_times' refers to the number of times the given outcome appeared and 'outcomes_sim' is simply a dictionary of keys 'outcome' and values 'num_times'. We can now easily visualise the results in the histogram below.

```python
plot_histogram(outcomes_sim)
```

As you can see, we have about an even split between 01 and 10. Does the outcome make sense? At this point if you haven't done Exercise 6 yet, have a look at the solution to see why the outcome is like this and why, even though player one chose $X$ as their strategy, they still end up with an outcome of '0' half the time!

### Playing on an actual quantum computer

__If you haven't setup your IBMQ account, please click [here](https://github.com/Qiskit/qiskit-iqx-tutorials/blob/master/qiskit/fundamentals/1_getting_started_with_qiskit.ipynb) and go to the section titled 'Running circuits from the IBM Q account'.__

So far we have been running everything on the QASM simulator (under the hood). Let's rerun the same game (quantum prisoner's dilemma) with the same strategies (player 1 choose $X$ and player 2 chooses $S$) on a real quantum computer this time! We can do this by simply adding the group and backend of the machine we desire when spinning up the game (this might take a while to run depending on the queue of the machine you select).

```python
prisoner_game_quantum = Game(game_name='prisoner', protocol="EWL", group='open', backend="ibmqx2")
P1_strategy = 'X'
P2_strategy = 'S'
combined_strategies = [P1_strategy, P2_strategy]
outcomes_q, results_q = prisoner_game_quantum.play_game(combined_strategies, n_times = 100)
results_q
```

      outcome
      payoffs
      winners
      num_times
      backend

      0
      11
      (-2, -2)
      no winners
      7
      ibmqx2

      1
      01
      (-3, 0)
      P2
      36
      ibmqx2

      2
      10
      (0, -3)
      P1
      49
      ibmqx2

      3
      00
      (-1, -1)
      no winners
      8
      ibmqx2

And let's plot the two results (from the QASM simulator and from the actual machine) side-by-side

```python
plot_histogram([outcomes_sim,outcomes_q], legend=['simulator', 'ibmqx2'])
```

We see that the outcomes '00' and '11' sometimes appear due to the noise of the real machine. Playing around with different systems and/or strategies, one can get a feel for how different machines perform due to their different types and levels of noise.

## Classical vs Quantum Games

So by now you hopefully have a good notion of basic 2x2 classical and quantum games. But what is the real difference between these games? More specifically, if I play the classical game but allow my choices to be randomized, can I reproduce the quantum game? To answer this question, we have to introduce the notion of mixed (i.e. probabilistic) strategies.

### Mixed Strategies
A mixed strategy is just a strategy which includes classical stochasticity in it, for example "I choose '0' with 50% probability and '1' with 50% probability". This works for quantum strategies too e.g. "I choose 'X' with 33% probability, 'H' with 33% probability and 'S' with 33% probability."  Already we can start to see that classical games (even when allowing for stochasticity) cannot reproduce quantum games (how do we model the effect of stochastic unitary gates with only classical probabilities?). Let's see it in a concrete example: back to our old friend the Prisoner's Dilemma.

### Prisoner's Dilemma Revisited
Let's think of the prisoner's dilemma again but this time using mixed strategies. Let's start by spinning up another prisoner's dilemma game to look at the payoff table.

```python
prisoner_game = Game('prisoner', protocol="Classical")
prisoner_game.display_payoffs()
```

    Game: prisoner
    Payoffs:

      outcome
      payoffs

      0
      00
      (-1, -1)

      1
      01
      (-3, 0)

      2
      10
      (0, -3)

      3
      11
      (-2, -2)

You may remember from Exercise 1 that there is a dominant strategy for both players in this game. So mixed strategies are not useful in this case. So how about for the quantum game? Let's just consider pure strategies (i.e. strategies that are not classically stochastic). Is there a dominant strategy there? Naively one could guess that the $X$ strategy is the dominant one, as it corresponds to the classical dominant strategy of choosing '1'. However if a player - say P1 - chooses $X$, then P2 can choose $Z$ which will cause the result to go to their favor 100% of the time:

```python
prisoner_game.set_protocol("EWL") # we can simply change the protocol of an exisiting game instead of having to spin up a new game every time
P1_strategy = 'X'
P2_strategy = 'Z'
combined_strategies = [P1_strategy, P2_strategy]
outcomes_q, results_q = prisoner_game.play_game(combined_strategies, n_times = 100)
results_q
```

      outcome
      payoffs
      winners
      num_times
      backend

      0
      01
      (-3, 0)
      P2
      100
      qasm_simulator

So therefore a quantum version of the game is a very different playing field to the original classical game!

### Nash equilibria in Quantum Games
It turns out that for any 2x2 quantum game in which players only use pure strategies, there is no Nash equilibrium since every strategy always has a perfect counter-strategy [6].

__Exercise 7__: We just saw that, for the prisoner's dilemma, the perfect counter-strategy to $X$ is $Z$. What is the perfect counter-strategy to $Z$?

Click here for the answer to Exercise 7
If we say that P1 uses $Z$ and P2 is trying to find the perfect counter-strategy $U_c$, we define without loss of generality

$\begin{align}
U_c =  \left(\begin{array}{cc} e^{i\phi}\cos\left(\frac{\theta}{2}\right)&\sin\left(\frac{\theta}{2}\right)\\
                          -\sin\left(\frac{\theta}{2}\right)&e^{-i\phi}\cos\left(\frac{\theta}{2}\right)\end{array}\right)
\end{align}$

So now we have

$\begin{align}
J^{\dagger}J &= J^{\dagger}Z_1U_cJ|00\rangle \\
&= J^{\dagger}Z_1U_c\frac{1}{\sqrt{2}}\left(|00\rangle + i|11\rangle\right) \\
&= J^{\dagger}U_c\frac{1}{\sqrt{2}}\left(|00\rangle - i|11\rangle\right) \\
&= J^{\dagger}\frac{1}{\sqrt{2}}\left[|0\rangle\left(e^{i\phi}\cos\left(\frac{\theta}{2}\right)|0\rangle
                                                -\sin\left(\frac{\theta}{2}\right)|1\rangle\right)
                                       - i|1\rangle\left(\sin\left(\frac{\theta}{2}\right)|0\rangle+
                                                    e^{-i\phi}\cos\left(\frac{\theta}{2}\right)|1\rangle\right)\right] \\
&= J^{\dagger}\frac{1}{\sqrt{2}}\left(e^{i\phi}\cos\left(\frac{\theta}{2}\right)|00\rangle
                                      -\sin\left(\frac{\theta}{2}\right)|01\rangle
                                      -i\sin\left(\frac{\theta}{2}\right)|10\rangle
                                      -ie^{-i\phi}\cos\left(\frac{\theta}{2}\right)|11\rangle\right) \\
&= \frac{1}{2}\left(e^{i\phi}\cos\left(\frac{\theta}{2}\right)(|00\rangle-i|11\rangle)
                    -\sin\left(\frac{\theta}{2}\right)(|01\rangle-i|10\rangle)
                    -i\sin\left(\frac{\theta}{2}\right)(|10\rangle-i|01\rangle)
                    -ie^{-i\phi}\cos\left(\frac{\theta}{2}\right)(|11\rangle-i|00\rangle)\right) \\
\end{align}$

P2's optimal outcome is '01', so we want the coefficient of the $|01\rangle$ terms to be at a maximum, so looking at the $|01\rangle$ coefficient

$\begin{align}
    -\sin\left(\frac{\theta}{2}\right)-\sin\left(\frac{\theta}{2}\right)
\end{align}$

gives us $\theta=\pm\pi$

Which gives us

$\begin{align}
U_c &= \left(\begin{array}{cc} 0&1\\
                              -1&0\end{array}\right)\\
    &= Z \cdot X
\end{align}$

Feel free to test it by changing a the following lines in the cell above

P1_strategy = 'Z'
P2_strategy = ['Z', 'X']

What about allowing for mixed quantum strategies? Well in that case, there are indeed Nash equilibria that emerge for the quantum prisoner's dilemma [6]. Finding Nash equilibria for quantum games is an active area of research, and we will now demonstrate a really neat result by Benjamin & Hayden from Ref [5] which discusses the 4-minority game.

The 4-minority game is a simple 4 player game in which a player wins the game if their outcome is different to everyone else's, i.e. if their outcome is 1 and everyone else's is 0 or vice versa, where their outcome is 0 and everyone else's is 1. As usual, let's start by looking at the payoff table.

```python
minority_game = Game('4-minority', protocol="Classical")
minority_game.display_payoffs()
```

    Game: 4-minority
    Payoffs:

      outcome
      payoffs

      0
      0000
      (0, 0, 0, 0)

      1
      0001
      (0, 0, 0, 1)

      2
      0010
      (0, 0, 1, 0)

      3
      0011
      (0, 0, 0, 0)

      4
      0100
      (0, 1, 0, 0)

      5
      0101
      (0, 0, 0, 0)

      6
      0110
      (0, 0, 0, 0)

      7
      0111
      (1, 0, 0, 0)

      8
      1000
      (1, 0, 0, 0)

      9
      1001
      (0, 0, 0, 0)

      10
      1010
      (0, 0, 0, 0)

      11
      1011
      (0, 1, 0, 0)

      12
      1100
      (0, 0, 0, 0)

      13
      1101
      (0, 0, 1, 0)

      14
      1110
      (0, 0, 0, 1)

      15
      1111
      (0, 0, 0, 0)

In the classical game, the best that one can do is pick the strategy 0 or 1 randomly with a 50% probability and hope for the best. This is indeed a Nash equilibrium and gives each player a 1/8 chance of winning. However, things get very interesting in the quantum world...

It turns out that there are non-trivial pure strategy Nash equilibria in the quantum world. An example is every player deciding to apply the following strategy: $R_z(-3\pi/8) \cdot R_y(pi/2) \cdot R_z(\pi/2)$. Each of the individual operators have been named Rz1, Ry1, Rz2 respectively.

```python
minority_game.set_protocol("EWL")
P1_strategy = ['Rz1', 'Ry1', 'Rz2']
P2_strategy = ['Rz1', 'Ry1', 'Rz2']
P3_strategy = ['Rz1', 'Ry1', 'Rz2']
P4_strategy = ['Rz1', 'Ry1', 'Rz2']
combined_strategies = [P1_strategy, P2_strategy, P3_strategy, P4_strategy]
outcomes, results = minority_game.play_game(combined_strategies, n_times=1000)
minority_game._quantum_game.circ.draw(output='mpl')
```

Notice that the $J$ gate has now become a 4-qubit gate with the form $$J = \frac{1}{\sqrt{2}} (I^{\otimes 4} + iX^{\otimes 4})$$
Now let us look at the results after playing this game 100 times.

```python
results
```

      outcome
      payoffs
      winners
      num_times
      backend

      0
      1110
      (0, 0, 0, 1)
      P4
      114
      qasm_simulator

      1
      1011
      (0, 1, 0, 0)
      P2
      134
      qasm_simulator

      2
      0010
      (0, 0, 1, 0)
      P3
      130
      qasm_simulator

      3
      1101
      (0, 0, 1, 0)
      P3
      134
      qasm_simulator

      4
      0001
      (0, 0, 0, 1)
      P4
      133
      qasm_simulator

      5
      1000
      (1, 0, 0, 0)
      P1
      114
      qasm_simulator

      6
      0100
      (0, 1, 0, 0)
      P2
      116
      qasm_simulator

      7
      0111
      (1, 0, 0, 0)
      P1
      125
      qasm_simulator

```python
plot_histogram(outcomes)
```

This result is rather remarkable since it shows that every player has a 1/4 chance of winning despite using only pure strategies! This is unachievable in the classical version of the game even when using mixed strategies. And what is even more intriguing is that in the 3-player version of this game 1) there are no pure strategy Nash equilibria and 2) the players each have a 1/4 chance of winning when in a mixed strategy Nash equilibrium, which is also true for the Nash equilibrium solution for the classical version of the game. So basically, we can't do any better in the 3-player version of the game by making it quantum, but we _can_ in the 4-player version of the game. Why should this be the case? And in what cases of games can we do better in the quantum version compared to the classical ones? The answer to these questions that no one exactly knows yet... welcome to the wonderful and weird world of quantum game theory!

## What next?
Sadly (or maybe finally!) this tutorial has come to an end. But of course, this notebook only scratches the very surface of quantum game theory. Thank you for taking the time to go through this tutorial and we hope you enjoyed it, and maybe even learnt a little something :)

If you are interested in exploring the field of quantum game theory further, but are maybe wondering where to start, we have a few suggestions as to what you could do going forward

### 1. Explore and help build the QuantumGameTheory package
Although we have showcased the QuantumGameTheory package in this tutorial, the code is still very much in progress and there are a many more improvements that could be made to it. Some ideas include
* decomposing the $J$ and $J^\dagger$ gates into single and 2-qubit gates, such that games with more than 2 players can be played on real quantum computers
* integrate using mixed strategies into the package (currently the randomization has to be done before inputting the strategies into the Game object)
* Implement the use of arbitrary rotation and/or unitary gates as strategies (currently only certain fixed gates can be used as strategies)

And of course whatever you think would be an interesting addition to the package! We are always happy to hear new ideas and collaborate so please feel free to reach out to us :)

### 2. Play around with the QuantumGameTheory package and start finding interesting results
We have only shown the very basics of quantum game theory, and there is so much left to explore, ranging from different protocols (try using the ['MW' protocol](https://www.sciencedirect.com/science/article/abs/pii/S0375960100004412?via%3Dihub) in our package!), to multiplayer and multidecision games, finding applications in optimization problems etc. A good place to start looking for inspiration might be the [review by Khan et al](https://link.springer.com/article/10.1007/s11128-018-2082-8) on quanutm game theory. We hope that as we add more features to the QuantumGameTheory package that people will be able to use it as tool to study this fascinating field. Play on playas!

## References
[1] “Chapter 1: Decision-Theoretic Foundations.” [_Game Theory: Analysis of Conflict_](https://www.jstor.org/stable/j.ctvjsf522), by Roger B. Myerson, Harvard University Press, 1997.

[2] “Chapter 2: Nash Equilibrium.” [_A Course in Game Theory_](http://ebour.com.ar/pdfs/A%20Course%20in%20Game%20Theory.pdf), by Martin J. Osborne and Ariel Rubinstein, MIT Press, 1994.

[3] Meyer, David A. “Quantum Strategies.” _Physical Review Letters_ , vol. 82, no. 5, 1999, pp. 1052–1055., doi:10.1103/physrevlett.82.1052.

[4] Eisert, Jens, et al. “Quantum Games and Quantum Strategies.” _Physical Review Letters_ , vol. 83, no. 15, 1999, pp. 3077–3080., doi:10.1103/physrevlett.83.3077.

[5] Benjamin, Simon C., and Patrick M. Hayden. “Multiplayer Quantum Games.” _Physical Review A_ , vol. 64, no. 3, 2001, doi:10.1103/physreva.64.030301.

[6] Benjamin, Simon C., and Patrick M. Hayden. “Comment on ‘Quantum Games and Quantum Strategies''.” _Physical Review Letters_ , vol. 87, no. 6, 2001, doi:10.1103/physrevlett.87.069801.


## Data Analytics

### Basic Univariate Statistics in Python

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

### Data Analytics: Basic Univariate Statistics

Here's a demonstration of calculation of univariate statistics in Python. This demonstration is part of the resources that I include for my courses in Spatial / Subsurface Data Analytics and Geostatistics at the Cockrell School of Engineering and Jackson School of Goesciences at the University of Texas at Austin.

We will cover the following statistics:

#### Measures of Centrality
* Arithmetic Average / Mean
* Median
* Mode (most frequent binned)
* Geometric Mean
* Harmonic Mean
* Power Law Average

#### Measures of Dispersion
* Population Variance
* Sample Variance
* Population Standard Deviation
* Sample Standard Deviation
* Range
* Percentile w. Tail Assumptions
* Interquartile Range

#### Tukey Outlier Test
* Lower Quartile/P25
* Upper Quartile/P75
* Interquartile Range
* Lower Fence
* Upper Fence
* Calculating Outliers

#### Measures of Shape
* Skew
* Excess Kurtosis
* Pearson' Mode Skewness
* Quartile Skew Coefficient

#### Nonparmetric Cumulative Distribution Functions (CDFs)
* plotting a nonparametric CDF
* fitting a parametric distribution and plotting

I have a lecture on these univariate statistics available on [YouTube](https://www.youtube.com/watch?v=wAcbA2cIqec&list=PLG19vXLQHvSB-D4XKYieEku9GQMQyAzjJ&index=11&t=0s).

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

You will need to copy the data file to your working directory.  The dataset is available on my GitHub account in my GeoDataSets repository at:

* Tabular data - [2D_MV_200wells.csv](https://github.com/GeostatsGuy/GeoDataSets/blob/master/2D_MV_200wells.csv)

#### Importing Packages

We will need some standard packages. These should have been installed with Anaconda 3.

```python
import numpy as np                        # ndarrys for gridded data
import pandas as pd                       # DataFrames for tabular data
import os                                 # set working directory, run executables
import matplotlib.pyplot as plt           # plotting
from scipy import stats                   # summary statistics
import scipy                              # statistics
import statistics as stats                # statistics like the mode
from scipy.stats import norm              # fitting a Gaussian distribution
```

#### Set the Working Directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time). Set this to your working directory, with the above mentioned data file.

```python
#os.chdir("c:/PGE383")                     # set the working directory
```

#### Loading Data

Let's load the provided multivariate, spatial dataset.  '2D_MV_200wells.csv' is available at https://github.com/GeostatsGuy/GeoDataSets.  It is a comma delimited file with X and Y coordinates,facies 1 and 2 (1 is sandstone and 2 interbedded sand and mudstone), porosity (fraction), permeability (mDarcy) and acoustic impedance (kg/m2s*10^6). We load it with the pandas 'read_csv' function into a data frame we called 'df' and then preview it by printing a slice and by utilizing the 'head' DataFrame member function (with a nice and clean format, see below).

```python
#df = pd.read_csv("2D_MV_200wells.csv")                      # read a .csv file in as a DataFrame
df = pd.read_csv("https://raw.githubusercontent.com/GeostatsGuy/GeoDataSets/master/2D_MV_200wells.csv")
#print(df.iloc[0:5,:])                                       # display first 4 samples in the table as a preview
df.head()                                                   # we could also use this command for a table preview
```

      X
      Y
      facies_threshold_0.3
      porosity
      permeability
      acoustic_impedance

      0
      565
      1485
      1
      0.1184
      6.170
      2.009

      1
      2585
      1185
      1
      0.1566
      6.275
      2.864

      2
      2065
      2865
      2
      0.1920
      92.297
      3.524

      3
      3575
      2655
      1
      0.1621
      9.048
      2.157

      4
      1835
      35
      1
      0.1766
      7.123
      3.979

Let's extract one of the features, porosity, into a 1D ndarray and do our statistics on porosity.

* then we can use NumPy's statistics methods

```python
por = df['porosity'].values
```

```python
por
```

    array([0.1184 , 0.1566 , 0.192  , 0.1621 , 0.1766 , 0.1239 , 0.179  ,
           0.1914 , 0.1763 , 0.1674 , 0.1369 , 0.1095 , 0.1677 , 0.1817 ,
           0.119  , 0.1774 , 0.1418 , 0.1296 , 0.1325 , 0.1729 , 0.08494,
           0.188  , 0.1147 , 0.1795 , 0.1981 , 0.1258 , 0.215  , 0.1781 ,
           0.1428 , 0.1495 , 0.1435 , 0.1026 , 0.1772 , 0.19   , 0.1597 ,
           0.1453 , 0.1653 , 0.176  , 0.2232 , 0.1336 , 0.1819 , 0.1591 ,
           0.1328 , 0.1517 , 0.1061 , 0.08187, 0.1682 , 0.1367 , 0.1628 ,
           0.1305 , 0.1097 , 0.2062 , 0.168  , 0.1663 , 0.1231 , 0.1965 ,
           0.1292 , 0.1347 , 0.1481 , 0.1523 , 0.1388 , 0.1574 , 0.1769 ,
           0.1982 , 0.161  , 0.1584 , 0.1594 , 0.1146 , 0.1367 , 0.1742 ,
           0.1585 , 0.1065 , 0.181  , 0.1372 , 0.1384 , 0.0991 , 0.137  ,
           0.1957 , 0.1032 , 0.1973 , 0.1336 , 0.1792 , 0.152  , 0.08245,
           0.1505 , 0.0938 , 0.1563 , 0.1277 , 0.141  , 0.1712 , 0.1428 ,
           0.07338, 0.1595 , 0.1079 , 0.1191 , 0.132  , 0.1394 , 0.2154 ,
           0.1428 , 0.1416 , 0.1658 , 0.187  , 0.1796 , 0.177  , 0.1714 ,
           0.1438 , 0.186  , 0.0964 , 0.1455 , 0.1322 , 0.06726, 0.1162 ,
           0.1021 , 0.1396 , 0.1139 , 0.1576 , 0.1417 , 0.2159 , 0.1352 ,
           0.0964 , 0.1853 , 0.1898 , 0.1785 , 0.1648 , 0.1367 , 0.1644 ,
           0.1376 , 0.1974 , 0.1498 , 0.1524 , 0.1579 , 0.147  , 0.1105 ,
           0.1825 , 0.1227 , 0.1414 , 0.1347 , 0.165  , 0.2019 , 0.1487 ,
           0.1784 , 0.0784 , 0.1061 , 0.1491 , 0.0915 , 0.1677 , 0.1811 ,
           0.1377 , 0.2158 , 0.1356 , 0.1062 , 0.132  , 0.05   , 0.08678,
           0.1474 , 0.164  , 0.15   , 0.1606 , 0.1478 , 0.1645 , 0.1415 ,
           0.1821 , 0.1319 , 0.08012, 0.2158 , 0.177  , 0.08209, 0.1254 ,
           0.1611 , 0.1916 , 0.1742 , 0.2063 , 0.122  , 0.1541 , 0.1435 ,
           0.1505 , 0.192  , 0.1104 , 0.1717 , 0.1394 , 0.1503 , 0.18   ,
           0.164  , 0.1808 , 0.1577 , 0.1211 , 0.1523 , 0.1467 , 0.1591 ,
           0.1367 , 0.192  , 0.1359 , 0.1513 , 0.1473 , 0.1377 , 0.1634 ,
           0.1548 , 0.1321 , 0.06092, 0.1505 ])

Now let's go through all the univariate statistics listed above one-by-one.

#### Measures of Central Tendency

Let's start with measures of central tendency.

##### The Arithmetic Average / Mean

\begin{equation}
\overline{x} = \frac{1}{n}\sum^n_{i=1} x_i
\end{equation}

```python
por_average = np.average(por)
print('Porosity average is ' + str(round(por_average,2)) + '.')
```

    Porosity average is 0.15.

##### Median

\begin{equation}
P50_x = F^{-1}_{x}(0.50)
\end{equation}

```python
por_median = np.median(por)
print('Porosity median is ' + str(round(por_median,2)) + '.')
```

    Porosity median is 0.15.

##### Mode

The most common value. To do this we should bin the data, like into histogram bins/bars.  To do this we will round the data to the 2nd decimal place.  We are assume bin boundaries, $0.01, 0.02,\ldots, 0.30$.

```python
por_mode = stats.mode(np.round(por,2))
print('Porosity mode is ' + str(round(por_mode,2)) + '.')
```

    Porosity mode is 0.14.

##### Geometric Mean

\begin{equation}
\overline{x}_G = ( \prod^n_{i=1} x_i )^{\frac{1}{n}}
\end{equation}

```python
por_geometric = scipy.stats.mstats.gmean(por)
print('Porosity geometric mean is ' + str(round(por_geometric,2)) + '.')
```

    Porosity geometric mean is 0.15.

##### Harmonic Mean

\begin{equation}
\overline{x}_H = \frac{n}{\sum^n_{i=1} \frac{1}{x_i}}
\end{equation}

```python
por_hmean = scipy.stats.mstats.hmean(por)
print('Porosity harmonic mean is ' + str(round(por_hmean,2)) + '.')
```

    Porosity harmonic mean is 0.14.

##### Power Law Average

\begin{equation}
\overline{x}_p = (\frac{1}{n}\sum^n_{i=1}{x_i^{p}})^\frac{1}{p}
\end{equation}

```python
power = 0.3
por_power = np.average(np.power(por,power))**(1/power)
print('Porosity law mean for p = ' + str(power) + ' is ' + str(round(por_power,2)) + '.')
```

    Porosity law mean for p = 0.3 is 0.15.

#### Measures of Dispersion

##### Population Variance

\begin{equation}
\sigma^2_{x} = \frac{1}{n}\sum^n_{i=1}(x_i - \mu)
\end{equation}

```python
por_varp = stats.pvariance(por)
print('Porosity population variance is ' + str(round(por_varp,4)) + '.')
```

    Porosity population variance is 0.0011.

##### Sample Variance

\begin{equation}
\sigma^2_{x} = \frac{1}{n-1}\sum^n_{i=1}(x_i - \overline{x})^2
\end{equation}

```python
por_var = stats.variance(por)
print('Porosity sample variance is ' + str(round(por_var,4)) + '.')
```

    Porosity sample variance is 0.0011.

##### Population Standard Deviation

\begin{equation}
\sigma_{x} = \sqrt{ \frac{1}{n}\sum^n_{i=1}(x_i - \mu)^2 }
\end{equation}

```python
por_stdp = stats.pstdev(por)
print('Porosity sample variance is ' + str(round(por_stdp,4)) + '.')
```

    Porosity sample variance is 0.0329.

##### Sample Standard Deviation

\begin{equation}
\sigma_{x} = \sqrt{ \frac{1}{n-1}\sum^n_{i=1}(x_i - \mu)^2 }
\end{equation}

```python
por_std = stats.stdev(por)
print('Porosity sample variance is ' + str(round(por_std,4)) + '.')
```

    Porosity sample variance is 0.0329.

##### Range

\begin{equation}
range_x = P100_x - P00_x
\end{equation}

```python
por_range = np.max(por) - np.min(por)
print('Porosity range is ' + str(round(por_range,2)) + '.')
```

    Porosity range is 0.17.

##### Percentile

\begin{equation}
P(p)_x = F^{-1}_{x}(p)
\end{equation}

```python
p_value = 13
por_percentile = np.percentile(por,p_value)
print('Porosity ' + str(int(p_value)) + 'th percentile is ' + str(round(por_percentile,2)) + '.')
```

    Porosity 13th percentile is 0.11.

##### Inter Quartile Range

\begin{equation}
IQR = P(0.75)_x - P(0.25)_x
\end{equation}

```python
por_iqr = scipy.stats.iqr(por)
print('Porosity interquartile range is ' + str(round(por_iqr,2)) + '.')
```

    Porosity interquartile range is 0.04.

#### Tukey Test for Outliers

Let's demonstrate the Tukey test for outliers based on the lower and upper fences.

\begin{equation}
fence_{lower} = P_x(0.25) - 1.5 \times [P_x(0.75) - P_x(0.25)]
\end{equation}

\begin{equation}
fence_{upper} = P_x(0.75) + 1.5 \times [P_x(0.75) - P_x(0.25)]
\end{equation}

Then we declare samples values above the upper fence or below the lower fence as outliers.

```python
p25, p75 = np.percentile(por, [25, 75])
lower_fence = p25 - por_iqr * 1.5
upper_fence = p75 + por_iqr * 1.5
por_outliers = por[np.where((por > upper_fence) | (por < lower_fence))[0]]
print('Porosity outliers by Tukey test include ' + str(por_outliers) + '.')
por_outliers_indices = np.where((por > upper_fence) | (por < lower_fence))[0]
print('Porosity outlier indices by Tukey test are ' + str(por_outliers_indices) + '.')
```

    Porosity outliers by Tukey test include [0.06726 0.05    0.06092].
    Porosity outlier indices by Tukey test are [110 152 198].

#### Measures of Shape

##### Pearson's Mode Skewness

\begin{equation}
skew = \frac{(\overline{x} - P50_x)}{\sigma_x}
\end{equation}

```python
por_skew = (por_average - por_mode)/por_std
print('Porosity skew is ' + str(round(por_skew,2)) + '.')
```

    Porosity skew is 0.28.

##### Population Skew, 3rd Central Moment

\begin{equation}
\gamma_{x} = \frac{1}{n}\sum^n_{i=1}(x_i - \mu)^3
\end{equation}

```python
por_cm = scipy.stats.moment(por,moment=3)
print('Porosity 3rd cenral moment is ' + str(round(por_cm,7)) + '.')
```

    Porosity 3rd cenral moment is -1.22e-05.

##### Quartile Skew Coefficient

\begin{equation}
QS = \frac{(P75_x - P50_x) - (P50_x - P25_x)}{(P75_x - P25_x)}
\end{equation}

```python
por_qs = ((np.percentile(por,75)-np.percentile(por,50))
          -(np.percentile(por,50)-np.percentile(por,25))) /((np.percentile(por,75))-np.percentile(por,25))
print('Porosity quartile skew coefficient is ' + str(round(por_qs,2)) + '.')
```

#### Excess Kurtosis

\begin{equation}
kurt_{x} = \frac{1}{n}\sum^n_{i=1}(x_i - \mu)^4 - 3
\end{equation}

```python
por_kurt1 = scipy.stats.moment(por,moment=4) - 3.0
por_kurt2 = scipy.stats.kurtosis(por) - 3.0
print('Porosity excess kurtosis from 4th centered momement minus 3.0: ' + str(round(por_kurt1,6)))
print('Porosity excess kurtosis from kurtosis function minus 3.0: ' + str(round(por_kurt1,6)))
```

    Porosity excess kurtosis from 4th centered momement minus 3.0: -2.999996
    Porosity excess kurtosis from kurtosis function minus 3.0: -2.999996

#### Plot the Nonparametric CDF

Let's demonstrate plotting a nonparametric cumulative distribution function (CDF) in Python

```python
# sort the data:
por_sort = np.sort(por)

# calculate the cumulative probabilities assuming known tails
p = np.arange(len(por)) / (len(por) - 1)

# plot the cumulative probabilities vs. the sorted porosity values
plt.subplot(122)
plt.scatter(por_sort, p, c = 'red', edgecolors = 'black', s = 10, alpha = 0.7)
plt.xlabel('Porosity (fraction)'); plt.ylabel('Cumulative Probability'); plt.grid();
plt.title('Nonparametric Porosity CDF')
plt.ylim([0,1]); plt.xlim([0,0.25])
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.2, wspace=0.2, hspace=0.3)
```

#### Fit a Gaussian Distribution

Let's fit a Gaussian distribution

* we get fancy with Maximuum Likelihood Estimation (MLE) for the Gaussian parametric distribution fit mean and standard deviation

```python
por_values = np.linspace(0.0,0.25,100)
fit_mean, fit_stdev = norm.fit(por,loc = por_average, scale = por_std) # fit MLE of the distribution
cumul_p = norm.cdf(por_values, loc = fit_mean, scale = fit_stdev)

# plot the cumulative probabilities vs. the sorted porosity values
plt.subplot(122)
plt.scatter(por_sort, p, c = 'red', edgecolors = 'black', s = 10, alpha = 0.7)
plt.plot(por_values,cumul_p, c = 'black')
plt.xlabel('Porosity (fraction)'); plt.ylabel('Cumulative Probability'); plt.grid();
plt.title('Nonparametric Porosity CDF')
plt.ylim([0,1]); plt.xlim([0,0.25])
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.2, wspace=0.2, hspace=0.3)
```

#### Comments

This was a basic demonstration of univariate statistics in Python.

I have other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling and many other workflows available at [Python Demos](https://github.com/GeostatsGuy/PythonNumericalDemos) and a Python package for data analytics and geostatistics at [GeostatsPy](https://github.com/GeostatsGuy/GeostatsPy).

I hope this was helpful,

*Michael*

#### The Author:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```


```python
#%matplotlib ipympl
%matplotlib inline
```

For high dpi displays.

```python
%config InlineBackend.figure_format = 'retina'
```

# 0. Note

This notebook demonstrates how different physical parameters change the equation of state.

# 1. Setup

```python
import matplotlib.pyplot as plt
import numpy as np
from uncertainties import unumpy as unp
import pytheos as eos
from uncertainties import ufloat
```

In the Mie-Gruneisen approach, we divide total pressure, $P(V,T)$, into static pressure, $P(V,T_0)$, which is a reference isotherm (normally $T_0 = 300$ K), and thermal pressure, $\Delta P_\mathrm{th}(V,T)$, which is a pressure difference between 300-K and high-temperature isotherms.

\begin{equation}
P(V,T) = P(V,T_0) + \Delta P_\mathrm{th}(V,T).\label{eq:eos-totalP}
\end{equation}

# 2. Static pressure

We will use the Birch-Murnathan equation for the static part.

\begin{equation}
P = \frac{3K_0}{2} \left[ \left(\frac{V_0}{V}\right)^{7/3} -
\left(\frac{V_0}{V}\right)^{5/3} \right]\left\{
1-\xi\left[\left(\frac{V_0}{V}\right)^{2/3} -1\right] \right\},
\end{equation}
where $K_0$ is the bulk modulus (in GPa), $K'_0 = (dK/dP)$, and $\xi = \frac{3}{4}(4-K_0')$.

Generate volume strain: $\eta = V/V_0$.

```python
eta = np.linspace(1., 0.75, 21)
print(eta)
```

Suppose you have a material with the following properties measured at 300 K.

```python
v0 = 163. # A^3
k0 = ufloat(260., 3.) # GPa
k0p = ufloat(4., 0.2)
```

```python
v = unp.uarray(eta * v0, 0.05)
```

Then you can calculate pressures for the volumes at 300 K: a 300-K isotherm.

```python
p_st = eos.bm3_p(v, v0, k0, k0p)
print(p_st)
```

```python
plt.errorbar(unp.nominal_values(p_st), unp.nominal_values(v), \
            xerr=unp.std_devs(p_st), yerr=unp.std_devs(v))
plt.xlabel('Pressure')
plt.ylabel('Volume')
```

## Try these...

- Increase bulk modulus, $K_0$, to 300 GPa.

- Increase pressure derivative of bulk modulus, $K'_0$, to 3 and 5.

What did you find?

Can you convert this into a density profile along an isotherm?

# 3. Thermal pressure

Now let's calculate thermal pressure.  We will use the Debye approach.

\begin{equation}
\Delta P_\mathrm{th}(V,T) = \frac{\gamma(V)}{V}\Delta E_\mathrm{th}[\theta_D(V),T],\label{eq:eos-MGD}
\end{equation}
where $\gamma$ is the Gruneisen parameter ($= V (\partial P/\partial E)_V$), and $T_0=300$ K in most experiments.  The internal energy change can be calculated from the Debye model:

\begin{equation}
E_\mathrm{th} = \frac{9nRT}{x^3}\int^x _0 \frac{\xi^3}{e^\xi -1} d\xi,
\end{equation}
where $R$ is the gas constant, $x = \dfrac{\theta_D}{T}$, and $n$ is the number of atoms per formula unit.

Also,

\begin{equation}
\gamma = \gamma_0 \left( \dfrac{V}{V_0} \right) ^q
\end{equation}

We need some parameters for the Debye thermal pressure equation.

```python
temp = 2000.
gamma0 = ufloat(1.5, 0.3)
q = ufloat(1., 0.5)
theta0 = ufloat(1000., 50.)
```

```python
n = 5.
z = 4.
```

```python
p_th = eos.constq_pth(v, temp, v0, gamma0, q, theta0, n, z)
```

```python
p_ht = p_st + p_th
print(p_ht)
```

Let's plot 300-K and 3000-K isotherms together.

```python
plt.errorbar(unp.nominal_values(p_st), unp.nominal_values(v), xerr=unp.std_devs(p_st))
plt.errorbar(unp.nominal_values(p_ht), unp.nominal_values(v), xerr=unp.std_devs(p_ht))
plt.xlabel('Pressure')
plt.ylabel('Volume')
```

## Try these...

- Increase Gruneisen parameter, $\gamma_0$, to 3.

- Increase logarithmic volume dependence of $\gamma$, $q$, to 0.1 and 2.

What did you find?

Can you convert this into a density profile along an isotherm?

# 4. Volume expansion

```python
import pytheos.scales.objs
from collections import OrderedDict
```

```python
params_st = OrderedDict([('v0', ufloat(v0, 0.01)),
                         ('k0', k0),
                         ('k0p', k0p)])
params_th = OrderedDict([('v0', ufloat(v0, 0.01)),
                         ('gamma0', gamma0),
                         ('q', q),
                         ('theta0', theta0)])
brd = pytheos.scales.objs.MGEOS(n, z, params_st=params_st, params_th=params_th,
               eqn_st='bm3', eqn_th='constq')
```

```python
p_lm = np.linspace(25., 125., 101)
```

```python
v_r = brd.cal_v(p_lm, np.ones_like(p_lm)*1500.)
v_ht = brd.cal_v(p_lm, np.ones_like(p_lm)*2000.)
```

```python
plt.plot(p_lm, (v_ht - v_r) / v_r * 100.)

plt.ylabel('$\Delta V / V$ (%)')
plt.xlabel('Pressure (GPa)')
```