「PyTorch入門  5. 自動微分」
===============================================================
【原題】AUTOMATIC DIFFERENTIATION WITH TORCH.AUTOGRAD

【原著】
[Suraj Subramanian](https://github.com/suraj813)、[Seth Juarez](https://github.com/sethjuarez/) 、[Cassie Breviu](https://github.com/cassieview/) 、[Dmitry Soshnikov](https://soshnikov.com/)、[Ari Bornstein](https://github.com/aribornstein/)

【元URL】https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html

【翻訳】電通国際情報サービスISID AIトランスフォーメーションセンター　小川 雄太郎

【日付】2021年03月18日

【チュートリアル概要】

本チュートリアルでは、PyTorchで計算履歴を保存し、自動で微分操作を実現するAUTOGRADについて解説を行います。

---

Automatic Differentiation with ``torch.autograd``
=======================================

ニューラルネットワークを訓練する際、その学習アルゴリズムとして、基本的には**バックプロパゲーション（back propagation）**が使用されます。

バックプロパゲーションでは、モデルの重みなどの各パラメータは、損失関数に対するその変数の微分値（勾配）に応じて調整されます。

これらの勾配の値を計算するために、PyTorchには``torch.autograd`` という微分エンジンが組み込まれています。

autogradはPyTorchの計算グラフに対する勾配の自動計算を支援します。

シンプルな1レイヤーのネットワークを想定しましょう。

入力を``x``、パラメータを``w`` と ``b``、そして適切な損失関数を決めます。

PyTorchでは例えば以下のように実装します。

```python
%matplotlib inline
```

```python
import torch

x = torch.ones(5)  # input tensor
y = torch.zeros(3)  # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
```

---

テンソル、関数、計算グラフの関係
------------------------------------------

上記のコードは以下の**計算グラフ(computational graph)**を示しています。

上記のネットワークでは、``w``と``b``が最適したいパラメータです。

そのため、これらの変数に対する損失関数の微分値を計算する必要があります。

これらのパラメータで微分を可能にするために、``requires_grad``属性をこれらのテンソルに追記します。

【注意】

``requires_grad``はテンソルを定義する際、もしくはその後に、``x.requires_grad_(True)``を実行するなどして指定できます。

計算グラフを構築する際に、テンソルに適用する関数は実際には ``Function``クラスのオブジェクトです。

これらオブジェクトでは、順伝搬時に入力をどのように処理するのか定義されています。

加えて、バックプロパゲーション時に勾配をどのように計算するのかも把握しています。

そして勾配は、テンソルの ``grad_fn`` プロパティに格納されます。

 ``Function``のさらなる詳細については、[こちら](https://pytorch.org/docs/stable/autograd.html#function)を参照ください。

```python
print('Gradient function for z =',z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
```

    Gradient function for z = <AddBackward0 object at 0x7f7a86429b10>
    Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward object at 0x7f7a86429b50>

---

勾配の計算
-------------------
ニューラルネットワークの各パラメータを最適化するために、入力``x``と出力``y``が与えられたもとで、損失関数の各変数の偏微分値、

すなわち

$\frac{\partial loss}{\partial w}$ 、$\frac{\partial loss}{\partial b}$

を求める必要があります。

これらの偏微分値を求めるために``loss.backward()``を実行し、``w.grad``と``b.grad``の値を導出します。

```python
loss.backward()
print(w.grad)
print(b.grad)
```

    tensor([[0.1212, 0.1953, 0.3046],
            [0.1212, 0.1953, 0.3046],
            [0.1212, 0.1953, 0.3046],
            [0.1212, 0.1953, 0.3046],
            [0.1212, 0.1953, 0.3046]])
    tensor([0.1212, 0.1953, 0.3046])

【注意】

``grad``は計算グラフのleaf node（かつ、``requires_grad``が``True``の変数）のみで求めることができます。

全ての変数で勾配が計算できるわけではない点にはご注意ください。

また、勾配の計算は各計算グラフに対して、``backward``を実行し、1度だけ計算できます（1度しか計算できないのは、パフォーマンスの観点からの仕様です）。

仮に、同じ計算グラフに対して複数回``backward``を実行したい場合には、``retain_graph=True``を``backward``時に引数として渡す必要があります。

---

勾配計算をしない方法
---------------------------

デフォルトでは、``requires_grad=True``である全てのテンソルは計算履歴が保持され、勾配計算可能な状態です。

ですが、勾配計算が不要なケースも存在します。

例えば訓練済みモデルで推論するケースなどです。

すなわち、ネットワークの順伝搬関数のみを使用する場合となります。

実装コードで勾配計算を不要にするには、``torch.no_grad()``のブロックにそれらのコードを記載するようにします。

```python
z = torch.matmul(x, w)+b
print(z.requires_grad)

with torch.no_grad():
    z = torch.matmul(x, w)+b
print(z.requires_grad)
```

    True
    False

同様に、``detach()`` をテンソルに使用することでも実現できます。

```python
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
```

    False

勾配の計算、追跡を不能にしたいケースはいく種類かあります。

- ネットワークの一部のパラメータを固定したい（frozen parameters）ケース。これは[ファインチューニング](https://colab.research.google.com/github/YutaroOgawa/pytorch_tutorials_jp/blob/main/notebook/2_Image_Video/2_1_transfer_learning_tutorial_jp.ipynb)時によくあるケースです。

- 順伝搬の計算スピードを高速化したいケース。

---

計算グラフについて補足
----------------------------

概念的には、autogradはテンソルとそれらに対する演算処理を[`Function`](https://pytorch.org/docs/stable/autograd.html#torch.autograd.Function)を構成単位として、DAG（a directed acyclic graph）の形で保存したグラフです。

DAGにおいて、各leafは入力テンソル、そしてrootは出力テンソルです。

このグラフをrootから各leafまでchain rule（微分の連鎖律）で追跡することによって各変数に対する偏微分の値を求めることができます。

順伝搬では autograd は2つの処理を同時に行っています。

- 指定された演算を実行し、計算結果のテンソルを求める
- DAGの各操作の*gradient function* を更新する

逆伝搬では、``.backward()``がDAGのrootのテンソルに対して実行されると、autogradは、

- 各変数の ``.grad_fn``を計算する
- 各変数の``.grad``属性に微分値を代入する
- 微分の連鎖律を使用して、各leafのテンソルの微分値を求める

を行います。

【注意】

PyTorchではDAGは動的です（Functionで計算処理される際に逐次構築されていきます）。

そして、`.backward()`を呼び出すたびに、autogradは再度新しいグラフを作成します。

この特性こそが、モデルの順伝搬時に制御フロー文（if文やfor文）を使える理由であり、必要に応じて反復ごとに形や大きさ、操作を変えることができます。

【日本語訳注】

上記内容は初心者の方には非常に難しい話です。

PyTorchは Define-by-run 形式であり、事前に計算グラフを定義するのではなく、計算を実行する際に、柔軟に計算グラフを作ってくれます。

一方で、Define-and-run形式のディープラーニングフレームワーク（例えば、TensorFlow v1など）は、事前に計算グラフを定義する必要があるため、for文やif文といった制御フローの構文を柔軟に使いづらいです。

このことを上記内容では説明しています。

---

補注：テンソルに対する勾配とヤコビ行列
--------------------------------------

多くの場合、スカラー値を出力する損失関数に対して、とある変数の勾配を計算します。

ですが、関数の出力がスカラー値ではなく、任意のテンソルであるケースもあります。

このような場合、PyTorchでは実際の勾配ではなく、いわゆるヤコビ行列（Jacobian
matrix）を計算することができます。

ベクトル関数 $\vec{y}=f(\vec{x})$,において、
$\vec{x}=\langle x_1,\dots,x_n\rangle$ 、そして　$\vec{y}=\langle y_1,\dots,y_m\rangle$の場合、

その勾配、 $\vec{y}$ with respect to $\vec{x}$ はヤコビ行列 で与えられます。

\begin{split}\begin{align}J=\left(\begin{array}{ccc}
   \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\
   \vdots & \ddots & \vdots\\
   \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}
   \end{array}\right)\end{align}\end{split}

ヤコビ行列そのものを計算する代わりにPyTorchでは、**Jacobian Product**、 $v^T\cdot J$ を、入力ベクトル$v=(v_1 \dots v_m)$ に対して計算します。

これは、$v$を引数として ``backward``メソッドを呼び出すことで計算されます。

なお$v$ のサイズは積を計算したい、元のテンソルの大きさと同じである必要があります。

```python
inp = torch.eye(5, requires_grad=True)
out = (inp+1).pow(2)
out.backward(torch.ones_like(inp), retain_graph=True)
print("First call\n", inp.grad)
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nSecond call\n", inp.grad)
inp.grad.zero_()
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nCall after zeroing gradients\n", inp.grad)
```

    First call
     tensor([[4., 2., 2., 2., 2.],
            [2., 4., 2., 2., 2.],
            [2., 2., 4., 2., 2.],
            [2., 2., 2., 4., 2.],
            [2., 2., 2., 2., 4.]])

    Second call
     tensor([[8., 4., 4., 4., 4.],
            [4., 8., 4., 4., 4.],
            [4., 4., 8., 4., 4.],
            [4., 4., 4., 8., 4.],
            [4., 4., 4., 4., 8.]])

    Call after zeroing gradients
     tensor([[4., 2., 2., 2., 2.],
            [2., 4., 2., 2., 2.],
            [2., 2., 4., 2., 2.],
            [2., 2., 2., 4., 2.],
            [2., 2., 2., 2., 4.]])

上記において、同じ変数`inp`に対して、``backward``を2回目実行した際には、勾配が異なる値になった点に注意してください。

これはPyTorchでは``backward``を実行すると、勾配を蓄積（accumulate）する仕様だからです。

すなわち、計算グラフの全leafの``grad``には、勾配が足し算されます。

そのため適切に勾配を計算するには、``grad``を事前に0にリセットする必要があります。

なお実際にPyTorchでディープラーニングモデルの訓練を行う際には、**オプティマイザー（optimizer）**が、勾配をリセットする役割を担ってくれます。

【注意】

本チュートリアルの最初の方で、``backward()``関数を引数のパラメータなしに実行していました。

これは実質的には、``backward(torch.tensor(1.0))``を実行しているのと同じとなります。

``backward()``はスカラー値の関数（例えば訓練時の損失関数）に対して、各パラメータの勾配を計算する際に便利です。

--------------

さらなる詳細
--------------
以下のページも参考ください。

- [`Autograd Mechanics`](https://pytorch.org/docs/stable/notes/autograd.html)

以上。


# Converting between the 4-metric $g_{\mu\nu}$ and ADM variables $\left\{\gamma_{ij}, \alpha, \beta^i\right\}$ or BSSN variables $\left\{h_{ij}, {\rm cf}, \alpha, {\rm vet}^i\right\}$
## Author: Zach Etienne

[comment]: <> (Abstract: TODO)

### We will often find it useful to convert between the 4-metric $g_{\mu\nu}$ and the ADM or BSSN variables. This notebook documents the NRPy+ Python module [`BSSN.ADMBSSN_tofrom_4metric`](../edit/BSSN/ADMBSSN_tofrom_4metric.py), which provides that functionality.

**Notebook Status:**  Self-validated, some additional tests performed

**Validation Notes:** This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). In addition, the construction of $g_{\mu\nu}$ and $g^{\mu\nu}$ from BSSN variables has passed the test $g^{\mu\nu}g_{\mu\nu}=4$ [below](#validationcontraction). **Additional validation tests may have been performed, but are as yet, undocumented. (TODO)**

### NRPy+ Source Code for this module: [BSSN/ADMBSSN_tofrom_4metric.py](../edit/BSSN/ADMBSSN_tofrom_4metric.py)

## Introduction:

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#setup_ADM_quantities): `setup_ADM_quantities(inputvars)`: If `inputvars="ADM"` declare ADM quantities $\left\{\gamma_{ij},\beta^i,\alpha\right\}$; if `inputvars="ADM"` define ADM quantities in terms of BSSN quantities
1. [Step 2](#admbssn_to_fourmetric): Write 4-metric $g_{\mu\nu}$ and its inverse $g^{\mu\nu}$ in terms of ADM or BSSN quantities
    1. [Step 2.a](#admbssn_to_fourmetric_lower): 4-metric $g_{\mu\nu}$ in terms of ADM or BSSN quantities
    1. [Step 2.b](#admbssn_to_fourmetric_inv): 4-metric inverse $g^{\mu\nu}$ in terms of ADM or BSSN quantities
    1. [Step 2.c](#validationcontraction): Validation check: Confirm $g_{\mu\nu}g^{\mu\nu}=4$
1. [Step 3](#fourmetric_to_admbssn): Write ADM/BSSN metric quantities in terms of 4-metric $g_{\mu\nu}$ (Excludes extrinsic curvature $K_{ij}$ or the BSSN $\bar{A}_{ij}$, $K$)
    1. [Step 3.a](#adm_ito_fourmetric_validate): ADM in terms of 4-metric validation: Confirm $\gamma_{ij}\gamma^{ij}=3$
    1. [Step 3.b](#bssn_ito_fourmetric_validate): BSSN in terms of 4-metric validation: Confirm $\bar{\gamma}_{ij}\bar{\gamma}^{ij}=3$
1. [Step 4](#code_validation): Code Validation against `BSSN.ADMBSSN_tofrom_4metric` NRPy+ module
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: `setup_ADM_quantities(inputvars)`: If `inputvars="ADM"` declare ADM quantities $\left\{\gamma_{ij},\beta^i,\alpha\right\}$; if `inputvars="ADM"` define ADM quantities in terms of BSSN quantities \[Back to [top](#toc)\]
$$\label{setup_ADM_quantities}$$

```python
import sympy as sp
import NRPy_param_funcs as par
import indexedexp as ixp
import sys

def setup_ADM_quantities(inputvars):
    if inputvars == "ADM":
        gammaDD = ixp.declarerank2("gammaDD", "sym01")
        betaU = ixp.declarerank1("betaU")
        alpha = sp.symbols("alpha", real=True)
    elif inputvars == "BSSN":
        import BSSN.ADM_in_terms_of_BSSN as AitoB

        # Construct gamma_{ij} in terms of cf & gammabar_{ij}
        AitoB.ADM_in_terms_of_BSSN()
        gammaDD = AitoB.gammaDD
        # Next construct beta^i in terms of vet^i and reference metric quantities
        import BSSN.BSSN_quantities as Bq

        Bq.BSSN_basic_tensors()
        betaU = Bq.betaU
        alpha = sp.symbols("alpha", real=True)
    else:
        print("inputvars = " + str(inputvars) + " not supported. Please choose ADM or BSSN.")
        sys.exit(1)
    return gammaDD,betaU,alpha
```

# Step 2: Write 4-metric $g_{\mu\nu}$ and its inverse $g^{\mu\nu}$ in terms of ADM or BSSN variables \[Back to [top](#toc)\]
$$\label{admbssn_to_fourmetric}$$

## Step 2.a: 4-metric $g_{\mu\nu}$ in terms of ADM or BSSN variables \[Back to [top](#toc)\]
$$\label{admbssn_to_fourmetric_lower}$$

Given ADM variables $\left\{\gamma_{ij},\beta^i,\alpha \right\}$, which themselves may be written in terms of the rescaled BSSN curvilinear variables $\left\{h_{ij},{\rm cf},\mathcal{V}^i,\alpha \right\}$ for our chosen reference metric via simple function calls to `ADM_in_terms_of_BSSN()` and `BSSN_quantities.BSSN_basic_tensors()`, we are to construct the 4-metric $g_{\mu\nu}$.

We accomplish this via Eq. 2.122 (which can be trivially derived from the ADM 3+1 line element) of Baumgarte & Shapiro's *Numerical Relativity* (henceforth B&S):
$$
g_{\mu\nu} = \begin{pmatrix}
-\alpha^2 + \beta^k \beta_k & \beta_i \\
\beta_j & \gamma_{ij}
\end{pmatrix},
$$
where the shift vector $\beta^i$ is lowered via (Eq. 2.121):

$$\beta_k = \gamma_{ik} \beta^i.$$

```python
def g4DD_ito_BSSN_or_ADM(inputvars):
    # Step 0: Declare g4DD as globals, to make interfacing with other modules/functions easier
    global g4DD

    # Step 1: Check that inputvars is set to a supported value
    gammaDD,betaU,alpha = setup_ADM_quantities(inputvars)

    # Step 2: Compute g4DD = g_{mu nu}:
    # To get \gamma_{\mu \nu} = gamma4DD[mu][nu], we'll need to construct the 4-metric, using Eq. 2.122 in B&S:
    g4DD = ixp.zerorank2(DIM=4)

    # Step 2.a: Compute beta_i via Eq. 2.121 in B&S
    betaD = ixp.zerorank1()
    for i in range(3):
        for j in range(3):
            betaD[i] += gammaDD[i][j] * betaU[j]

    # Step 2.b: Compute beta_i beta^i, the beta contraction.
    beta2 = sp.sympify(0)
    for i in range(3):
        beta2 += betaU[i] * betaD[i]

    # Step 2.c: Construct g4DD via Eq. 2.122 in B&S
    g4DD[0][0] = -alpha ** 2 + beta2
    for mu in range(1, 4):
        g4DD[mu][0] = g4DD[0][mu] = betaD[mu - 1]
    for mu in range(1, 4):
        for nu in range(1, 4):
            g4DD[mu][nu] = gammaDD[mu - 1][nu - 1]
```

## Step 2.b: Inverse 4-metric $g^{\mu\nu}$ in terms of ADM or BSSN variables \[Back to [top](#toc)\]
$$\label{admbssn_to_fourmetric_inv}$$

B&S also provide a convenient form for the inverse 4-metric (Eq. 2.119; also Eq. 4.49 in [Gourgoulhon](https://arxiv.org/pdf/gr-qc/0703035.pdf)):
$$
g^{\mu\nu} = \gamma^{\mu\nu} - n^\mu n^\nu =
\begin{pmatrix}
-\frac{1}{\alpha^2} & \frac{\beta^i}{\alpha^2} \\
\frac{\beta^i}{\alpha^2} & \gamma^{ij} - \frac{\beta^i\beta^j}{\alpha^2}
\end{pmatrix},
$$
where the unit normal vector to the hypersurface is given by $n^{\mu} = \left(\alpha^{-1},-\beta^i/\alpha\right)$.

```python
def g4UU_ito_BSSN_or_ADM(inputvars):
    # Step 0: Declare g4UU as globals, to make interfacing with other modules/functions easier
    global g4UU

    # Step 1: Check that inputvars is set to a supported value
    gammaDD,betaU,alpha = setup_ADM_quantities(inputvars)

    # Step 2: Compute g4UU = g_{mu nu}:
    # To get \gamma^{\mu \nu} = gamma4UU[mu][nu], we'll need to use Eq. 2.119 in B&S.
    g4UU = ixp.zerorank2(DIM=4)

    # Step 3: Construct g4UU = g^{mu nu}
    # Step 3.a: Compute gammaUU based on provided gammaDD:
    gammaUU, _gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)

    # Then evaluate g4UU:
    g4UU = ixp.zerorank2(DIM=4)

    g4UU[0][0] = -1 / alpha**2
    for mu in range(1,4):
        g4UU[0][mu] = g4UU[mu][0] = betaU[mu-1]/alpha**2
    for mu in range(1,4):
        for nu in range(1,4):
            g4UU[mu][nu] = gammaUU[mu-1][nu-1] - betaU[mu-1]*betaU[nu-1]/alpha**2
```

## Step 2.c: Validation check: Confirm $g_{\mu\nu}g^{\mu\nu}=4$ \[Back to [top](#toc)\]
$$\label{validationcontraction}$$

Next we compute $g^{\mu\nu} g_{\mu\nu}$ as a validation check. It should equal 4:

```python
g4DD_ito_BSSN_or_ADM("BSSN")
g4UU_ito_BSSN_or_ADM("BSSN")
sum = 0
for mu in range(4):
    for nu in range(4):
        sum += g4DD[mu][nu]*g4UU[mu][nu]
if sp.simplify(sum) == sp.sympify(4):
    print("TEST PASSED!")
else:
    print("TEST FAILED: "+str(sum)+" does not apparently equal 4.")
    sys.exit(1)
```

    TEST PASSED!

# Step 3: Write ADM/BSSN metric quantities in terms of 4-metric $g_{\mu\nu}$ (Excludes extrinsic curvature $K_{ij}$, the BSSN $a_{ij}$, $K$, and $\lambda^i$) \[Back to [top](#toc)\]
$$\label{fourmetric_to_admbssn}$$

Given $g_{\mu\nu}$, we now compute ADM/BSSN metric quantities, excluding extrinsic curvature.

Let's start by computing the ADM quantities in terms of the 4-metric $g_{\mu\nu}$

Recall that
$$
g_{\mu\nu} = \begin{pmatrix}
-\alpha^2 + \beta^k \beta_k & \beta_i \\
\beta_j & \gamma_{ij}
\end{pmatrix}.
$$

From this equation we immediately obtain $\gamma_{ij}$. However we need $\beta^i$ and $\alpha$. After computing the inverse of $\gamma_{ij}$, $\gamma^{ij}$, we raise $\beta_j$ via $\beta^i=\gamma^{ij} \beta_j$ and then compute $\alpha$ via $\alpha = \sqrt{\beta^k \beta_k - g_{00}}$. To convert to BSSN variables $\left\{h_{ij},{\rm cf},\mathcal{V}^i,\alpha \right\}$, we need only convert from ADM via function calls to [`BSSN.BSSN_in_terms_of_ADM`](../edit/BSSN/BSSN_in_terms_of_ADM.py) ([**tutorial**](Tutorial-BSSN_in_terms_of_ADM.ipynb)).

```python
def BSSN_or_ADM_ito_g4DD(inputvars):
    # Step 0: Declare output variables as globals, to make interfacing with other modules/functions easier
    if inputvars == "ADM":
        global gammaDD,betaU,alpha
    elif inputvars == "BSSN":
        global hDD,cf,vetU,alpha
    else:
        print("inputvars = " + str(inputvars) + " not supported. Please choose ADM or BSSN.")
        sys.exit(1)

    # Step 1: declare g4DD as symmetric rank-4 tensor:
    g4DD    = ixp.declarerank2("g4DD","sym01",DIM=4)

    # Step 2: Compute gammaDD & betaD
    betaD   = ixp.zerorank1()
    gammaDD = ixp.zerorank2()
    for i in range(3):
        betaD[i] = g4DD[0][i]
        for j in range(3):
            gammaDD[i][j] = g4DD[i+1][j+1]

    # Step 3: Compute betaU
    # Step 3.a: Compute gammaUU based on provided gammaDD
    gammaUU, _gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)

    # Step 3.b: Use gammaUU to raise betaU
    betaU = ixp.zerorank1()
    for i in range(3):
        for j in range(3):
            betaU[i] += gammaUU[i][j]*betaD[j]

    # Step 4:   Compute alpha = sqrt(beta^2 - g_{00}):
    # Step 4.a: Compute beta^2 = beta^k beta_k:
    beta_squared = sp.sympify(0)
    for k in range(3):
        beta_squared += betaU[k]*betaD[k]

    # Step 4.b: alpha = sqrt(beta^2 - g_{00}):
    alpha = sp.sqrt(sp.simplify(beta_squared) - g4DD[0][0])

    # Step 5: If inputvars == "ADM", we are finished. Return.
    if inputvars == "ADM":
        return

    # Step 6: If inputvars == "BSSN", convert ADM to BSSN & return hDD, cf,
    import BSSN.BSSN_in_terms_of_ADM as BitoA
    dummyBU = ixp.zerorank1()
    BitoA.gammabarDD_hDD( gammaDD)
    BitoA.cf_from_gammaDD(gammaDD)
    BitoA.betU_vetU(      betaU,dummyBU)
    hDD  = BitoA.hDD
    cf   = BitoA.cf
    vetU = BitoA.vetU
```

## Step 3.a: ADM in terms of 4-metric validation: Confirm $\gamma_{ij}\gamma^{ij}=3$  \[Back to [top](#toc)\]
$$\label{adm_ito_fourmetric_validate}$$

Next we compute $\gamma^{ij} \gamma_{ij}$ as a validation check. It should equal 3:

```python
BSSN_or_ADM_ito_g4DD("ADM")
gammaUU, gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)

sum = sp.sympify(0)
for i in range(3):
    for j in range(3):
        sum += gammaDD[i][j]*gammaUU[i][j]
if sp.simplify(sum) == sp.sympify(3):
    print("TEST PASSED!")
else:
    print("TEST FAILED: "+str(sum)+" does not apparently equal 3.")
    sys.exit(1)
```

    TEST PASSED!

## Step 3.b: BSSN in terms of 4-metric validation: Confirm $\bar{\gamma}_{ij}\bar{\gamma}^{ij}=3$  \[Back to [top](#toc)\]
$$\label{bssn_ito_fourmetric_validate}$$

Next we compute $\bar{\gamma}_{ij}\bar{\gamma}^{ij}$ as a validation check. It should equal 3:

```python
import reference_metric as rfm
par.set_parval_from_str("reference_metric::CoordSystem","SinhCylindrical")
rfm.reference_metric()

BSSN_or_ADM_ito_g4DD("BSSN")
gammabarDD = ixp.zerorank2()
for i in range(3):
    for j in range(3):
        # gammabar_{ij}  = h_{ij}*ReDD[i][j] + gammahat_{ij}
        gammabarDD[i][j] = hDD[i][j] * rfm.ReDD[i][j] + rfm.ghatDD[i][j]

gammabarUU, gammabarDET = ixp.symm_matrix_inverter3x3(gammabarDD)

sum = sp.sympify(0)
for i in range(3):
    for j in range(3):
        sum += gammabarDD[i][j]*gammabarUU[i][j]
if sp.simplify(sum) == sp.sympify(3):
    print("TEST PASSED!")
else:
    print("TEST FAILED: "+str(sum)+" does not apparently equal 3.")
    sys.exit(1)
```

    TEST PASSED!

## Step 4: Code Validation against `BSSN.ADMBSSN_tofrom_4metric` NRPy+ module \[Back to [top](#toc)\]
$$\label{code_validation}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for BrillLindquist initial data between
1. this tutorial and
2. the NRPy+ [BSSN.ADMBSSN_tofrom_4metric](../edit/BSSN/ADMBSSN_tofrom_4metric.py) module.

By default, we analyze these expressions in SinhCylindrical coordinates, though other coordinate systems may be chosen.

```python
par.set_parval_from_str("reference_metric::CoordSystem","SinhCylindrical")
rfm.reference_metric()

import BSSN.ADMBSSN_tofrom_4metric as AB4m
for inputvars in ["BSSN","ADM"]:
    g4DD_ito_BSSN_or_ADM(inputvars)
    AB4m.g4DD_ito_BSSN_or_ADM(inputvars)
    for i in range(4):
        for j in range(4):
            print(inputvars+" input: g4DD["+str(i)+"]["+str(j)+"] - g4DD_mod["+str(i)+"]["
                  +str(j)+"] = "+str(g4DD[i][j]-AB4m.g4DD[i][j]))

    g4UU_ito_BSSN_or_ADM(inputvars)
    AB4m.g4UU_ito_BSSN_or_ADM(inputvars)
    for i in range(4):
        for j in range(4):
            print(inputvars+" input: g4UU["+str(i)+"]["+str(j)+"] - g4UU_mod["+str(i)+"]["
                  +str(j)+"] = "+str(g4UU[i][j]-AB4m.g4UU[i][j]))

BSSN_or_ADM_ito_g4DD("BSSN")
AB4m.BSSN_or_ADM_ito_g4DD("BSSN")
print("BSSN QUANTITIES (ito 4-metric g4DD)")
print("cf - mod_cf = " + str(cf - AB4m.cf))
print("alpha - mod_alpha = " + str(alpha - AB4m.alpha))
for i in range(3):
    print("vetU["+str(i)+"] - mod_vetU["+str(i)+"] = " + str(vetU[i] - AB4m.vetU[i]))
    for j in range(3):
        print("hDD["+str(i)+"]["+str(j)+"] - mod_hDD["+str(i)+"]["+str(j)+"] = "
              + str(hDD[i][j] - AB4m.hDD[i][j]))

BSSN_or_ADM_ito_g4DD("ADM")
AB4m.BSSN_or_ADM_ito_g4DD("ADM")
print("ADM QUANTITIES (ito 4-metric g4DD)")
print("alpha - mod_alpha = " + str(alpha - AB4m.alpha))
for i in range(3):
    print("betaU["+str(i)+"] - mod_betaU["+str(i)+"] = " + str(betaU[i] - AB4m.betaU[i]))
    for j in range(3):
        print("gammaDD["+str(i)+"]["+str(j)+"] - mod_gammaDD["+str(i)+"]["+str(j)+"] = "
              + str(gammaDD[i][j] - AB4m.gammaDD[i][j]))
```

    BSSN input: g4DD[0][0] - g4DD_mod[0][0] = 0
    BSSN input: g4DD[0][1] - g4DD_mod[0][1] = 0
    BSSN input: g4DD[0][2] - g4DD_mod[0][2] = 0
    BSSN input: g4DD[0][3] - g4DD_mod[0][3] = 0
    BSSN input: g4DD[1][0] - g4DD_mod[1][0] = 0
    BSSN input: g4DD[1][1] - g4DD_mod[1][1] = 0
    BSSN input: g4DD[1][2] - g4DD_mod[1][2] = 0
    BSSN input: g4DD[1][3] - g4DD_mod[1][3] = 0
    BSSN input: g4DD[2][0] - g4DD_mod[2][0] = 0
    BSSN input: g4DD[2][1] - g4DD_mod[2][1] = 0
    BSSN input: g4DD[2][2] - g4DD_mod[2][2] = 0
    BSSN input: g4DD[2][3] - g4DD_mod[2][3] = 0
    BSSN input: g4DD[3][0] - g4DD_mod[3][0] = 0
    BSSN input: g4DD[3][1] - g4DD_mod[3][1] = 0
    BSSN input: g4DD[3][2] - g4DD_mod[3][2] = 0
    BSSN input: g4DD[3][3] - g4DD_mod[3][3] = 0
    BSSN input: g4UU[0][0] - g4UU_mod[0][0] = 0
    BSSN input: g4UU[0][1] - g4UU_mod[0][1] = 0
    BSSN input: g4UU[0][2] - g4UU_mod[0][2] = 0
    BSSN input: g4UU[0][3] - g4UU_mod[0][3] = 0
    BSSN input: g4UU[1][0] - g4UU_mod[1][0] = 0
    BSSN input: g4UU[1][1] - g4UU_mod[1][1] = 0
    BSSN input: g4UU[1][2] - g4UU_mod[1][2] = 0
    BSSN input: g4UU[1][3] - g4UU_mod[1][3] = 0
    BSSN input: g4UU[2][0] - g4UU_mod[2][0] = 0
    BSSN input: g4UU[2][1] - g4UU_mod[2][1] = 0
    BSSN input: g4UU[2][2] - g4UU_mod[2][2] = 0
    BSSN input: g4UU[2][3] - g4UU_mod[2][3] = 0
    BSSN input: g4UU[3][0] - g4UU_mod[3][0] = 0
    BSSN input: g4UU[3][1] - g4UU_mod[3][1] = 0
    BSSN input: g4UU[3][2] - g4UU_mod[3][2] = 0
    BSSN input: g4UU[3][3] - g4UU_mod[3][3] = 0
    ADM input: g4DD[0][0] - g4DD_mod[0][0] = 0
    ADM input: g4DD[0][1] - g4DD_mod[0][1] = 0
    ADM input: g4DD[0][2] - g4DD_mod[0][2] = 0
    ADM input: g4DD[0][3] - g4DD_mod[0][3] = 0
    ADM input: g4DD[1][0] - g4DD_mod[1][0] = 0
    ADM input: g4DD[1][1] - g4DD_mod[1][1] = 0
    ADM input: g4DD[1][2] - g4DD_mod[1][2] = 0
    ADM input: g4DD[1][3] - g4DD_mod[1][3] = 0
    ADM input: g4DD[2][0] - g4DD_mod[2][0] = 0
    ADM input: g4DD[2][1] - g4DD_mod[2][1] = 0
    ADM input: g4DD[2][2] - g4DD_mod[2][2] = 0
    ADM input: g4DD[2][3] - g4DD_mod[2][3] = 0
    ADM input: g4DD[3][0] - g4DD_mod[3][0] = 0
    ADM input: g4DD[3][1] - g4DD_mod[3][1] = 0
    ADM input: g4DD[3][2] - g4DD_mod[3][2] = 0
    ADM input: g4DD[3][3] - g4DD_mod[3][3] = 0
    ADM input: g4UU[0][0] - g4UU_mod[0][0] = 0
    ADM input: g4UU[0][1] - g4UU_mod[0][1] = 0
    ADM input: g4UU[0][2] - g4UU_mod[0][2] = 0
    ADM input: g4UU[0][3] - g4UU_mod[0][3] = 0
    ADM input: g4UU[1][0] - g4UU_mod[1][0] = 0
    ADM input: g4UU[1][1] - g4UU_mod[1][1] = 0
    ADM input: g4UU[1][2] - g4UU_mod[1][2] = 0
    ADM input: g4UU[1][3] - g4UU_mod[1][3] = 0
    ADM input: g4UU[2][0] - g4UU_mod[2][0] = 0
    ADM input: g4UU[2][1] - g4UU_mod[2][1] = 0
    ADM input: g4UU[2][2] - g4UU_mod[2][2] = 0
    ADM input: g4UU[2][3] - g4UU_mod[2][3] = 0
    ADM input: g4UU[3][0] - g4UU_mod[3][0] = 0
    ADM input: g4UU[3][1] - g4UU_mod[3][1] = 0
    ADM input: g4UU[3][2] - g4UU_mod[3][2] = 0
    ADM input: g4UU[3][3] - g4UU_mod[3][3] = 0
    BSSN QUANTITIES (ito 4-metric g4DD)
    cf - mod_cf = 0
    alpha - mod_alpha = 0
    vetU[0] - mod_vetU[0] = 0
    hDD[0][0] - mod_hDD[0][0] = 0
    hDD[0][1] - mod_hDD[0][1] = 0
    hDD[0][2] - mod_hDD[0][2] = 0
    vetU[1] - mod_vetU[1] = 0
    hDD[1][0] - mod_hDD[1][0] = 0
    hDD[1][1] - mod_hDD[1][1] = 0
    hDD[1][2] - mod_hDD[1][2] = 0
    vetU[2] - mod_vetU[2] = 0
    hDD[2][0] - mod_hDD[2][0] = 0
    hDD[2][1] - mod_hDD[2][1] = 0
    hDD[2][2] - mod_hDD[2][2] = 0
    ADM QUANTITIES (ito 4-metric g4DD)
    alpha - mod_alpha = 0
    betaU[0] - mod_betaU[0] = 0
    gammaDD[0][0] - mod_gammaDD[0][0] = 0
    gammaDD[0][1] - mod_gammaDD[0][1] = 0
    gammaDD[0][2] - mod_gammaDD[0][2] = 0
    betaU[1] - mod_betaU[1] = 0
    gammaDD[1][0] - mod_gammaDD[1][0] = 0
    gammaDD[1][1] - mod_gammaDD[1][1] = 0
    gammaDD[1][2] - mod_gammaDD[1][2] = 0
    betaU[2] - mod_betaU[2] = 0
    gammaDD[2][0] - mod_gammaDD[2][0] = 0
    gammaDD[2][1] - mod_gammaDD[2][1] = 0
    gammaDD[2][2] - mod_gammaDD[2][2] = 0

# Step 4: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename [Tutorial-ADMBSSN_tofrom_4metric.pdf](Tutorial-ADMBSSN_tofrom_4metric.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-ADMBSSN_tofrom_4metric")
```

    Created Tutorial-ADMBSSN_tofrom_4metric.tex, and compiled LaTeX file to PDF
        file Tutorial-ADMBSSN_tofrom_4metric.pdf


# Code

## Standard imports

```python
# Data manipulation
import pandas as pd
import numpy as np

# Options for pandas
pd.options.display.max_columns = 50
pd.options.display.max_rows = 30

from IPython import get_ipython
ipython = get_ipython()

# autoreload extension
if 'autoreload' not in ipython.extension_manager.loaded:
    %load_ext autoreload

%autoreload 2

import matplotlib.pyplot as plt
from matplotlib import gridspec
%matplotlib inline

import time
np.random.seed(int(time.time()))
```

## Specific imports

```python
# import code of Zdenko Heyvaert

sys.path.insert(1, '../Zdenko/Clusters5.11') # now we can import files from this directory too
import Zdenko_timeseries_clusters as Zdenko

import sys
sys.path.insert(1, '../sglv_timeseries')

from sglv_timeseries import ibm
from sglv_timeseries import glv
from sglv_timeseries.noise_parameters import NOISE
from sglv_timeseries.models import MODEL
from sglv_timeseries.timeseries_plotting import PlotTimeseries, PlotRankAbundance, PlotTimeseriesComparison

sys.path.insert(1, '../SOI') # now we can import files from this directory too
import statevectors_Gillespie as Franziska

from matplotlib import colorbar as mcolorbar
from noise_properties_plotting import PiecewiseLogNorm, PiecewiseNormalize

from itertools import chain, count, combinations

from neutrality_analysis import JensenShannon

from matplotlib.colors import LogNorm

from heavytails import fit_heavytail, plot_heavytail, normalizations, plot_cdf_heavytail

from elife_settings import ELIFE, set_elife_settings

set_elife_settings()
```

    load sglv_timeseries

## Random paramaters and example plot functions

```python
# specify all parameters

from scipy import stats

def random_parameter_set(S, connectance=0.3, minint=-0.5, maxint=0.5,
                         minmigration=0.4, maxmigration=0.4,
                         minextinction=0.5, maxextinction=0.5, growth_rate=1.5):
    interaction = np.random.uniform(minint, maxint, [S, S])
    interaction *= np.random.choice([0, 1], interaction.shape, p=[1 - connectance, connectance])  # connectance
    np.fill_diagonal(interaction, -1.)  # self-interaction

    growth_rate = np.full([S, 1], growth_rate)
    immigration = np.random.uniform(minmigration, maxmigration, [S, 1])
    extinction = np.random.uniform(minextinction, maxextinction, [S, 1])

    params = {'interaction_matrix': interaction, 'immigration_rate': immigration,
              'extinction_rate': extinction, 'growth_rate': growth_rate}

    return params

def random_parameter_set_ibm(S, connectance=0.3, minint=-0.5, maxint=0.5,
                         minmigration=0.4, maxmigration=0.4,
                         minextinction=0.5, maxextinction=0.5, growth_rate=1.5):

    interaction = np.random.uniform(minint, maxint, [S, S])
    interaction *= np.random.choice([0, 1], interaction.shape, p=[1 - connectance, connectance])  # connectance
    np.fill_diagonal(interaction, -1.)  # self-interaction

    growth_rate = np.full([S, 1], growth_rate)
    immigration = np.random.uniform(minmigration, maxmigration, [S, 1])
    extinction = np.random.uniform(minextinction, maxextinction, [S, 1])

    SIS = []
    SISfactor = 200

    SISvector = np.ones(S, dtype=int)
    SISvector[SIS] *= SISfactor

    params = {'interaction_matrix' : interaction, 'immigration_rate' : immigration,
             'extinction_rate' : extinction,
               'growth_rate' : growth_rate, 'SIS':SIS, 'SISvector':SISvector}

    return params

def random_parameter_set_logistic(S, width_growth=1):
    growth_rate = stats.lognorm.rvs(loc=0, s=width_growth, size=[S, 1])
    if np.sqrt(4 - width_growth ** 2) == 0:
        self_int = np.ones(S)
    else:
        self_int = stats.lognorm.rvs(loc=0, s=np.sqrt(4 - width_growth ** 2), size=S)

    interaction = np.zeros([S, S])
    np.fill_diagonal(interaction, -self_int)  # self-interaction

    growth_rate = np.full([S, 1], growth_rate)
    immigration = np.zeros([S, 1])
    extinction = np.zeros([S, 1])

    params = {'interaction_matrix': interaction, 'immigration_rate': immigration,
              'extinction_rate': extinction, 'growth_rate': growth_rate}

    return params

def add_SIS(interaction, SISvector):
    interaction_SIS = interaction * SISvector
    np.fill_diagonal(interaction_SIS, np.diag(interaction))
    return interaction_SIS

def parameters_to_Zdenko_parameters(interaction, growth_rate, immigration, extinction, SISvector):
    omega_Z = np.zeros((len(interaction)+1, len(interaction)+1));
    omega_Z[1:,1:] = interaction; omega_Z[1:,0] = growth_rate.flatten()
    mu_Z = np.hstack(([0], immigration))
    e_Z = np.hstack(([0], extinction))
    SISvector_Z = np.hstack(([1], SISvector))

    return omega_Z, mu_Z, e_Z, SISvector_Z
```

```python
from variation import variation_coefficient, JS

def example_timeseries(connectance, immigration, int_strength, noise=None,
                       max_cap=None, sites=None, S=200,
                       noise_implementation=NOISE.LANGEVIN_LINEAR):

    if sites == None:
        params = random_parameter_set(S,
           minmigration=immigration, maxmigration=immigration, connectance=connectance,
           minint=-int_strength, maxint=int_strength)

        if np.isinf(max_cap):
            model = MODEL.GLV
            params['initial_condition'] = np.random.uniform(0, 1, [S,1])
        else:
            model = MODEL.MAX
            params['maximum_capacity'] = max_cap
            initcond = np.random.uniform(0, 1, [S,1])
            params['initial_condition'] = initcond*max_cap/S
        if noise_implementation == NOISE.LANGEVIN_LINEAR:
            params['noise_linear'] = noise
        elif noise_implementation == NOISE.LANGEVIN_SQRT:
            params['noise_sqrt'] = noise
        elif noise_implementation == NOISE.LANGEVIN_CONSTANT:
            params['noise_constant'] = noise

    elif sites != None and max_cap == None:
        params = random_parameter_set_ibm(S,
           minmigration=immigration, maxmigration=immigration,
                                          connectance=connectance,
           minint=-int_strength, maxint=int_strength)

        model = MODEL.IBM
        params['initial_condition'] = np.random.randint(0, int(0.66*sites/S), S)
        params['sites'] = sites
    return show_example(params, model, noise_implementation)

def example_timeseries_logistic(width_growth, noise, S=200):
    params = random_parameter_set_logistic(S, width_growth)

    #interaction_SIS = add_SIS(interaction, SISvector)
    initcond = np.random.uniform(0, 1, [S,1])

    params = {'interaction_matrix' : interaction, 'immigration_rate' : immigration,
            'extinction_rate' : extinction, 'initial_condition' : initcond,
            'noise_linear' : noise, 'growth_rate' : growth_rate}

    return show_example(params, MODEL.GLV)

def show_example(params, model, noise_implementation):
    # find steady state without noise
    if model in [MODEL.GLV, MODEL.MAX]:
        params_init = params.copy()
        params_init['noise_linear'] = 0
        ts = glv.Timeseries(params_init, T = 125, dt=0.005, tskip=199, model=model)
        discrete = False
    elif model == MODEL.IBM:
        ts = ibm.Timeseries(params, T = 150)
        discrete = True

    stab = (np.max(np.abs((ts.timeseries.iloc[-50, 1:] - ts.timeseries.iloc[-1, 1:])/ ts.timeseries.iloc[-50, 1:])) < 0.1)
    print("Deterministic solution is stable:", stab)
    #if stab == False:
    #    print(ts.timeseries.iloc[0,:])
    #    print(ts.timeseries.iloc[1,:])
    ep = ts.endpoint
    col_to_drop = ep.index[ep.endpoint < 1e-6*np.max(ep.endpoint)]

    PlotTimeseries(ts.timeseries.drop(columns=col_to_drop))

    if model in [MODEL.GLV, MODEL.MAX]:
        params['initial_condition'] = ts.endpoint.values.astype('float')
        ts = glv.Timeseries(params, T = 250, dt=0.005, tskip=199, model=model, noise_implementation=noise_implementation)
    elif model == MODEL.IBM:
        params['initial_condition'] = ts.endpoint.values.astype('int').flatten()
        ts = ibm.Timeseries(params, T = 250)
    PlotTimeseries(ts.timeseries.drop(columns=col_to_drop))
    params = variation_coefficient(ts.timeseries)
    print("variation:", params[0])
    params = JS(ts.timeseries, verbose=True)
    print("JS:", params[0])
    print("pval stable (higher more stable):", params[-1])

    ep = ts.endpoint.values.astype('float').flatten()
    ep = ep[ep > 1e-6*np.max(ep)]

    print("remaining species", len(ep))

    fig = plt.figure()
    ax = fig.add_subplot(111)

    print("things", ep[0], ep[-1], len(ep), discrete)
    params = fit_heavytail(ep, func='lognorm', discrete=discrete)
    plot_heavytail(ep, params, func='lognorm', ax=ax, discrete=discrete)
    print("Width lognorm:", params[0], params[1], params[2])
    print("Stat lognorm:", params[-2])
    for f in ['expon', 'norm', 'powerlaw', 'pareto']:
        params = fit_heavytail(ep, func=f, discrete=discrete)
        plot_heavytail(ep, params, func=f, ax=ax, discrete=discrete)
        print("Stat %s:" % f, params[-2])
    params = fit_heavytail(ep, func='trunc_powerlaw', discrete=discrete)
    plot_heavytail(ep, params, func='trunc_powerlaw', ax=ax, discrete=discrete)
    print("Stat trunc powerlaw:", params[-2])
    print("R powerlaw (negative -> lognormal):", params[2])

    return ep
```

# Introduction

Gibson 2016 proved that SIS can give rise to clustering of the data.
Zdenko also obtained clusters with an IBM approach.

Whether Zdenko's results were obvious depends on how the steady states of both algorithms are connected.

# IBM

## Stability of Zdenko algorithm

Is the steady state of the Zdenko algorithm robust against changes in initial conditions? Yes

```python
S = 20        # species in metacommunity
s = None      # species in local community
sites = 10000
SIS = []
SISfactor = 200

SISvector = np.ones(S, dtype=int)
SISvector[SIS] *= SISfactor

p = random_parameter_set(S)
interaction, growth_rate, immigration, extinction = p['interaction'], p['growth_rate'], p['immigration'], p['extinction']
interaction_SIS = add_SIS(interaction, SISvector)
omega_Z, mu_Z, e_Z, SISvector_Z = parameters_to_Zdenko_parameters(interaction, growth_rate, immigration, extinction, SISvector)
omega_SIS_Z, mu_Z, e_Z, SISvector_Z = parameters_to_Zdenko_parameters(interaction_SIS, growth_rate, immigration, extinction, SISvector)

# plot omega and omega with SIS
ax1 = plt.subplot(121)
vmin = - np.nanmax(abs(omega_Z)); vmax = np.nanmax(abs(omega_Z))
ax1.matshow(omega_Z, vmin=vmin, vmax=vmax, cmap='coolwarm')
#ax1.colorbar()

ax2 = plt.subplot(122)
vmin = - np.nanmax(abs(omega_SIS_Z)); vmax = np.nanmax(abs(omega_SIS_Z))
ax2.matshow(omega_SIS_Z, vmin=vmin, vmax=vmax, cmap='coolwarm')
#ax2.colorbar()
plt.show()

# generate multiple timeseries and remove the empty sites

N = 3

Z_ts = [Zdenko.timeseries(omega_Z, mu_Z, e_Z, SIS, SISvector_Z, s, sites).drop('empty', axis='columns')
        for i in range(N)]

# compare them

PlotTimeseriesComparison(Z_ts, composition=['ts'])
plt.show()
```

```python
# plot steady states (end point of time series)

ss = [ ts[[col for col in ts.columns if col.startswith('species')]].values[-1] for ts in Z_ts]

for i, ssi in zip(count(), ss):
    plt.plot(range(1, len(ssi)+1), ssi, label=str(i), alpha=0.5, marker='o')

plt.errorbar(range(1, len(ssi)+1), Z_ts[0][[col for col in Z_ts[0].columns if col.startswith('species')]].mean(axis=0),
            yerr=Z_ts[0][[col for col in Z_ts[0].columns if col.startswith('species')]].std(axis=0), color='grey')
#plt.yscale('log')
#print(min(np.nanmin(ss1),np.nanmin(ss2)), max(np.nanmax(ss1),np.nanmax(ss2)))
#plt.ylim(min(np.nanmin(ss1),np.nanmin(ss2)), max(np.nanmax(ss1),np.nanmax(ss2)))
plt.ylim([0, 1.1*max(chain.from_iterable(ss))]) # chain.from_iterable to flatten the ss
plt.legend()

plt.show()
```

## Consistent IBM

I wrote a version of Zdenko's code that is in the line of the code I have for PLM.
Is it the same? Yes

```python
initcond = np.random.randint(0, int(0.66*sites/S), S)

params = {'interaction_matrix' : interaction, 'immigration_rate' : immigration, 'extinction_rate' : extinction,
          'initial_condition' : initcond, 'SISvector' : SISvector,
          'growth_rate' : growth_rate, 'sites' : sites}

ibm_ts = [ibm.Timeseries(params, T = 50).timeseries for i in range(2)]
```

```python
PlotTimeseriesComparison([Z_ts[0], Z_ts[2], ibm_ts[0], ibm_ts[1]],
                         composition=['ts'], titles=['Zdenko 1', 'Zdenko 2', 'IBM 1', 'IBM 2'])

# plot steady states (end point of time series)

plt.figure()

if False: # plot endpoints
    ss_Z = [ ts[[col for col in ts.columns if col.startswith('species')]].values[-1] for ts in Z_ts]
    labels_Z = ['Zdenko %d' % i for i in range(1, len(ss_Z)+1)]
    ss_ibm = [ts[[col for col in ts.columns if col.startswith('species')]].values[-1] for ts in ibm_ts]
    labels_ibm = ['ibm %d' % i for i in range(1, len(ss_ibm)+1)]

    ss = ss_Z + ss_ibm
    labels = labels_Z + labels_ibm

    for ss, label in zip(ss, labels):
        plt.plot(range(1, len(ss)+1), ss, label=label, alpha=0.5, marker='o')
else: # plot mean and standard deviation
    for ts, label in zip([Z_ts[0],ibm_ts[0]], ['Zdenko', 'ibm']):
        plt.errorbar(range(1, len(ssi)+1), ts[[col for col in ts.columns if col.startswith('species')]].mean(axis=0),
            yerr=ts[[col for col in ts.columns if col.startswith('species')]].std(axis=0), label=label, alpha=0.5)

#plt.yscale('log')
#print(min(np.nanmin(ss1),np.nanmin(ss2)), max(np.nanmax(ss1),np.nanmax(ss2)))
#plt.ylim(min(np.nanmin(ss1),np.nanmin(ss2)), max(np.nanmax(ss1),np.nanmax(ss2)))
plt.legend()

plt.show()
```

Not slower? No

```python
%timeit Zdenko.timeseries(omega, mu, e, SIS, SISfactor, s, sites)
%timeit ibm.Timeseries(params, T = 50)
```

    10.6 s ± 621 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    10.3 s ± 975 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

## Smaller lattice results in more noise

```python
S = 20        # species in metacommunity
s = None      # species in local community
sites = 10000
SIS = []
SISfactor = 200

SISvector = np.ones(S, dtype=int)
SISvector[SIS] *= SISfactor

params_ibm = random_parameter_set(S)

for sites in [100, 1000, 10000]:
    initcond = np.random.randint(0, int(0.66*sites/S), S)

    params_ibm['initial_condition'] = initcond
    params_ibm['sites'] = sites

    ts = ibm.Timeseries(params_ibm, T = 50).timeseries
    y = ts[[col for col in ts.columns if col.startswith('species')]].mean(axis=0)
    yerr = ts[[col for col in ts.columns if col.startswith('species')]].std(axis=0)

    # rescale
    y, yerr = y/np.mean(y), yerr/np.mean(y)

    plt.errorbar(range(1, len(y)+1), y, yerr=yerr, label=sites, alpha=0.5)

plt.legend()
plt.show()
```

## Immigration and connectance define the width of the lognormal distribution

```python
S = 200        # species in metacommunity
s = None      # species in local community
SIS = []
SISfactor = 200
sites = 50000

SISvector = np.ones(S, dtype=int)
SISvector[SIS] *= SISfactor

migrations = [0.0001] #[0.005] #[0.5, 0.05, 0.005]
connectances = [0.3] #[0, 0.01, 0.1]

fig_ra = plt.figure(figsize=(12,12))
gs_ra = gridspec.GridSpec(3,3)

# for migration, connectance in zip(migrations, connectances):
for i, migration in enumerate(migrations):
    for j, connectance in enumerate(connectances):
        ax = fig_ra.add_subplot(gs_ra[i,j])

        if i == len(migrations) - 1:
            ax.set_xlabel('C = %.3f' % connectance)
        if j == 0:
            ax.set_ylabel(r'$\mu$ = %.3f' % migration)

        N = 1
        ep = np.zeros(N*S)

        for k in range(N):
            params_ibm = random_parameter_set(S,
             minmigration=migration, maxmigration=migration, connectance=connectance)
            initcond = np.random.randint(0, int(0.66*sites/S), S)

            params_ibm['initial_condition'] = initcond
            params_ibm['sites'] = sites

            ts = ibm.Timeseries(params_ibm, T = 150)

            PlotTimeseries(ts.timeseries)

            ep[k*200:(k+1)*200] = ts.endpoint.values.astype('float').flatten()

        for func, ls in zip(['lognorm', 'norm', 'powerlaw'], ['-', '-.', ':']):
            params = fit_heavytail(ep, func=func)
            plot_heavytail(ep, params, func=func, ax=ax, linestyle=ls,
                       xscale='log', yscale='log')
#ax_hist.legend()
plt.show()
```

## Steady state gLV with maximum capacity

Steady state of gLV is not the same as steady state of IBM
The steady state of IBM is closer to equal distribution of species.

What are the differences in the implementation? The growth rate is dependent on the emptyness of the lattice for IBM.

In an IBM implementation the differences between the species are less big, the steady states are closer to an equal partitioning of the species.

\begin{align}
d x_i(t) &= \left(\lambda_i + g_i x_i(t) + \sum_j \omega_{ij} x_i(t) x_j(t) - \frac{\sum_k x_k(t)}{\Nmax} \left( g_i^+ x_i(t) + \sum_j \omega_{ij}^+ x_i x_j\right)\right) dt \\
&= \left( \lambda_i + g_i x_i(t) + \sum_j \left( \omega_{ij} - \frac{g_i^+}{\Nmax} \right) x_i(t) x_j(t) - \sum_j \sum_k \frac{\omega_{ij}^+}{\Nmax}  x_i(t) x_j(t) x_k(t) \right) dt
\end{align}

Adding a maximum capacity, makes the steady state closer to the IBM results.

<font color='red'> TODO Derive this result analytically </font>

```python
def test():
    N = 10

    A = np.random.normal(0, 0.1, [N,N])
    np.fill_diagonal(A, -1)
    g = np.random.uniform(0, 5, [N,1])

    offdiag = np.full([N,N], -0.1); np.fill_diagonal(offdiag, 0)
    omega = A + offdiag

    v = -np.linalg.inv(A).dot(g)
    v2 = -np.linalg.inv(omega).dot(g)

    print(v - v2)
    plt.hist(v, alpha=0.2, label='A')
    plt.hist(v2, alpha=0.2, label='max cap')
    plt.legend()
    plt.show()
```

```python
# maximal capacity closer to all same value?

# specify all parameters

S = 20        # species in metacommunity
s = None      # species in local community
sites = 10000
SIS = []
SISfactor = 200

SISvector = np.ones(S, dtype=int)
SISvector[SIS] *= SISfactor

interaction, growth_rate, immigration, extinction
params_ibm = random_parameter_set(S, minmigration=0, maxmigration=0)
params_ibm['interaction_matrix'] = add_SIS(params['interaction_matrix'], SISvector)
params_ibm['initial_condition'] = np.random.randint(0, int(0.66*sites/S), S),
params_ibm['sites'] = sites

ibm_ep = ibm.Timeseries(params_ibm, T = 50).endpoint.to_numpy().flatten()

params_glv = {'interaction_matrix' : interaction_SIS, 'growth_rate' : growth_rate, 'immigration_rate' : immigration,
          'extinction_rate' : extinction, 'noise_linear' : 0, 'initial_condition' : np.random.uniform(0,1,[S,1])}

fig = plt.figure()
gs = gridspec.GridSpec(1, 2, width_ratios=[6,1])
ax = fig.add_subplot(gs[0])

ts = glv.Timeseries(params_glv, dt=1e-2, tskip=9, T=100, seed=int(time.time()))
glv_ts = ts.timeseries
glv_ep = ts.endpoint.to_numpy().flatten()
order = np.argsort(glv_ep)[::-1]
ax.plot(range(1, S+1), glv_ep[order] / np.sum(glv_ep), label='glv', color='green')
ax.plot(range(1, S+1), ibm_ep[order] / np.sum(ibm_ep), label='ibm', color='orange')

reducers = [1000, 100, 10, 1, 0.1]
glv_mc_ts = [None] * len(reducers)
glv_mc_ep = [None] * len(reducers)

cmap = plt.cm.get_cmap('seismic')
norm = LogNorm(min(reducers), max(reducers))

for i, reducer in enumerate(reducers):
    params_glv['maximum_capacity'] = reducer
    params_glv['initial_condition'] = 0.005 * reducer * glv_ep.reshape([S,1]) #* np.random.uniform(0.5,1,[S,1])

    ts = glv.Timeseries(params_glv, dt=5e-2, tskip=19, T=500, seed=int(time.time()),
                                  model=MODEL.MAX)
    glv_mc_ts[i] = ts.timeseries
    ep = ts.endpoint.to_numpy().flatten()
    glv_mc_ep[i] = ep

    ax.plot(range(1, S+1), ep[order] / np.sum(ep), label='mc = %.2f' % reducer, color=cmap(norm(reducer)))

ax_cbar = fig.add_subplot(gs[1])
mcolorbar.ColorbarBase(ax_cbar, cmap=plt.cm.coolwarm, orientation='vertical', norm=norm)

ax.axhline(y=1/len(ep), c='k', alpha=0.2)
ax.legend()

average_ep = np.mean(np.array(glv_mc_ep))

PlotTimeseriesComparison(glv_mc_ts, composition=['ts'])
plt.show()

differences = [np.mean(ss - glv_ep) for ss in glv_mc_ep]
square_differences = [np.mean((ss - glv_ep)**2) for ss in glv_mc_ep]

plt.scatter(reducers, differences, label="diff")
plt.scatter(reducers, square_differences, label="diff squared")
plt.legend()
plt.xscale('log')
plt.show()

JS_ibm = [JensenShannon(ts.iloc[-1]['species_1':]/np.sum(ts.iloc[-1]['species_1':]),
                        ibm_ts.iloc[-1]['species_1':]/np.sum(ibm_ts.iloc[-1]['species_1':])) for ts in glv_mc_ts]
JS_glv = [JensenShannon(ts.iloc[-1]['species_1':]/np.sum(ts.iloc[-1]['species_1':]),
                        glv_ts.iloc[-1]['species_1':]/np.sum(glv_ts.iloc[-1]['species_1':])) for ts in glv_mc_ts]

plt.plot(reducers, JS_ibm, label='wrt IBM')
plt.plot(reducers, JS_glv, label='wrt gLV')
plt.xscale('log')
plt.xlabel('Maximal capacity')
plt.ylabel('Jensen Shannon distance')
plt.legend()
plt.show()
```

```python
new = True

def random_ts():
    S = 20                              # number of species in the metacommunity
    s = None                           # number of species in each local community
    sites = 10000
    connectance = 0.3

    SIS = [1]
    SISfactor_ = 1  # 200
    maxint = 0.5
    minmigration = 0.4
    maxmigration = 0.4
    minextinction = 0.3
    maxextinction = 0.3

    SISfactor = np.ones(S+1, dtype=int)
    SISfactor[SIS] *= SISfactor_

    omega = np.random.uniform(-maxint, maxint, [S+1, S+1])
    omega *= np.random.choice([0, 1], omega.shape,
                              p=[1-connectance, connectance])  # connectance
    omega[:, 0] = 0.3  # growth rates

    np.fill_diagonal(omega, -1.)  # self-interaction
    omega[0, :] = 0  # no growth of empty sites

    omega_SIS = omega * SISfactor
    omega_SIS[SIS, SIS] = omega[SIS, SIS] * np.sqrt(SISfactor[SIS])

    # immigration rates
    mu = np.random.uniform(minmigration, maxmigration, [S+1])
    mu[0] = 0

    # extinction rates
    e = np.random.uniform(minextinction, maxextinction, [S+1])
    e[0] = 0

    initcond = np.full([S+1], int(0.66*sites/S))
    initcond[0] = sites - sum(initcond[1:])

    ibm_ts = [ibm.run_timeseries(
        omega, mu, e, initcond, SISfactor).iloc[-1]['species_1':].astype(float).to_numpy().flatten()
              for i in range(3)]

    params = {}
    params['interaction_matrix'] = omega_SIS[1:, 1:]
    params['growth_rate'] = omega[1:, 0].reshape([S, 1])
    params['immigration_rate'] = mu[1:].reshape([S, 1])
    params['noise_linear'] = 0

    params['initial_condition'] = np.random.uniform(0, 1, [S, 1])

    glv_ts = glv.Timeseries(params, dt=1e-2, tskip=9, T=100,
                            seed=int(time.time())).endpoint.astype(float).to_numpy().flatten()

    mc_reducers = [10, 1, 0.5, 0.1, 0.05, 0.01]
    noises = [0, 0.01, 0.05]

    glv_mc_ts = [None] * len(noises) * len(mc_reducers)

    for j, noise in enumerate(noises):
        for i, reducer in enumerate(mc_reducers):
            params['maximum_capacity'] = reducer * np.sum(glv_ts)
            params['initial_condition'] = reducer * \
                np.random.uniform(0, 1, [S, 1])
            params['noise_linear'] = noise
            glv_mc_ts[j*len(mc_reducers) + i] = glv.Timeseries(params, dt=1e-2,
              tskip=19, T=100, seed=int(time.time()), model=MODEL.MAX).endpoint.astype(float).to_numpy().flatten()

    JS_ibm = [np.mean([JensenShannon(ibm_i, ibm_j) for ibm_i, ibm_j in
                       combinations(ibm_ts, r=2)])] + \
        [np.mean([JensenShannon(glv_ts, ibm_i) for ibm_i in ibm_ts])] + \
        [np.mean([JensenShannon(glv_mc_ts_i, ibm_i) for ibm_i in ibm_ts])
         for glv_mc_ts_i in glv_mc_ts]

    JS_glv = [np.mean([JensenShannon(ibm_i, glv_ts) for ibm_i in ibm_ts])] + \
        [JensenShannon(glv_ts, glv_ts)] + \
        [JensenShannon(glv_mc_ts_i, glv_ts) for glv_mc_ts_i in glv_mc_ts]

    return JS_ibm, JS_glv

if new:
    Nibm = 3
    mc_reducers = [10, 1, 0.5, 0.1, 0.05, 0.01]

    tuples_top = ['ibm'] + ['glv'] + ['glv_mc'] * len(mc_reducers) + \
        ['glv_mc_noise001'] * len(mc_reducers) + \
        ['glv_mc_noise005'] * len(mc_reducers)
    tuples_bottom = [1] + [1] + mc_reducers * 3

    tuples = list(zip(*[tuples_top, tuples_bottom]))

    multi_col = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])

    index = range(10)

    df_ibm = pd.DataFrame(index=index, columns=multi_col)
    df_glv = pd.DataFrame(index=index, columns=multi_col)

    for i in index:
        JS_ibm, JS_glv = random_ts()
        df_ibm.loc[i] = JS_ibm
        df_glv.loc[i] = JS_glv

    df_ibm.to_csv('comparison_ibm_glv/JS_to_ibm2.csv')
    df_glv.to_csv('comparison_ibm_glv/JS_to_glv2.csv')
```

For large maximal capacity, the steady state are close to the gLV solutions and further from the IBM solution (expected). For decreasing maximal capacity (stronger saturation constraints) the solution becomes closer to the IBM solution and further from the gLV (this is what we wanted).
The dotted line is the distance between several replicates of IBM simulations, this is the distance we want to obtain with the blue curve. The dash-dotted line is the distance gLV is from IBM. We don't want to perform worse than gLV, so we want to stay under this line.

<font color="red"> TODO Check typical JS distances between experimental data of same / different clusters. <br>
    Do this analysis for larger number of species </font>

```python
df_ibm_mean = df_ibm.mean(axis='rows')
df_glv_mean = df_glv.mean(axis='rows')
df_ibm_std = df_ibm.std(axis='rows')
df_glv_std = df_glv.std(axis='rows')

fig = plt.figure(figsize=(8,3))
gs = gridspec.GridSpec(1, 2, width_ratios=[2,1])
ax = fig.add_subplot(gs[0])
ax_l = fig.add_subplot(gs[1], frameon=False)
ax_l.axis('off')

ax.axhline(df_ibm_mean['ibm'].to_numpy()[0], label='ibm', color='grey', linestyle=':')
ax.axhline(df_ibm_mean['glv'].to_numpy()[0], label='glv', color='grey', linestyle='-.')

x = range(len(df_ibm_mean['glv_mc'].index))

ax.errorbar(x, df_ibm_mean['glv_mc'].to_numpy(), df_ibm_std['glv_mc'].to_numpy(),
            label='wrt ibm')
ax.errorbar(x, df_glv_mean['glv_mc'].to_numpy(), df_glv_std['glv_mc'].to_numpy(),
            label='wrt glv')
ax.errorbar(x, df_glv_mean['glv_mc_noise001'].to_numpy(), df_glv_std['glv_mc_noise001'].to_numpy(),
           label='wrt glv noise 0.01')
ax.errorbar(x, df_glv_mean['glv_mc_noise005'].to_numpy(), df_glv_std['glv_mc_noise005'].to_numpy(),
            label='wrt glv noise 0.05')

ax.set_xticks(x)
ax.set_xticklabels(df_ibm_mean['glv_mc'].index, rotation=90)
ax.set_ylabel('Jensen Shannon distance')
ax.set_xlabel('Maximal capacity')

h, l = ax.get_legend_handles_labels()
ax_l.legend(h, l)
plt.show()
```

# Are IBM and gLV the same in the absence of immigration? No

```python
# specify all parameters

S = 20                              # number of species in the metacommunity
s = None                           # number of species in each local community
sites = 10000
connectance = 0.3

SIS = [1]
SISfactor_ = 200
maxint = 0.5
minmigration = 0.5; maxmigration = 0.5
minextinction = 0.3; maxextinction = 0.3

SISfactor = np.ones(S+1, dtype=int)
SISfactor[SIS] *= SISfactor_

omega = np.random.uniform(-maxint,maxint, [S+1, S+1])
omega *= np.random.choice([0, 1], omega.shape, p=[1-connectance, connectance]) # connectance
omega[:,0] = 0.3 # growth rates

np.fill_diagonal(omega, -1.) # self-interaction
omega[0,:] = 0 # no growth of empty sites

omega_SIS = omega * SISfactor
omega_SIS[SIS, SIS] = omega[SIS, SIS] * np.sqrt(SISfactor[SIS])

# immigration rates
mu = np.random.uniform(minmigration,maxmigration, [S+1]); mu[0] = 0

# extinction rates
e = np.random.uniform(minextinction,maxextinction, [S+1]); e[0] = 0

initcond = np.full([S+1], int(0.66*sites/S))
initcond[0] = sites - sum(initcond[1:])

# produce 2 time series with different initial conditions but same parameters

params = {}
params['interaction_matrix'] = omega_SIS[1:,1:]
params['growth_rate'] = omega[1:,0].reshape([S, 1])
params['immigration_rate'] = mu[1:].reshape([S, 1])
params['noise_linear'] = 0

params['initial_condition'] = np.random.uniform(0,1,[S,1])
```

    /Users/wkits/anaconda/lib/python3.5/site-packages/numpy/core/numeric.py:301: FutureWarning: in the future, full([21], 330) will return an array of dtype('int64')
      format(shape, fill_value, array(fill_value).dtype), FutureWarning)

```python
ibm_noim_ts = ibm.run_timeseries(omega, mu, e, initcond, SISfactor, T=100)

params['immigration_rate'] = mu[1:].reshape([S, 1])
glv_noim_ts = glv.Timeseries(params, dt=1e-2, tskip=9, T=50, seed=int(time.time())).timeseries

PlotTimeseriesComparison([ibm_noim_ts], composition=['ts'], titles=['IBM'])
PlotTimeseriesComparison([glv_noim_ts], composition=['ts'], titles=['gLV'])

# plot steady states (end point of time series)

plt.figure()

for ts, label in zip([ibm_noim_ts, glv_noim_ts], ['IBM', 'gLV']):
    # get steady state (endpoint time series)
    ss = ts[[col for col in ts.columns if col.startswith('species')]].values[-1]

    # normalize
    ss_norm = ss/np.sum(ss)

    # plot
    plt.plot(range(1, len(ss)+1), ss_norm, alpha=0.5)
    plt.scatter(range(1, len(ss)+1), ss_norm, label=label, alpha=0.5)
plt.legend()

plt.show()
```

# Gillespie IBM

What about the algorithm based on the one of Franziska?

```python
# produce 2 time series with different initial conditions but same parameters

S = 20
sites = 10000

params = {}
params['interaction_matrix'] = omega_SIS[1:S+1,1:S+1] / sites
params['growth_rate'] = omega[1:S+1,0].reshape([S, 1]) / sites
params['immigration_rate'] = mu[1:S+1].reshape([S, 1])
params['noise_linear'] = 0

propfunc = Franziska.propensities_Franziska(params)

initcond = np.full([S+1], int(0.8*sites/S))
initcond[0] = sites - sum(initcond[1:])
f_ts1 = Franziska.TimeseriesGillespie(propfunc, dt=1e-1, T=20, initcond=initcond, empty=0).timeseries

initcond = np.full([S+1], int(0.2*sites/S))
initcond[0] = sites - sum(initcond[1:])
f_ts2 = Franziska.TimeseriesGillespie(propfunc, dt=1e-1, T=20, initcond=initcond, empty=0).timeseries

PlotTimeseriesComparison([f_ts1, f_ts2], composition=['ts'])
plt.show()
```

```python
plt.figure()

for ts, label in zip([Z_ts1, Z_ts2, ibm_ts, glv_ts1, glv_ts2],
                     ['Zdenko 1', 'Zdenko 2', 'IBM', 'gLV 1', 'gLV 2']):
    # get steady state (endpoint time series)
    ss = ts[[col for col in ts.columns if col.startswith('species')]].values[-1]

    # normalize
    ss_norm = ss/np.sum(ss)

    # plot
    plt.plot(range(1, len(ss)+1), ss_norm, alpha=0.5)
    plt.scatter(range(1, len(ss)+1), ss_norm, label=label, alpha=0.5)
plt.legend()

plt.show()
```

# Do we need immigration for stability?

```python
# specify all parameters

S = 20                              # number of species in the metacommunity
s = None                           # number of species in each local community
sites = 10000
connectance = 0.3

SIS = [1]
SISfactor_ = 1 #200
maxint = 0.5
minmigration = 0.; maxmigration = 0.
minextinction = 0.3; maxextinction = 0.3

SISfactor = np.ones(S+1, dtype=int)
SISfactor[SIS] *= SISfactor_

omega = np.random.uniform(-maxint,maxint, [S+1, S+1])
omega *= np.random.choice([0, 1], omega.shape, p=[1-connectance, connectance]) # connectance
omega[:,0] = 0.3 # growth rates

np.fill_diagonal(omega, -1.) # self-interaction
omega[0,:] = 0 # no growth of empty sites

omega_SIS = omega * SISfactor
omega_SIS[SIS, SIS] = omega[SIS, SIS] * np.sqrt(SISfactor[SIS])

# immigration rates
mu = np.random.uniform(minmigration,maxmigration, [S+1]); mu[0] = 0

# extinction rates
e = np.random.uniform(minextinction,maxextinction, [S+1]); e[0] = 0
```

```python
ts = Zdenko.timeseries(omega, mu, e, SIS, SISfactor, s, sites).drop('empty', axis='columns')
PlotTimeseries(ts)
```

# Rank abundance

```python
# specify all parameters

S = 200                              # number of species in the metacommunity
s = None                           # number of species in each local community
sites = 4000
connectance = 0.01

SIS = [1]
SISfactor_ = 1 #200
maxint = 0.5
minmigration = 0.025; maxmigration = 0.025
minextinction = 0.1; maxextinction = 0.1

SISfactor = np.ones(S+1, dtype=int)
SISfactor[SIS] *= SISfactor_

omega = np.random.uniform(-maxint,maxint, [S, S])
omega *= np.random.choice([0, 1], omega.shape, p=[1-connectance, connectance]) # connectance
np.fill_diagonal(omega, -1.) # self-interaction

growth_rates = np.random.uniform(0, 0.6, S)

# immigration rates
mu = np.random.uniform(minmigration,maxmigration, [S+1]); mu[0] = 0

# extinction rates
e = np.random.uniform(minextinction,maxextinction, [S+1]); e[0] = 0

# initial condition
initcond = np.zeros(S, dtype=int); initcond[0:2] = 1;

params = {}
params['interaction_matrix'] = omega
params['SISvector'] = SISfactor
params['immigration_rate'] = mu
params['extinction_rate'] = e
params['initial_condition'] = initcond
params['sites'] = sites
params['growth_rate'] = growth_rates

ts = ibm.Timeseries(params, T=500)

PlotTimeseries(ts.timeseries)

PlotRankAbundance(ts.endpoint)
```

```python
fig = plt.figure(figsize=(15,4))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)

for ax in [ax1, ax2]:
    ax.plot(np.sort(ts.timeseries.iloc[1])[::-1], label='1')
    ax.plot(np.sort(ts.timeseries.iloc[5])[::-1], label='5')
    ax.plot(np.sort(ts.timeseries.iloc[49])[::-1], label='50')
    ax.plot(np.sort(ts.timeseries.iloc[99])[::-1], label='100')

    ax.set_yscale('log')
ax2.set_xscale('log')
plt.legend()
```

Connection steady state of IBM and steady state of gLV.ipynb


# 标准合并问题 Standard Pooling Problem

## 目的和先决条件

Gurobi 9.0的新功能之一是增加了双线性求解器，它可以找到非凸二次规划问题（即QP，QCQP，MIQP和MIQCQP）的最佳解决方案。本笔记本将通过处理标准池问题（最著名的双线性程序之一）的实例，向您展示如何使用此功能。此外，我们将介绍两个替代公式，将其转换为二次约束二次问题，以在调用优化程序时对比它们的性能。

此建模示例处于高级水平。为了完全理解此笔记本的内容，读者应熟悉以下内容：

- Python.
- Gurobi Python interface.
- Advanced knowledge of building mathematical optimization models. Typically, the constraints of these Jupyter Notebooks are complex or require advanced features of the Gurobi Python Interface.

**Note:** You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=Github&utm_medium=website_JupyterME&utm_campaign=CommercialDataScience) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=Github&utm_medium=website_JupyterME&utm_campaign=AcademicDataScience) as an *academic user*.

---
## Motivation

The pooling problem is a challenging problem in the petrochemical refining, wastewater treatment and mining industries. This problem can be regarded as a generalization of the minimum-cost flow problem and the blending problem. It is indeed important because of the significant savings it can generate, so it comes at no surprise that it has been studied extensively since Haverly pointed out the non-linear structure of this problem in 1978 [5].

---
## Problem Description

The Minimum-Cost Flow Problem (MCFP) seeks to find the cheapest way of sending a certain amount of flow from a set of source nodes to a set of target nodes, possibily via transshipment nodes, in a directed capacitated network. The Blending Problem is a type of MCFP with only source and target nodes, where raw materials with different attribute qualities are blended together to create end products in such a way that their attribute qualities are within tolerances.

The Pooling Problem combines features of both problems, as flow streams from different sources are mixed at intermediate pools and blended again at the target nodes. The non-linearity is in fact the direct result of considering pools, as the quality of a given attribute at a pool —defined as the weighted average of the qualities of the incoming streams— is an unknown quantity and thus needs to be captured by a decision variable. We refer to this problem as the Standard Pooling Problem when the network can be represented by a tripartite graph, i.e. three disjoint sets of nodes such that no nodes within the same set are adjacent. In a nutshell, it can be stated as follows: Given a list of source nodes with raw materials containing known attribute qualities, what is the cheapest way of mixing these materials at intermediate pools so as to meet the demand and tolerances at multiple target nodes? (Gupte et al., 2017) [4]. Several different formulations for the Standard Pooling Problem and its extensions exist in the literature, which can be classified into two main categories: one that consists of flow and quality variables, and the other that uses flow proportions instead of quality variables. Both categories will be considered in this notebook.

---
## Problem Instance

As an illustrative example, we will solve the second Pooling Problem posed by Rehfeldt and Tisljar in 1997 and cited by Audet et al. in 2004:

To that end, let's declare the required data structures to represent this problem instance:

```python
from itertools import product

import gurobipy as gp
import numpy as np
import pandas as pd
from gurobipy import GRB

attrs = {'den', 'bnz', 'roz', 'moz'}

sources, cost, supply, content = gp.multidict({
    "s1": [49.2, 6097.56, {'den': 0.82, 'bnz':3, 'roz':99.2,'moz':90.5}],
    "s2": [62.0, 16129, {'den': 0.62, 'bnz':0, 'roz':87.9,'moz':83.5}],
    "s3": [300.0, 500, {'den': 0.75, 'bnz':0, 'roz':114,'moz':98.7}]
})

targets, price, demand, min_tol, max_tol = gp.multidict({
    "t1": [190, 500, {'den': 0.74, 'roz':95,'moz':85}, {'den': 0.79}],
    "t2": [230, 500, {'den': 0.74, 'roz':96,'moz':88}, {'den': 0.79, 'bnz':0.9}],
    "t3": [150, 500, {'den': 0.74, 'roz':91}, {'den': 0.79}]
})

pools, cap = gp.multidict({
    "p1": 1250,
    "p2": 1750
})

# The function `product` deploys the Cartesian product of elements in sets A and B
s2p = set(product(sources, pools))
p2t = set(product(pools, targets))
s2t = {("s1", "t2"),
       ("s2", "t1"),
       ("s2", "t3"),
       ("s3", "t1")}
```

---
## Solution Approach

Mathematical programming is a declarative approach where the modeler formulates a mathematical optimization model that captures the key aspects of a complex decision problem. The Gurobi Optimizer solves such models using state-of-the-art mathematics and computer science.

A mathematical optimization model has five components, namely:

- Sets and indices.
- Parameters.
- Decision variables.
- Objective function(s).
- Constraints.

A quadratic constraint that involves only products of disjoint pairs of variables is often called a bilinear constraint, and a model that contains bilinear constraints is often called a Bilinear Program. Bilinear constraints are a special case of non-convex quadratic constraints. This type of problems is typically solved using spatial Branch and Bound (sB&B). This algorithm explores the entire search space, so it provides a globally valid lower bound on the optimal objective value and —given enough time— it will find a globally optimal solution (subject to tolerances). The interested reader is referred to [references](#references) [3], [6] and [7].

We now present two alternative Bilinear Programs for the Standard Pooling Problem:

### P-formulation (Concentration)

#### Sets and Indices

$G=(V,E)$: Directed graph.

$i,j \in V$: Set of nodes.

$(i,j) \in E \subset  V \times V$: Set of edges.

$N(i)^+ = \{j \in V \mid (i,j) \in E \}$: Set of successor nodes receiving outflow from node $i$.

$N(j)^- = \{i \in V \mid (i,j) \in E \}$: Set of predecessor nodes sending inflow to node $i$.

$k \in \text{Attrs}$: Set of attributes.

$s \in \text{Sources} \subset V$: Set of source nodes, i.e. $N(s)^-= \emptyset$.

$t \in \text{Targets} \subset V$: Set of target nodes, i.e. $N(t)^+= \emptyset$.

$p \in \text{Pools} = V \setminus (\text{Sources} \cup \text{Targets})$: Set of pools.

#### Parameters

$\text{Cost}_s \in \mathbb{R}^+$: Cost of acquiring one unit of raw material at source node $s$.

$\text{Supply}_s \in \mathbb{R}^+$: Maximum number of units of raw material available at source node $s$.

$\text{Content}_{s,k} \in \mathbb{R}^+$: Content of attribute $k$ in raw material at source node $s$.

$\text{Price}_t \in \mathbb{R}^+$: Price for selling one unit of final blend at target node $t$.

$\text{Demand}_t \in \mathbb{R}^+$: Minimum number of units required of final blend at target node $t$.

$\text{Min_tol}_{t,k} \in \mathbb{R}^+$: Minimum tolerance for attribute $k$ in final blend at target node $t$.

$\text{Max_tol}_{t,k} \in \mathbb{R}^+$: Maximum tolerance for attribute $k$ in final blend at target node $t$.

$\text{Cap}_p \in \mathbb{R}^+$: Maximum Capacity to store intermediate blend at pool $p$.

$\text{UB}_{i,j}\in \mathbb{R}^+$: Maximum flow from node $i$ to node $j$.

#### Decision Variables

$\text{flow}_{i,j} \in [0, \text{UB}_{i,j}]$: Flow from node $i$ to node $j$.

$\text{quality}_{p,k} \in \mathbb{R}^+$: Concentration of attribute $k$ at pool $p$.

#### Objective Function

- **Profit**: Maximize total profits.

\begin{equation}
\text{Max} \quad Z = \sum_{t \in \text{Targets}}{\sum_{i \in N(t)^-}{\text{Price}_t \cdot \text{flow}_{i,t}}} - \sum_{s \in \text{Sources}}{\sum_{j \in N(s)^+}{\text{Cost}_s \cdot \text{flow}_{s,j}}}
\tag{0}
\end{equation}

#### Constraints

- **Flow conservation**: Total inflow of pool $p$ must be equal to its total outflow (nothing is stored in them).

\begin{equation}
\sum_{t \in N(p)^+}{\text{flow}_{p,t}} - \sum_{s \in N(p)^-}{\text{flow}_{s,p}} = 0 \quad \forall p \in \text{Pools}
\tag{1}
\end{equation}

- **Source capacity**: Total outflow of source $s$ cannot exceed its capacity.

\begin{equation}
\sum_{j \in N(s)^+}{\text{flow}_{s,j}} \leq \text{Supply}_s \quad \forall s \in \text{Sources}
\tag{2}
\end{equation}

- **Pool capacity**: Total outflow of pool $p$ cannot exceed its capacity.

\begin{equation}
\sum_{t \in N(p)^+}{\text{flow}_{p,t}} \leq \text{Cap}_p \quad \forall p \in \text{Pools}
\tag{3}
\end{equation}

- **Target demand**: Total inflow of target $t$ must at least meet its minimum demand.

\begin{equation}
\sum_{i \in N(t)^-}{\text{flow}_{i,t}} \geq \text{Demand}_t \quad \forall t \in \text{Targets}
\tag{4}
\end{equation}

- **Pool concentration**: Concentration of attribute $k$ at pool $p$ is expressed as the weighted average (linear blending) of the concentrations associated to the incoming flows (notice the bilinear terms on the right-hand side).

\begin{equation}
\sum_{s \in N(p)^-}{\text{Content}_{s,k} \cdot \text{flow}_{s,p}} = \text{quality}_{p,k} \cdot \sum_{t \in N(p)^+}{\text{flow}_{p,t}} \quad \forall (p,k) \in \text{Pools} \times \text{Attrs}
\tag{5}
\end{equation}

- **Target tolerances**: Concentration of attribute $k$ at target $t$ is also the result of linear blending, and must be within tolerances (notice the bilinear terms on the second expression of the left-hand side).

\begin{equation}
\sum_{s \in N(t)^- \cap \text{Sources}}{\text{Content}_{s,k} \cdot \text{flow}_{s,t}}+ \sum_{p \in N(t)^- \cap \text{Pools}}{\text{quality}_{p,k} \cdot \text{flow}_{p,t}} \geq \text{Min_tol}_{t,k} \cdot \sum_{i \in N(t)^-}{\text{flow}_{i,t}} \quad \forall (t,k) \in \text{Targets} \times \text{Attrs}
\tag{6.1}
\end{equation}

\begin{equation}
\sum_{s \in N(t)^- \cap \text{Sources}}{\text{Content}_{s,k} \cdot \text{flow}_{s,t}}+ \sum_{p \in N(t)^- \cap \text{Pools}}{\text{quality}_{p,k} \cdot \text{flow}_{p,t}} \leq \text{Max_tol}_{t,k} \cdot \sum_{i \in N(t)^-}{\text{flow}_{i,t}} \quad \forall (t,k) \in \text{Targets} \times \text{Attrs}
\tag{6.2}
\end{equation}

The number of bilinear terms in this formulation is proportional to the number of attributes. An alternative formulation relies on decision variables that represent fractions of flow instead of concentrations, so that the bilinear terms are no longer associated to the number of attributes. Two types of decision variables may be used:

- fraction of total inflow at pool $p$, coming from source $s$.
- fraction of total outflow at pool $p$, going to terminal $t$.

A model based on the first option is now presented:

### Q-formulation (Proportion)

#### Decision Variables

$\text{flow}_{i,j} \in [0, \text{UB}_{i,j}]$: Flow from node $i$ to node $j$.

$\text{prop}_{s,p} \in \mathbb{R}^+$: fraction of total inflow at pool $p$, coming from source $s$.

**Note:** The $\text{flow}$ variables from sources to pools are replaced by the $\text{prop}$ variables.

#### Objective Function

- **Profit**: Maximize total profits (notice the bilinear terms on the second expression).

\begin{equation}
\text{Max} \quad Z = \sum_{t \in \text{Targets}}{\sum_{i \in N(t)^-}{\text{Price}_t \cdot \text{flow}_{i,t}}} - \sum_{s \in \text{Sources}}{\text{cost}_s \cdot \left( \sum_{t \in N(s)^+ \cap \text{Targets}}{\text{flow}_{s,t}} + \sum_{p \in N(s)^+ \cap \text{Pools}}{\text{prop}_{s,p} \cdot \sum_{t \in N(p)^+}{\text{flow}_{p,t}}} \right) }
\tag{0}
\end{equation}

#### Constraints

- **Source capacity**: Total outflow of source $s$ cannot exceed its capacity (notice the bilinear terms on the first expression of the left-hand side).

\begin{equation}
\sum_{p \in N(s)^+ \cap \text{Pools}}{\text{prop}_{s,p} \cdot \sum_{t \in N(p)^+}{\text{flow}_{p,t}}} + \sum_{t \in N(s)^+ \cap \text{Targets}}{\text{flow}_{s,t}} \leq \text{Supply}_s \quad \forall s \in \text{Sources}
\tag{1}
\end{equation}

- **Pool capacity**: Total outflow of pool $p$ cannot exceed its capacity.

\begin{equation}
\sum_{t \in N(p)^+}{\text{flow}_{p,t}} \leq \text{Cap}_p \quad \forall p \in \text{Pools}
\tag{2}
\end{equation}

- **Target demand**: Total inflow of target $t$ must at least meet its minimum demand.

\begin{equation}
\sum_{i \in N(t)^-}{\text{flow}_{i,t}} \geq \text{Demand}_t \quad \forall t \in \text{Targets}
\tag{3}
\end{equation}

- **Pool inflow**: The sum of the contributions of all incoming sources to pool $p$ must equal one.

\begin{equation}
\sum_{s \in N(p)^-}{\text{prop}_{s,p}} = 1 \quad \forall p \in \text{Pools}
\tag{4}
\end{equation}

- **Target tolerances**: Concentration of attribute $k$ at target $t$ is also the result of linear blending, and must be within tolerances (notice the bilinear terms on the second expression of the left-hand side).

\begin{equation}
\sum_{s \in N(t)^- \cap \text{Sources}}{\text{Content}_{s,k} \cdot \text{flow}_{s,t}} + \sum_{p \in N(t)^- \cap \text{Pools}}{\text{flow}_{p,t} \cdot \sum_{s \in N(p)^-}{\text{content}_{s,k} \cdot \text{prop}_{s,p}}}
\geq \text{Min_tol}_{t,k} \cdot \sum_{i \in N(t)^-}{\text{flow}_{i,t}} \\
\forall (t,k) \in \text{Targets} \times \text{Attrs}
\tag{5.1}
\end{equation}

\begin{equation}
\sum_{s \in N(t)^- \cap \text{Sources}}{\text{Content}_{s,k} \cdot \text{flow}_{s,t}} + \sum_{p \in N(t)^- \cap \text{Pools}}{\text{flow}_{p,t} \cdot \sum_{s \in N(p)^-}{\text{content}_{s,k} \cdot \text{prop}_{s,p}}}
\leq \text{Max_tol}_{t,k} \cdot \sum_{i \in N(t)^-}{\text{flow}_{i,t}} \\
\forall (t,k) \in \text{Targets} \times \text{Attrs}
\tag{5.2}
\end{equation}

One drawback is that if some of the source-to-pool edges have flow capacity, we need to define additional constraints instead of just specifying upper bounds on the associated decision variables. Such constraints can be defined with bilinear terms as follows:

- **Flow limit**: Flow from source $s$ to pool $p$ cannot exceed the installed capacity (notice the bilinear terms on the left-hand side). In the P-formulation, declaring this is as easy as setting the upper bound of the associated $\text{flow}$ variable. However, this variable no longer exists in the Q-formulation so we need to model the capacity as a constraint.

\begin{equation}
\text{prop}_{s,p} \cdot \sum_{t \in N(p)^+}{\text{flow}_{p,t}} \leq \text{UB}_{s,p} \quad \forall (i,j) \in E \cap \left( \text{Sources} \times \text{Pools} \right) \mid \text{UB}_{i,j} < \infty
\tag{6}
\end{equation}

---
## Python Implementation

Solving Bilinear Programs with Gurobi is as easy as configuring the global parameter `nonConvex`. When setting this parameter to a value of 2, non-convex quadratic problems are solved by means of translating them into bilinear form and applying sB&B. We will first deploy the P-formulation model, and then compare it with the Q-formulation model:

### P-formulation (Concentration)

```python
p_pooling = gp.Model("Pooling")

# Set global parameters
p_pooling.params.nonConvex = 2
p_pooling.params.timelimit = 5*60 # time limit of 5 minutes

# Declare decision variables

# flow
ik = p_pooling.addVars(s2t, name="Source2Target")
ij = p_pooling.addVars(s2p, name="Source2Pool")
jk = p_pooling.addVars(p2t, name="Pool2Target")
ik["s1","t2"].ub = 750
ik["s3","t1"].ub = 750
# quality
prop = p_pooling.addVars(pools, attrs, name="Proportion")

# Deploy constraint sets

# 1. Flow conservation
p_pooling.addConstrs((ij.sum('*',j) == jk.sum(j,'*') for j in pools),
                     name="Flow_conservation")
# 2. Source capacity
p_pooling.addConstrs((ij.sum(i,'*') + ik.sum(i,'*') <= supply[i] for i in sources),
                     name="Source_capacity")
# 3. Pool capacity
p_pooling.addConstrs((jk.sum(j,'*') <= cap[j] for j in pools),
                     name="Pool_capacity")
# 4. Target demand
p_pooling.addConstrs((ik.sum('*',k) + jk.sum('*',k) >= demand[k] for k in targets),
                     name="Target_demand")
# 5. Pool concentration
p_pooling.addConstrs((gp.quicksum(content[i][attr]*ij[i,j]
                               for i in sources if (i,j) in s2p)
                      == prop[j,attr]*jk.sum(j,'*') for j in pools for attr in attrs),
                     name="Pool_concentration")
# 6.1 Target (min) tolerances
p_pooling.addConstrs((gp.quicksum(content[i][attr]*ik[i,k]
                               for i in sources if (i,k) in s2t)
                      + gp.quicksum(prop[j,attr]*jk[j,k]
                                 for j in pools if (j,k) in p2t)
                      >= min_tol[k][attr]*(ik.sum('*',k) + jk.sum('*',k))
                      for k in targets for attr in min_tol[k].keys()),
                     name="Target_min_tolerances")
# 6.2 Target (max) tolerances
p_pooling.addConstrs((gp.quicksum(content[i][attr]*ik[i,k]
                               for i in sources if (i,k) in s2t)
                      + gp.quicksum(prop[j,attr]*jk[j,k]
                                 for j in pools if (j,k) in p2t)
                      <= max_tol[k][attr]*(ik.sum('*',k) + jk.sum('*',k))
                      for k in targets for attr in max_tol[k].keys()),
                     name="Target_max_tolerances")

# Deploy Objective Function

# 0. Total profit
obj = gp.quicksum(price[k]*(ik.sum('*',k) + jk.sum('*',k))
               for k in targets) \
- gp.quicksum(cost[i]*(ij.sum(i,'*') + ik.sum(i,'*'))
           for i in sources)
p_pooling.setObjective(obj, GRB.MAXIMIZE)

# Find the optimal solution
p_pooling.optimize()
```

    Using license file c:\gurobi\gurobi.lic
    Set parameter TokenServer to value SANTOS-SURFACE-
    Changed value of parameter nonConvex to 2
       Prev: -1  Min: -1  Max: 2  Default: -1
    Changed value of parameter timelimit to 300.0
       Prev: inf  Min: 0.0  Max: inf  Default: inf
    Gurobi Optimizer version 9.0.0 build v9.0.0rc2 (win64)
    Optimize a model with 10 rows, 24 columns and 38 nonzeros
    Model fingerprint: 0x59d32e14
    Model has 20 quadratic constraints
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      QMatrix range    [1e+00, 1e+00]
      QLMatrix range   [1e-02, 1e+02]
      Objective range  [5e+01, 3e+02]
      Bounds range     [8e+02, 8e+02]
      RHS range        [5e+02, 2e+04]

    Continuous model is non-convex -- solving as a MIP.

    Presolve removed 1 rows and 0 columns
    Presolve time: 0.02s
    Presolved: 125 rows, 49 columns, 311 nonzeros
    Presolved model has 24 bilinear constraint(s)
    Variable types: 49 continuous, 0 integer (0 binary)

    Root relaxation: objective 9.577646e+05, 56 iterations, 0.01 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 957764.599    0   10          - 957764.599      -     -    0s
         0     0 957764.599    0   13          - 957764.599      -     -    0s
         0     0 878230.977    0   22          - 878230.977      -     -    0s
         0     0 878230.977    0   21          - 878230.977      -     -    0s
         0     0 878230.977    0   20          - 878230.977      -     -    0s
         0     2 878230.977    0   20          - 878230.977      -     -    0s
    * 1927  1907             134    263730.11967 837698.434   218%   7.3    0s
    * 5021  3355              54    266377.71782 724146.388   172%   6.8    0s
    * 5593  3432              95    308249.57852 716081.192   132%   6.9    0s
    * 5599  3345              95    308249.57856 716081.192   132%   6.9    0s
    * 5609  3262              95    308249.57860 716081.192   132%   6.9    0s
    * 6033  3469             130    322769.88783 712632.245   121%   6.8    0s
    * 6047  3367             136    323738.64180 712632.245   120%   6.8    0s
    * 7343  3714              62    324011.27928 709866.667   119%   6.9    0s
    * 8515  4213              92    325287.96968 709866.667   118%   7.0    0s
    * 8517  4213              93    325287.96973 709866.667   118%   7.0    0s
    * 8521  4213              95    325287.96977 709866.667   118%   7.0    0s
    *12548  6096              83    325315.73755 707517.455   117%   7.7    1s
    *20116  7853             195    328873.95358 705636.713   115%   7.8    2s
    *20117  7849             195    329060.83777 705636.713   114%   7.8    2s
    *20118  7849             194    329070.27690 705636.713   114%   7.8    2s
    *20119  7839             193    329080.21283 705636.713   114%   7.8    2s
    *20120  7839             192    329090.67171 705636.713   114%   7.8    2s
    *20121  7839             191    329101.68105 705636.713   114%   7.8    2s
    *23551  6770             121    346978.40032 700617.165   102%   8.9    2s
    *23552  6770             121    346978.40257 700617.165   102%   8.9    2s
    *23557  6770             120    346978.40681 700617.165   102%   8.9    2s
     64724 36164 infeasible   56      346978.407 666107.243  92.0%   7.4    5s
     146554 98426 573450.731   61   10 346978.407 665919.393  91.9%   6.4   10s
     217069 148294 infeasible   62      346978.407 665907.668  91.9%   6.4   15s
     286106 196549 588780.141  100    7 346978.407 665905.832  91.9%   6.7   20s
     367033 255876 470531.514   85    6 346978.407 665905.506  91.9%   6.7   25s
     446831 315965 665905.382   75    9 346978.407 665905.463  91.9%   6.6   30s
     523723 373196 574337.483  129   10 346978.407 665905.454  91.9%   6.7   35s
     596512 430108 587451.543  157   10 346978.407 665905.452  91.9%   6.7   40s
     669032 487207 588752.309  114    8 346978.407 665905.451  91.9%   6.7   45s
     735697 539826 infeasible  147      346978.407 665905.450  91.9%   6.8   50s
     804933 594427 587243.791  132   12 346978.407 665905.450  91.9%   6.8   55s
     869363 644240 infeasible   91      346978.407 665905.449  91.9%   6.8   60s
     925972 688706 587453.210  153   11 346978.407 665905.449  91.9%   6.8   65s
     987758 737398 508376.862  143   12 346978.407 665905.449  91.9%   6.8   70s
     1045671 782955 587448.100  162   13 346978.407 665905.448  91.9%   6.8   75s
     1099488 824053 597466.701   81   13 346978.407 665905.448  91.9%   6.9   80s
     1145517 860103     cutoff  100      346978.407 665905.448  91.9%   6.9   85s
     1185587 891503 587466.790  119    9 346978.407 665905.448  91.9%   6.9   90s
     1234648 929817 429795.325   96   13 346978.407 665905.448  91.9%   6.9   95s
     1278698 965633 587456.630  149    9 346978.407 665905.448  91.9%   6.9  100s
     1325849 1002349 493064.825   93   13 346978.407 665905.447  91.9%   6.9  105s
     1372171 1038902 574392.037  150   18 346978.407 665905.447  91.9%   6.9  110s
     1415976 1073276 587453.870  178    8 346978.407 665905.447  91.9%   6.9  115s
     1452666 1102042 587408.017  185   13 346978.407 665905.447  91.9%   6.9  120s
     1494087 1135484 518491.257   99   11 346978.407 665905.447  91.9%   6.9  125s
     1533746 1166382 587455.978  158   11 346978.407 665905.447  91.9%   6.9  130s
     1569354 1195994 591156.447   85    8 346978.407 665905.447  91.9%   6.9  135s
     1602435 1223016 infeasible   90      346978.407 665905.447  91.9%   6.9  140s
     1629176 1244050 592746.319   90   15 346978.407 665905.447  91.9%   6.9  145s
     1658759 1266971 587503.448  111    9 346978.407 665905.447  91.9%   6.9  150s
     1691677 1293037 infeasible  198      346978.407 665905.447  91.9%   6.9  155s
     1721818 1317169 587452.069  164   14 346978.407 665905.447  91.9%   6.9  160s
     1751223 1340185 infeasible  192      346978.407 665905.446  91.9%   6.9  165s
     1780045 1363452 585006.800  122    8 346978.407 665905.446  91.9%   6.9  170s
     1808104 1386855 665905.446   85   15 346978.407 665905.446  91.9%   6.9  175s
     1829333 1403671 589411.413  133    9 346978.407 665905.446  91.9%   6.9  180s
     1853880 1422918 594834.193   85   14 346978.407 665905.446  91.9%   6.9  185s
     1878017 1441690 587460.140  128    5 346978.407 665905.446  91.9%   6.9  190s
     1897993 1457668 553412.444  143    7 346978.407 665905.446  91.9%   6.9  195s
     1918186 1473566 587551.973  108   10 346978.407 665905.446  91.9%   6.9  200s
     1935433 1487525 infeasible   86      346978.407 665905.446  91.9%   6.9  205s
     1951817 1500274     cutoff   95      346978.407 665905.446  91.9%   6.9  210s
     1966580 1512279 594738.299  103    6 346978.407 665905.446  91.9%   6.9  215s
     1984393 1526167 588701.419  117    6 346978.407 665905.446  91.9%   6.9  220s
     2003743 1541428 587107.111  117    8 346978.407 665905.446  91.9%   6.9  225s
     2026379 1559255 587456.458  172    7 346978.407 665905.446  91.9%   6.9  230s
     2047413 1576413 588788.175  104    8 346978.407 665905.446  91.9%   6.9  235s
     2068624 1593485 665905.444   90   11 346978.407 665905.446  91.9%   6.9  240s
     2090768 1610499 587457.490  137    8 346978.407 665905.446  91.9%   6.9  245s
     2111937 1627414 infeasible  203      346978.407 665905.446  91.9%   6.9  250s
     2125245 1638227 418041.761   87    9 346978.407 665905.446  91.9%   6.9  255s
     2144493 1652753 587415.379  140   13 346978.407 665905.446  91.9%   6.9  260s
     2164972 1669513 588880.129  107    8 346978.407 665905.446  91.9%   6.9  265s
     2186542 1685907 infeasible  211      346978.407 665905.446  91.9%   6.9  270s
     2207633 1701919 577032.735  107   15 346978.407 665905.446  91.9%   6.9  275s
     2229326 1719066 587624.771  134    9 346978.407 665905.446  91.9%   6.9  280s
     2249495 1735417 587458.057  137    9 346978.407 665905.445  91.9%   6.9  285s
     2272006 1753436 589725.738   99    6 346978.407 665905.445  91.9%   6.9  290s
     2294769 1771504 infeasible   94      346978.407 665905.445  91.9%   6.9  295s
     2312724 1786109 597466.046   93   17 346978.407 665905.445  91.9%   6.9  300s

    Explored 2313831 nodes (16012361 simplex iterations) in 300.01 seconds
    Thread count was 8 (of 8 available processors)

    Solution count 10: 346978 346978 346978 ... 325316

    Time limit reached
    Warning: max constraint violation (1.6281e-06) exceeds tolerance
    Best objective 3.469784068080e+05, best bound 6.659054453585e+05, gap 91.9155%

The P-formulation for this instance has:

- 24 decision variables.
- 10 linear constraints.
- 20 bilinear constraints.
- a linear objective function.

As can be seen, we still observe a gap of 64.82% after reaching the time limit of five minutes (at this point, the incumbent solution induces a total profit of 429,486.87 USD). In fact, even after 20 minutes the solver does not make much progress in closing the gap.

### Q-Formulation (proportion)

Let's now see how the q-formulation model performs:

```python
q_pooling = gp.Model("Pooling")

# Set global parameters
q_pooling.params.nonConvex = 2
q_pooling.params.timelimit = 5*60

# Declare decision variables

# flow
ik = q_pooling.addVars(s2t, name="Source2Target")
jk = q_pooling.addVars(p2t, name="Pool2Target")
ik["s1","t2"].ub = 750
ik["s3","t1"].ub = 750
# proportion
p_ij = q_pooling.addVars(s2p, ub=1.0, name="Prop_Source2Pool")

# Deploy constraint sets

# 1. Source capacity
q_pooling.addConstrs((gp.quicksum(p_ij[i,j]*jk.sum(j,'*')
                               for j in pools if (i,j) in s2p)
                      + ik.sum(i,'*') <= supply[i] for i in sources),
                     name="Source_capacity")
# 2. Pool capacity
q_pooling.addConstrs((jk.sum(j,'*') <= cap[j] for j in pools),
                     name="Pool_capacity")
# 3. Target demand
q_pooling.addConstrs((ik.sum('*',k) + jk.sum('*',k) >= demand[k] for k in targets),
                     name="Target_demand")
# 4. Pool inflow
q_pooling.addConstrs((p_ij.sum('*',j) == 1 for j in pools),
                     name="Pool_inflow")
# 5.1 Target (min) tolerances
q_pooling.addConstrs((gp.quicksum(content[i][attr]*ik[i,k]
                               for i in sources if (i,k) in s2t)
                      + gp.quicksum(content[i][attr]*p_ij[i,j]*jk[j,k]
                                 for (i,j) in s2p if (j,k) in p2t)
                      >= min_tol[k][attr]*(ik.sum('*',k) + jk.sum('*',k))
                      for k in targets for attr in min_tol[k].keys()),
                     name="Target_min_tolerances")
# 5.2 Target (max) tolerances
q_pooling.addConstrs((gp.quicksum(content[i][attr]*ik[i,k]
                               for i in sources if (i,k) in s2t)
                      + gp.quicksum(content[i][attr]*p_ij[i,j]*jk[j,k]
                                 for (i,j) in s2p if (j,k) in p2t)
                      <= max_tol[k][attr]*(ik.sum('*',k) + jk.sum('*',k))
                      for k in targets for attr in max_tol[k].keys()),
                     name="Target_max_tolerances")

# Deploy Objective Function

# 0. Total profit
obj = gp.quicksum(price[k]*(ik.sum('*',k) + jk.sum('*',k))
               for k in targets) \
- gp.quicksum(cost[i]*(gp.quicksum(p_ij[i,j]*jk.sum(j,'*')
                             for j in pools if (i,j) in s2p)
                    + ik.sum(i,'*'))
           for i in sources)
q_pooling.setObjective(obj, GRB.MAXIMIZE)

# Find the optimal solution
q_pooling.optimize()
```

    Changed value of parameter nonConvex to 2
       Prev: -1  Min: -1  Max: 2  Default: -1
    Changed value of parameter timelimit to 300.0
       Prev: inf  Min: 0.0  Max: inf  Default: inf
    Gurobi Optimizer version 9.0.0 build v9.0.0rc2 (win64)
    Optimize a model with 7 rows, 16 columns and 22 nonzeros
    Model fingerprint: 0xfc755c91
    Model has 18 quadratic objective terms
    Model has 15 quadratic constraints
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      QMatrix range    [6e-01, 1e+02]
      QLMatrix range   [1e-02, 1e+02]
      Objective range  [9e+01, 2e+02]
      QObjective range [1e+02, 6e+02]
      Bounds range     [1e+00, 8e+02]
      RHS range        [1e+00, 2e+03]
      QRHS range       [5e+02, 2e+04]

    Continuous model is non-convex -- solving as a MIP.

    Presolve time: 0.01s
    Presolved: 95 rows, 35 columns, 315 nonzeros
    Presolved model has 18 bilinear constraint(s)
    Variable types: 35 continuous, 0 integer (0 binary)

    Root relaxation: objective 1.810743e+06, 36 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 1810743.26    0   13          - 1810743.26      -     -    0s
         0     0 865587.763    0   13          - 865587.763      -     -    0s
         0     0 595135.123    0   13          - 595135.123      -     -    0s
         0     0 564193.525    0   15          - 564193.525      -     -    0s
         0     0 556780.563    0   15          - 556780.563      -     -    0s
         0     0 549889.932    0   15          - 549889.932      -     -    0s
         0     0 549889.932    0   18          - 549889.932      -     -    0s
         0     0 549889.932    0   18          - 549889.932      -     -    0s
         0     2 549889.932    0   18          - 549889.932      -     -    0s
    *   47    54               8    404852.41910 476226.236  17.6%   7.3    0s
    H   62    52                    433077.56632 476226.236  10.0%   7.1    0s
    H   65    52                    439182.58928 476226.236  8.43%   6.9    0s

    Cutting planes:
      RLT: 18

    Explored 131 nodes (684 simplex iterations) in 0.20 seconds
    Thread count was 8 (of 8 available processors)

    Solution count 3: 439183 433078 404852

    Optimal solution found (tolerance 1.00e-04)
    Best objective 4.391825892775e+05, best bound 4.392081187464e+05, gap 0.0058%

The Q-formulation for this instance has:

- 16 decision variables.
- 7 linear constraints.
- 15 bilinear constraints.
- a bilinear objective function.

Notice it has fewer decision variables and also fewer bilinear constraints. Now Gurobi was able to find the optimal solution of 439,182.59 USD in less than one second.

---
## Analysis

Let's see the optimal flows found:

### Flow from Sources to Targets
The following table determines the flows from source nodes to target nodes. For example, from source node s2 to target node t1 there is a flow of 966.7$\times 10^{2}$ bbl.

```python
rows = sources.copy()
columns = targets.copy()
s2t_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)

for source, target in ik.keys():
    if (abs(ik[source, target].x) > 1e-6):
        s2t_plan.loc[source, target] = np.round(ik[source, target].x, 1)
s2t_plan
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>t1</th>
      <th>t2</th>
      <th>t3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>s1</th>
      <td>0.0</td>
      <td>0.0</td>
      <td>0.0</td>
    </tr>
    <tr>
      <th>s2</th>
      <td>966.7</td>
      <td>0.0</td>
      <td>200.0</td>
    </tr>
    <tr>
      <th>s3</th>
      <td>0.0</td>
      <td>0.0</td>
      <td>0.0</td>
    </tr>
  </tbody>
</table>
</div>

### Flow from Pools to Targets
The following table defines the flows from pool nodes to target nodes. For example, from pool node p1 to target node t1 there is a flow of 92.8$\times 10^{2}$ bbl.

```python
rows = pools.copy()
columns = targets.copy()
p2t_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)

for pool, target in jk.keys():
    if (abs(jk[pool, target].x) > 1e-6):
        p2t_plan.loc[pool, target] = np.round(jk[pool, target].x, 1)
p2t_plan
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>t1</th>
      <th>t2</th>
      <th>t3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>p1</th>
      <td>92.8</td>
      <td>990.6</td>
      <td>0.0</td>
    </tr>
    <tr>
      <th>p2</th>
      <td>1450.0</td>
      <td>0.0</td>
      <td>300.0</td>
    </tr>
  </tbody>
</table>
</div>

### Flow from Sources to Pools
The following table shows the flows from source nodes to pool nodes. For example, from source node s2 to pool node p1  there is a flow of 258.3$\times 10^{2}$ bbl.

```python
rows = sources.copy()
columns = pools.copy()
s2p_plan = pd.DataFrame(columns=columns, index=rows, data=0.0)

for source, pool in p_ij.keys():
    if (abs(p_ij[source, pool].x) > 1e-6):
        flow = p_ij[source, pool].x * p2t_plan.loc[pool,:].sum()
        s2p_plan.loc[source, pool] = np.round(flow, 1)
s2p_plan
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>p1</th>
      <th>p2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>s1</th>
      <td>325.0</td>
      <td>1750.0</td>
    </tr>
    <tr>
      <th>s2</th>
      <td>258.3</td>
      <td>0.0</td>
    </tr>
    <tr>
      <th>s3</th>
      <td>500.0</td>
      <td>0.0</td>
    </tr>
  </tbody>
</table>
</div>

---
## Conclusions

This notebook showed how easy it is to solve Bilinear Programs using Gurobi. It also highlighted the dramatic difference in performance of alternative formulations when solving challenging problems, such as the Standard Pooling Problem. It is thus of utmost importance to analyze carefully the context of the problem at hand, and to weigh the pros and cons of alternative models.

---
<a id='references'></a>
## References

1. Alfaki, M. (2012). Models and solution methods for the pooling problem.
2. Audet, C., Brimberg, J., Hansen, P., Digabel, S. L., & Mladenović, N. (2004). Pooling problem: Alternate formulations and solution methods. Management science, 50(6), 761-776.
3. Dombrowski, J. (2015, June 07). McCormick envelopes. Retrieved from https://optimization.mccormick.northwestern.edu/index.php/McCormick_envelopes
4. Gupte, A., Ahmed, S., Dey, S. S., & Cheon, M. S. (2017). Relaxations and discretizations for the pooling problem. Journal of Global Optimization, 67(3), 631-669.
5. Haverly, C. A. (1978). Studies of the behavior of recursion for the pooling problem. Acm sigmap bulletin, (25), 19-28.
6. Liberti, L. (2008). Introduction to global optimization. Ecole Polytechnique.
7. Zhuang E. (2015, June 06). Spatial branch and bound method. Retrieved from
https://optimization.mccormick.northwestern.edu/index.php/Spatial_branch_and_bound_method

Copyright © 2020 Gurobi Optimization, LLC

```python

```