```python
%matplotlib inline
import sympy
import matplotlib.pyplot as plt
import numpy as np
sympy.init_printing()
```

```python
def inv_laplace(Y):
    s, t = sympy.symbols('s, t')
    # define a function to perform more complicated inv laplace
    y = 0
    for a in Y.apart().expand().args:
        y += sympy.inverse_laplace_transform(a, s, t)
    y = y.subs(
        {sympy.InverseLaplaceTransform(s, s, t, None): sympy.DiracDelta(t)}
    )
    return y

def responses(G, R):
    s, t = sympy.symbols('s, t')

    ER = (1/(1 + G)).simplify()
    YR = (G/(1 + G)).simplify()
    Y = YR*R

    r = sympy.inverse_laplace_transform(R, s, t)
    y = inv_laplace(Y)
    e = r - y
    return locals()

def f_eval(f):
    return sympy.lambdify((t), (f), modules=['numpy', {
        'Heaviside': lambda x: np.heaviside(x, 1)}])

def plots(res, t_vect):
    plt.plot(t_vect, f_eval(res['r'])(t_vect), label='r')
    plt.plot(t_vect, f_eval(res['y'])(t_vect), label='y')
    plt.plot(t_vect, f_eval(res['e'])(t_vect), 'k--', label='e')
    plt.legend()
    plt.xlabel('t')
```

# Type 0

```python
zeta = 0.75
omega_n = 3
s, t = sympy.symbols('s, t')
G = 1/(s**2 + 2*zeta*omega_n*s + omega_n**2)
```

```python
G
```

$K_p = 1/9$

$e_{ss} = \frac{1}{1 + K_p} = 9/10$

```python
res = responses(G, 1/s)
plots(res, np.arange(-1, 3, 0.001))
plt.hlines(9.0/10, -1, 3)
plt.title('unit step');
```

```python
res = responses(G, 1/s**2)
t_vect = np.arange(-1, 3, 0.001)
plots(res, np.arange(-1, 3, 0.001))
plt.title('ramp');
```

# Type 1

```python
G2 = G/s
G2
```

```python
res = responses(G2, 1/s)
plots(res, np.arange(-1, 60, 0.1))
plt.title('unit step');
```

```python
res = responses(G2, 1/s**2)
plots(res, np.arange(-1, 60, 0.1))
plt.hlines(9, 0, 60)
plt.title('ramp');
```

```python
res = responses(G2, 1/s**3)
plots(res, np.arange(-1, 60, 0.1))
plt.title('parabolic');
```

# Non-Symbolic Approach

```python
import control
s = control.tf([1, 0], [0, 1])
G = 1/(s*(s**2 + 4.5*s + 9))
G
```

$$\frac{1}{s^3 + 4.5 s^2 + 9 s}$$

```python
Kp = np.real(control.minreal(G, verbose=False).horner(0)[0][0])
ess_step = 1/(1 + Kp)
Kp, ess_step
```

```python
Kv = np.real(control.minreal(s*G, verbose=False).horner(0)[0][0])
ess_ramp = 1/Kv
Kv, ess_ramp
```

```python
Ka = np.real(control.minreal(s**2*G, verbose=False).horner(0)[0][0])
ess_parab = 1/Ka
Ka, ess_parab
```

```python
t, y = control.step_response((G/(1 + G)))
r = np.ones(len(t))
plt.plot(t, r, label='r')
plt.plot(t, y, label='y')
plt.plot(t, r - y, 'k--', label='e')
plt.legend()
plt.title('unit step')
```

```python
t = np.linspace(0, 60)
t, y = control.step_response((G/(1 + G))/s, t)
r = t
plt.plot(t, r, label='r')
plt.plot(t, y, label='y')
plt.plot(t, r - y, 'k--', label='e')
plt.legend()
plt.title('ramp')
```

```python
t = np.linspace(0, 60)
t, y = control.step_response((G/(1 + G))/s**2/2, t)
r = t**2/2
plt.plot(t, r, label='r')
plt.plot(t, y, label='y')
plt.plot(t, r - y, 'k--', label='e')
plt.legend()
plt.title('parabolic')
```

```python
G = (s-2)/(s+1)
control.rlocus(G, np.linspace(0, 0.5));
```

```python
G = (s+2)/(s*(s-2))
control.rlocus(G, np.linspace(0, 100));
```

```python
t = np.linspace(0, 60)
G = (s+1)/((s+2)*(s+3))
t, y = control.step_response((G/(1 + G))*2, t)
r = 2*np.ones(len(t))
plt.plot(t, r, label='r')
plt.plot(t, y, label='y')
plt.hlines(12.0/7, t[0], t[-1])
plt.plot(t, r - y, 'k--', label='e')
plt.legend()
plt.title('step')
```

```python
t = np.linspace(0, 30)
G = (s+1)/(s*(s+2)*(s+3))
t, y = control.step_response((G/(1 + G))*10/s, t)
r = 10*t
plt.plot(t, r, label='r')
plt.plot(t, y, label='y')
plt.plot(t, r - y, 'k--', label='e')
plt.hlines(60, 0, 30)
plt.legend()
plt.title('step')
```

```python
G = (s+2)/(s-2)
control.rlocus(G, np.linspace(0, 1, 1000));
plt.grid();

plt.figure()
control.bode(G);
```

```python
G = (s+2)/(s*(s-2))
control.rlocus(G, np.linspace(0, 20, 1000));
plt.grid();

plt.figure()
control.bode(G);
```

```python
G = 1/s
control.rlocus(G, np.linspace(0, 2, 1000));
plt.grid();
```

```python
G = 1/(s*(s+2))
control.rlocus(G, np.linspace(0, 2, 1000));
plt.grid();
```

```python
G = (s+2)/(s*(s+1))
#control.rlocus(G, np.linspace(0, 1, 1000));
#plt.grid();
```


# Balancer

Measure the vibration caused by an unknown imbalance:
$$m_1\left(s\right) = G\left(s\right) \cdot \epsilon$$
Then add a known test imbalance.
$$m_2\left(s\right) = G\left(s\right) \cdot \left( \epsilon + \epsilon_\text{test} \right)$$
solve the system of linear equations for $\epsilon_\text{test}$.
\begin{align}
     & \frac{m_1\left(s\right)}{m_2\left(s\right)} = \frac{\epsilon}{\epsilon + \epsilon_\text{test}} \\
\iff & \epsilon \frac{m_1\left(s\right)}{m_2\left(s\right)} + \epsilon_\text{test} \frac{m_1\left(s\right)}{m_2\left(s\right)} = \epsilon \\
\iff & \epsilon_\text{test} \frac{m_1\left(s\right)}{m_2\left(s\right)} = \epsilon \frac{m_2\left(s\right) - m_1\left(s\right)}{m_2\left(s\right)} \\
\iff & \epsilon = \epsilon_\text{test} \frac{m_1\left(s\right)}{m_2\left(s\right) - m_1\left(s\right)} \\
\end{align}

```python

```


## Demonstration of Lorenz Coefficient for Quantifying Spatial, Subsurface Heterogeneity

#### Alan Scherman, Rice University, UT PGE 2020 SURI

#### Supervised and Updated Plots by:

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

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

***
#### Introduction

This is a demonstration of how to calculate the Lorenz Coefficient of a subsurface sample from well log porosity and permeability measurements. The Lorenz Coefficient is an important heuristic imported form the sphere of macroeconomics used to quantify the spatial heterogeneity of a subsurface sample. The Lorenz Coefficient is obtained by doubling the area between the Lorenz Curve and the Homogeneous Line. It ranges from 0 to 1 where, by convention, a coefficient of less than 0.3 suggests low heterogeneity and a coefficient of more than 0.6 indicates high heterogeneity. In practical terms, a low spatial heterogeneity allows for simple displacement of subsurface fluids and a high recovery factor.
***
#### Objective

To understand and apply the methodology to calculate the Lorenz Curve and Coefficient from porosity and permeability measurements through Python functionalities.

***
#### Calculation procedure

The following list contains the necessary steps to determine the Lorenz Coefficient of a subsurface sample:

**(1)** Sort porosity ($\phi$) and permeability (**_k_**) in descending order of ratio **_k_**/$\phi$;

**(2)** Calculate storage (**_S.C._**) and flow capacity (**_F.C._**) of each layer (region between depth measurements) with:

\begin{equation}
S.C. = \phi*h
\end{equation}
\begin{equation}
F.C. = k*h
\end{equation}

where **_h_** is the layer thickness;

**(3)** Calculate the _cumulative_ storage (**_C.S.C._**) and flow capacities (**_C.F.C._**) of each layer with:

\begin{equation}
C.S.C. = \sum^{current layer}_{i = 1} \phi*h
\end{equation}
\begin{equation}
C.F.C. = \sum^{current layer}_{i = 1} k*h
\end{equation}

**(4)** Normalize the cumulative storage and flow capacities by dividing them by the largest cumulative storage and flow capacities (i.e. the last cumulative capacities calculated), respectively;

**(5)** _(Optional)_ Plot the normalized cumulative flow capacities against the normalized cumulative storage capacities (i.e. the Lorenz Curve);

**(6)** Find a curve fit for the normalized capacities in the Cartesian plane (usually a 3rd degree polynomial is sufficient);

**(7)** Integrate to find the area between the Lorenz Curve and the Homogenous Line;

**(8)** Divide the result found in **(7)** by 0.5 to obtain the Lorenz Coefficient:

\begin{equation}
LorenzCoefficient = \frac{\int^{1}_{0} (LorenzCurve - HomogeneousLine)}{0.5}
\end{equation}
***
#### Load the required libraries

The program below utilizes some standard Python packages. These should be previously installed if you have Anaconda or other similar software.

```python
import pandas as pd                                                  # To import data from .xlsx or .csv file
import numpy as np                                                   # For numerical array management
from matplotlib import pyplot as plt                                 # For graphical display of Lorenz Curve
from matplotlib.ticker import (MultipleLocator, AutoMinorLocator) # control of axes ticks
plt.rc('axes', axisbelow=True)                            # set axes and grids in the background for all plots
```

If you get a package import error, you may have to first install some of these packages. This can usually be accomplished by opening up a command window on Windows and then typing 'python -m pip install [package-name]'. More assistance is available with the respective package docs.
***
#### Importing porosity and permeability data

The data file for this demonstration is availbale at [here](https://github.com/GeostatsGuy/GeoDataSets/blob/master/WellPorPermSample_3.xlsx).

The well log data will be imported from an Excel (.xlsx) file. There are similar Pandas methods which allow the importation of data from .csv and other common file types. First, the Excel data is stored as a Pandas data frame. Then, the depth, porosity, and permeability data are placed into separate data frames by means of their Excel column names. Make sure that the strings match every character of the column titles in the Excel file.

```python
#df = pd.read_excel('WellPorPermSample_3.xlsx')                # Include file directory with os.chdir() method if necessary
df = pd.read_csv('https://raw.githubusercontent.com/GeostatsGuy/GeoDataSets/master/WellPorPermSample_3.csv') # load from Dr. Pyrcz's GitHub respository
depth = pd.DataFrame(df, columns = ['Depth (m)']);
poros = pd.DataFrame(df, columns = ['Por (%)']);
perme = pd.DataFrame(df, columns = ['Perm (mD)']);
```

To certify that the subsurface data have been imported appropriately:

```python
df.head()
```

      Depth (m)
      Por (%)
      Perm (mD)

      0
      0.25
      13.0
      265.5

      1
      0.50
      13.6
      116.9

      2
      0.75
      9.0
      136.9

      3
      1.00
      17.6
      216.7

      4
      1.25
      9.4
      131.6

***
#### Convert data frames to NumPy arrays

In order to facilitate data management and allow the use of helpful mathematical methods, we'll convert the Pandas data frames to NumPy arrays. Also, let's transpose the arrays to row format for conventionality.

```python
depth = np.transpose(depth.to_numpy()[:,0])
poros = np.transpose(poros.to_numpy()[:,0])
perme = np.transpose(perme.to_numpy()[:,0])
```

It's always a good idea to check after every step in our workflows! Let's preview the resulting arrays:

```python
print('depth =', depth[:5], '\nporos =', poros[:5], '\nperme =', perme[:5])   # To visualize the first 5 elements of each array
```

    depth = [0.25 0.5  0.75 1.   1.25]
    poros = [13.  13.6  9.  17.6  9.4]
    perme = [265.5 116.9 136.9 216.7 131.6]

***
#### Determine thicknesses of layers

The layer thickness is found by subtracting a shallower depth from a deeper depth. For most well logs, the layers will have a uniform thickness. However, irregular thicknesses are also possible and should be accounted for. Whichever is the case, the layer thicknesses array can be calculated in the following manner:

```python
layer_thick = depth - np.concatenate(([0],depth[:-1]))
```

Note that the layer_thick array should have the same dimensions as the porosity and permeability arrays.
***
#### Step (1) - Sort data in decreasing order of k/$\phi$

Recall that the first step to calculate the Lorenz Coefficient is to sort all layers in descending order of their ratio of permeability by porosity. Because both the porosity and permeability values are expressed as NumPy arrays, these ratios can be easily obtained by:

```python
ratio = perme/poros                                                           # Equivalent to np.multiply(perme,poros)
```

Now, it's possible to sort the layer thickness, porosity and permeability arrays in the desired pattern:

```python
layer_thick = np.flip(layer_thick[np.argsort(ratio)], axis = 0);
poros = np.flip(poros[np.argsort(ratio)], axis = 0)
perme = np.flip(perme[np.argsort(ratio)], axis = 0)
```

***
#### Step (2) - Calculate storage and flow capacities

The storage and flow capacities are found by performing element-wise multiplication of the porosity and permeability arrays by the thickness array. Hence:

```python
storage_cap = poros*layer_thick
flow_cap = perme*layer_thick
```

***
#### Step (3) - Calculate the cumulative storage and flow capacities

The next step is to compute the cumulative capacities for each layer. An easy solution to this assignment is provided by a for-loop. First, we pre-allocate the cumulative storage and flow capacities arrays. Then, we set the cumulative capacities of the first layer equal to its own capacities. Finally, we run a for-loop to compute the remaining cumlative capacities for the sequential layers.

```python
cumul_storage_cap = np.zeros(len(flow_cap)+1)
cumul_flow_cap = np.zeros(len(flow_cap)+1)

cumul_storage_cap[1] = storage_cap[0]
cumul_flow_cap[1] = flow_cap[0]

for i in range(2,len(flow_cap)+1):
    cumul_storage_cap[i] = cumul_storage_cap[i-1] + storage_cap[i-1]
    cumul_flow_cap[i] = cumul_flow_cap[i-1] + flow_cap[i-1]
```

Note that the length of the cumulative capacities arrays are one index greater than the flow and storage capacity arrays. In fact, the first entry of both arrays is set to 0 (zero) in order to ensure convergence of the future Lorenz Curve to the origin.

***
#### Step (4) - Normalize the cumulative storage and flow capacities

To normalize the cumulative storage and flow capacities (i.e. limiting the range to [0,1]), we'll perform element-wise division of the cumulative storage and flow capacities by the sum of the all layers' storage and flow capacities, respectively. Thus, the normalized (also known as fractional) storage and flow capacities are given by:

```python
frac_storage_cap = cumul_storage_cap/cumul_storage_cap[-1]
frac_flow_cap = cumul_flow_cap/cumul_flow_cap[-1]
```

***
#### Step (5) - Plot the Lorenz Curve

Even though we say plot the Lorenz Curve, we'll indeed plot the fractional flow capacities against the fractional storage capacities (i.e. we'll plot some of the points on the Lorenz Curve).

Let's instantiate a figure to hold our Lorenz Curve points:

```python
plt.subplot(111)
plt.plot([0,1], [0,1], color = 'red', label = 'Homogeneous Line',linewidth=2)            # Homogeneous line
plt.scatter(frac_storage_cap, frac_flow_cap, marker = 'o',s=20,
         label = 'Lorenz Curve Points',color = 'darkorange',edgecolor = 'black',alpha=0.8)                # Sample points of Lorenz Curve
plt.xlim([0,1]); plt.ylim([0,1])
plt.grid(True, which='major',linewidth = 1.0); plt.grid(True, which='minor',linewidth = 0.2) # add y grids
plt.tick_params(which='major',length=7); plt.tick_params(which='minor', length=4)
plt.gca().xaxis.set_minor_locator(AutoMinorLocator()); plt.gca().yaxis.set_minor_locator(AutoMinorLocator()) # turn on minor ticks

plt.title('Lorenz Curve')
plt.xlabel('Cumulative Fraction of Storage Capacity ($m$)')
plt.ylabel('Cumulative Fraction of Flow Capacity ($mD \cdot m$)')
plt.legend(loc = 'lower right')

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.1, wspace=0.2, hspace=0.3); plt.show()
```

***
#### Step (6) - Find a curve fit

Now that we have some of the data points of the Lorenz Curve, we can apply a curve fit to predict the behavior of this entire subsurface sample. We'll use a 3rd degree polynomial to fit the Lorenz Curve.

```python
weights = np.ones(len(frac_storage_cap))
weights[0] = 1000; weights[-1] = 1000                                           # To ensure the fit converges to (0,0) and (1,1)

poly_fit = np.polyfit(frac_storage_cap, frac_flow_cap, deg=3, w=weights)
print('poly_fit = ', poly_fit)
```

    poly_fit =  [ 2.83252318e-01 -1.00166028e+00  1.71840790e+00  1.64140836e-07]

The resulting array contains the coefficients of the polynomial fit. To better visualize this polynomial in standard equation form:

```python
poly_fit = np.poly1d(poly_fit)
print(poly_fit)
```

            3         2
    0.2833 x - 1.002 x + 1.718 x + 1.641e-07

***
#### Step (7) - Integrate to find the area between the Lorenz Curve and the Homogenenous LIne

The advantage of having applied the _numpy.poly1d()_ method on Step (6) is that we now have a callable object for which we can evaluate it over any domain (here [0,1]). Then:

```python
integral = np.polyint(poly_fit)
print(integral)
```

             4          3          2
    0.07081 x - 0.3339 x + 0.8592 x + 1.641e-07 x

The result above can be easily validated since the fit has polynomial behavior. Now, we can evaluate the definite integral for the area under the Lorenz Curve and then subtract the area under the homogeneous line (i.e. area of isosceles triangle of side length 1 = 0.5):

```python
lorenz_area = integral(1) - integral(0) - 0.5
print(lorenz_area)
```

    0.09613043353885176

Note that for practical purposes the integral(0) term could have been ignored since the integration constant is assumed to be 0.
***
#### Step (8) - Obtain the Lorenz Coefficient

The final step of this procedure is to divide the _lorenz_area_ calculated on Step (7) by 0.5, which is the total area between the lines **y = 1** and **y = x** in the domain [0,1]:

```python
lorenz_coefficient = lorenz_area/0.5
print('Lorenz Coefficient = %.3f' %lorenz_coefficient)
```

    Lorenz Coefficient = 0.192

Because the Lorenz Coefficient sits below the conventional threshold of 0.3, we can assume that the subsurface sample near the well exhibits simple displacement of fluids and a potentially high recovery factor.

Let's replicate the plot from Step (5) with shaded areas to better illustrate this Lorenz Coefficient:

```python
#plt.plot([0,1], [0,1], color = 'black', label = 'Homogeneous Line',linewidth=2)            # Homogeneous line
plt.scatter(frac_storage_cap, frac_flow_cap, marker = 'o',s=20,
         label = 'Lorenz Curve Points',color = 'darkorange',edgecolor = 'black',alpha=0.8,zorder=10)                # Sample points of Lorenz Curve
plt.xlim([0,1]); plt.ylim([0,1])
plt.grid(True, which='major',linewidth = 1.0); plt.grid(True, which='minor',linewidth = 0.2) # add y grids
plt.tick_params(which='major',length=7); plt.tick_params(which='minor', length=4)
plt.gca().xaxis.set_minor_locator(AutoMinorLocator()); plt.gca().yaxis.set_minor_locator(AutoMinorLocator()) # turn on minor ticks

plt.plot(np.linspace(0,1,100), poly_fit(np.linspace(0,1,100)),
         linewidth = 2, label = 'Polynomial Fit', color = 'black', zorder = 10)                            # Polynomial fit
plt.fill_between(np.linspace(0,1,100), poly_fit(np.linspace(0,1,100)),
                 np.linspace(0,1,100), alpha = 0.75,label = 'Heterogeneous Zone',facecolor='red')                                            # Blue area
plt.fill_between(np.linspace(0,1,100), np.linspace(0,1,100),
                 np.zeros(100), alpha = 0.5,hatch='XX',facecolor='gray')

plt.title('Lorenz Curve, Emprical Points, Curve Fit and Coefficient')
plt.xlabel('Cumulative Fraction of Storage Capacity ($m$)')
plt.ylabel('Cumulative Fraction of Flow Capacity ($mD \cdot m$)')
plt.legend(loc = 'lower right',facecolor='white',framealpha = 1.0,edgecolor='black')
plt.text(0.3, 0.8, 'Lorenz Coefficient = \n %.3f' %lorenz_coefficient, size = 10, horizontalalignment = 'center', \
         bbox=dict(facecolor='white', edgecolor='black'))

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.1, wspace=0.2, hspace=0.3); plt.show()
```

It's now clear that, graphically, the Lorenz Coefficient is equivalent to the red area divided by the non-hatched area.
***
#### Conclusion

This exercise detailed the procedure towards calculating the Lorenz Coefficient of a subsurface sample from its porosity and permeability measurements. We also covered some of the Python resources available to improve management of well data, perform the necessary mathematical transformations, and visualize the solution curve and the Lorenz Coefficient.

If you use the GeostatsPy package, the calculation and display of the Lorenz Coefficient are available through the **lorenz_curve()** and **lorenz_display()** functions. Please check the documentation on those for more detail.

***

#### More on Michael Pyrcz and the Texas Center for Geostatistics:

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

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

For more about Michael check out these links:

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

#### Want to Work Together?

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

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

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

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

I'm always happy to discuss,

*Michael*

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

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


# Standard Pooling Problem

## Objective and Prerequisites

One of the new features of Gurobi 9.0 is the addition of a bilinear solver, which enables finding the optimal solution of non-convex quadratic programming  problems (i.e. QPs, QCQPs, MIQPs, and MIQCQPs). This notebook will show you how to use this feature by tackling an instance of the Standard Pooling Problem, one of the most well-known Bilinear Programs. Moreover, we will present two alternative formulations, casted as Quadratically Constrained Quadratic Problems, to contrast their performance when calling the optimizer.

This modeling example is at the advanced level. To fully understand the content of this notebook, the reader should be familiar with the following:

- Python.
- Gurobi Python interface.
- Advanced knowledge of building mathematical optimization models. Typically, the constraints of these Jupyter Notebooks are complex or requires 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
import numpy as np
import pandas as pd
from itertools import product

import gurobipy as gp
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: 0xbc6a0534
    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.00s
    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.00 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   21          - 878230.977      -     -    0s
         0     0 878230.977    0   21          - 878230.977      -     -    0s
         0     0 878230.977    0   21          - 878230.977      -     -    0s
         0     2 878230.977    0   21          - 878230.977      -     -    0s
    * 7755  5016             176    361559.89746 794608.854   120%   8.0    0s
    H 8002  4250                    429486.87554 791222.570  84.2%   7.9    0s
     59550 35103     cutoff  181      429486.876 708419.794  64.9%   8.8    5s
     123791 79228 574846.165   53   24 429486.876 708114.573  64.9%   8.1   10s
     188466 121191 537322.609   69   24 429486.876 708051.981  64.9%   8.0   15s
     248805 160411     cutoff  134      429486.876 708020.291  64.9%   7.9   20s
     313890 202596 707864.463  130   20 429486.876 707998.040  64.8%   7.9   25s
     381875 245096 631360.129   61   24 429486.876 707979.895  64.8%   7.9   30s
     444602 283579 707865.779  102   15 429486.876 707966.661  64.8%   7.8   35s
     508797 308664 446073.443   78   12 429486.876 707962.142  64.8%   8.4   40s
     556417 311361     cutoff   90      429486.876 707962.141  64.8%   9.5   45s
     603185 308581     cutoff   81      429486.876 707962.141  64.8%  10.5   50s
     650438 309275 706950.609   92   16 429486.876 707959.763  64.8%  11.2   55s
     725142 351325     cutoff  145      429486.876 707945.315  64.8%  10.8   60s
     793172 392774 707937.753  104   12 429486.876 707937.775  64.8%  10.5   65s
     850759 403557     cutoff   85      429486.876 707937.753  64.8%  11.0   70s
     893386 404251     cutoff   93      429486.876 707937.753  64.8%  11.5   75s
     936348 399987 644061.179   84    9 429486.876 707937.752  64.8%  12.0   80s
     987726 407645 639269.081   76   13 429486.876 707935.300  64.8%  12.2   85s
     1055140 447996 707865.718  134   16 429486.876 707925.810  64.8%  11.9   90s
     1124864 489466 562854.589   95   20 429486.876 707919.386  64.8%  11.6   95s
     1187555 513018 570791.957   96    9 429486.876 707917.739  64.8%  11.6  100s
     1235468 515645     cutoff   92      429486.876 707917.738  64.8%  12.0  105s
     1282046 511235 537230.798  110   10 429486.876 707917.738  64.8%  12.4  110s
     1333059 522852 499506.055   96   24 429486.876 707915.270  64.8%  12.5  115s
     1404409 565569 infeasible   73      429486.876 707910.055  64.8%  12.2  120s
     1467475 603592 707865.701  173   15 429486.876 707906.160  64.8%  12.0  125s
     1523171 635321 707863.919  178   20 429486.876 707902.982  64.8%  11.9  130s
     1570845 659363 489854.896   90   20 429486.876 707901.310  64.8%  11.8  135s
     1612942 665362     cutoff  111      429486.876 707901.306  64.8%  12.0  140s
     1656630 666297     cutoff   97      429486.876 707901.305  64.8%  12.2  145s
     1697853 659289     cutoff   94      429486.876 707901.305  64.8%  12.5  150s
     1756169 688843 707897.784   58   19 429486.876 707899.066  64.8%  12.4  155s
     1819420 725284 707865.717  138   13 429486.876 707896.434  64.8%  12.3  160s
     1874352 756777 707563.279   61   20 429486.876 707894.267  64.8%  12.1  165s
     1930404 791515 707876.645   75   19 429486.876 707892.226  64.8%  12.0  170s
     1987465 825562 517702.504  106   17 429486.876 707890.391  64.8%  11.8  175s
     2039605 856823 707861.871  173   16 429486.876 707889.225  64.8%  11.7  180s
     2095606 889827 707884.682  133   16 429486.876 707888.050  64.8%  11.6  185s
     2149617 905065     cutoff   88      429486.876 707887.802  64.8%  11.7  190s
     2190424 905882     cutoff   98      429486.876 707887.802  64.8%  11.9  195s
     2227073 900428     cutoff   95      429486.876 707887.802  64.8%  12.1  200s
     2277625 925195 707884.666   77   19 429486.876 707886.896  64.8%  12.0  205s
     2339555 962134 548427.684  260   16 429486.876 707885.730  64.8%  11.9  210s
     2395447 993578 676444.567   63   15 429486.876 707884.897  64.8%  11.8  215s
     2450055 1027326     cutoff   84      429486.876 707883.985  64.8%  11.7  220s
     2507517 1060713 563291.463  220   15 429486.876 707883.115  64.8%  11.7  225s
     2562152 1094278 608572.162   70   13 429486.876 707882.340  64.8%  11.6  230s
     2615232 1125545 707865.718  218   15 429486.876 707881.692  64.8%  11.5  235s
     2663515 1153402 707870.572   90   11 429486.876 707881.230  64.8%  11.5  240s
     2713955 1181641 528604.389   89   12 429486.876 707880.717  64.8%  11.4  245s
     2762458 1208078 614153.392   79   24 429486.876 707880.287  64.8%  11.3  250s
     2812735 1236197 526883.990   76   24 429486.876 707879.779  64.8%  11.3  255s
     2855453 1260517 707865.763   95   18 429486.876 707879.382  64.8%  11.2  260s
     2906286 1290299 707866.815   78   19 429486.876 707878.991  64.8%  11.2  265s
     2958935 1319706 651895.494   74   24 429486.876 707878.574  64.8%  11.1  270s
     3009623 1350010 632100.680  140   16 429486.876 707878.251  64.8%  11.1  275s
     3063745 1381307 infeasible   86      429486.876 707877.859  64.8%  11.1  280s
     3114814 1410512 569234.103   72   20 429486.876 707877.513  64.8%  11.0  285s
     3164746 1438971 475252.814  184   17 429486.876 707877.209  64.8%  11.0  290s
     3218901 1470654     cutoff   97      429486.876 707876.920  64.8%  10.9  295s
     3263018 1496835 infeasible  109      429486.876 707876.718  64.8%  10.9  300s

    Cutting planes:
      RLT: 13

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

    Solution count 2: 429487 361560

    Time limit reached
    Best objective 4.294868755398e+05, best bound 7.078767157931e+05, gap 64.8192%

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: 0x057ae768
    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.00s
    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 488385.774  20.6%   7.6    0s
    H   62    52                    433077.56632 488385.774  12.8%   6.9    0s
    H   65    52                    439182.58928 488385.774  11.2%   6.6    0s

    Cutting planes:
      RLT: 18

    Explored 149 nodes (673 simplex iterations) in 0.09 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.391856726689e+05, gap 0.0007%

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

```


# AxCaliber with time-dependent zeppelin

In this example, we use the AxCaliber method *(Assaf et al. 2008)* presented in a previous example while including the diffusion time-dependency of the extra-axonal space *(Novikov et al. 2014, Burcaw et al. 2015)*.

The signal representation is

\begin{equation}
E_{\textrm{AxCaliber}}^{\textrm{Temporal}}= \underbrace{(1-f_r)\overbrace{E_h(b,\textbf{n},\lambda_\parallel,D_{\infty},A)}^{\textrm{GPA-Zeppelin}}}_{\textrm{Extra-Axonal}} + \underbrace{f_r \overbrace{\Gamma(\alpha,\beta)}^{\textrm{Gamma Distribution}}*_{\mathbb{R}}\overbrace{E_r(\cdot|D_\perp)}^{\textrm{Cylinder}}}_{\textrm{Intra-Axonal}}
\end{equation}

where the cylinder was initially represented using the Callaghan model, but was later replaced with the more general Van Gelderen model *(Huang et al. 2015, De Santis et al. 2016)*. The initial Ball, which accounted for the extra-axonal signal, is replaced by the time-dependent Zeppelin *(De Santis et al, 2016)*. The formulation of the time-dependent Zeppelin is as follows:

\begin{equation}
 E_h(b,\textbf{n},\lambda_\parallel,D_{\infty},A)=\exp(-b\textbf{n}^T(\textbf{R}\textbf{D}^h_{\textrm{diag}}\textbf{R}^T)\textbf{n})\quad\textrm{with}\quad
 \textbf{D}^h_{\textrm{diag}}=
\begin{pmatrix}
  \lambda_\parallel & 0 & 0  \\
  0 & D_{\infty}+A\frac{\ln(\Delta/\delta)+3/2}{\Delta-\delta/3} & 0  \\
  0  & 0  & D_{\infty}+A\frac{\ln(\Delta/\delta)+3/2}{\Delta-\delta/3}
 \end{pmatrix}
\end{equation}

here the perpendicular diffusivity is now dependent on $\Delta$ and $\delta$, with $D_{\infty}$ the bulk diffusion constant and $A$ a characteristic coefficient for extra-axonal hindrence.

Our goal here is to reproduce the Monte Carlo simulations presented in *(De Santis et al, 2016)*, where they investigated the impact of including extra-axonal hindrence through the temporal zeppelin on axonal density and diameter estimation.

# AxCaliber model with time-dependent zeppelin

Using Dmipy, we can construct the time-dependent version of AxCaliber by calling `G3TemporalZeppelin` instead of the previously used `G1Ball`.
As before, the Gamma-distributed Gaussian-Phase cylinders are instantiated by first calling the cylinder model, and then using it as input for the GammaDistributed model.

```python
from dmipy.signal_models import cylinder_models, gaussian_models
from dmipy.distributions import distribute_models

temporal_zeppelin = gaussian_models.G3TemporalZeppelin()
cylinder = cylinder_models.C4CylinderGaussianPhaseApproximation()
gamma_cylinder = distribute_models.DD1GammaDistributed(models=[cylinder])
```

Then put the models together in a MultiCompartmentModel and display the parameters. Note that this model has a lot of parameters.

```python
from dmipy.core import modeling_framework
temporal_AxCaliber = modeling_framework.MultiCompartmentModel(models=[temporal_zeppelin, gamma_cylinder])
temporal_AxCaliber.parameter_cardinality
```

    OrderedDict([('G3TemporalZeppelin_1_mu', 2),
                 ('DD1GammaDistributed_1_DD1Gamma_1_beta', 1),
                 ('G3TemporalZeppelin_1_lambda_inf', 1),
                 ('DD1GammaDistributed_1_C4CylinderGaussianPhaseApproximation_1_lambda_par',
                  1),
                 ('DD1GammaDistributed_1_C4CylinderGaussianPhaseApproximation_1_mu',
                  2),
                 ('G3TemporalZeppelin_1_A', 1),
                 ('DD1GammaDistributed_1_DD1Gamma_1_alpha', 1),
                 ('G3TemporalZeppelin_1_lambda_par', 1),
                 ('partial_volume_0', 1),
                 ('partial_volume_1', 1)])

Visualize the model:

```python
from IPython.display import Image
temporal_AxCaliber.visualize_model_setup(view=False, cleanup=False, with_parameters=True)
Image('Model Setup.png')
```

The data we will fit is measured only along 2 gradients perpendicular to the fiber axis (along x and y axis). This means there is no data that provides information to fit parallel diffusivity "lambda_par" or the axon orientation "mu".

So first, we make sure that the intra- and extra-axonal orientations are aligned and that their parallel diffusivities are equal by using the set_equal_parameter() :

```python
temporal_AxCaliber.set_equal_parameter(
    'G3TemporalZeppelin_1_mu',
    'DD1GammaDistributed_1_C4CylinderGaussianPhaseApproximation_1_mu');
temporal_AxCaliber.set_equal_parameter(
    'G3TemporalZeppelin_1_lambda_par',
    'DD1GammaDistributed_1_C4CylinderGaussianPhaseApproximation_1_lambda_par');
```

Then to not fit these superfluous parameters, we fix the orientation mu along the z-axis and give lambda_par an arbitrary value (it will not affect the results).

```python
temporal_AxCaliber.set_fixed_parameter(
    'G3TemporalZeppelin_1_lambda_par', 1.5e-9)
temporal_AxCaliber.set_fixed_parameter(
    'G3TemporalZeppelin_1_mu', [0, 0])
```

Notice that we are now only fitting the volume fractions, the Gamma distribution parameters $\alpha$ and $\beta$, the bulk diffusivity $\lambda_\infty$ and the coefficient for extra-axonal hindrance $A$.

```python
temporal_AxCaliber.parameter_names
```

    ['DD1GammaDistributed_1_DD1Gamma_1_beta',
     'G3TemporalZeppelin_1_lambda_inf',
     'G3TemporalZeppelin_1_A',
     'DD1GammaDistributed_1_DD1Gamma_1_alpha',
     'partial_volume_0',
     'partial_volume_1']

# Load Monte Carlo simulated data

Using Camino, we previously generated a substrate of Gamma-distributed cylinders and simulated the perpendicular acquisition scheme that was used by *(De Santis et al. 2016)*.

```python
from dmipy.data import saved_data
acquisition_scheme, signal = saved_data.de_santis_camino_data()
```

Unlike De Santis et al who simulated STEAM acquistion scheme, we simulate a PGSE scheme considering their parameters: $\delta = 17$ ms, $\Delta = 48,60,80,100,120,140,160,180,195$ ms, 4 b-values $(500, 1000, 2000$ and $4000$ $s/mm^2$) and 2 unweighted images for each $\Delta$.

```python
acquisition_scheme.print_acquisition_info
```

    Acquisition scheme summary

    total number of measurements: 54
    number of b0 measurements: 18
    number of DWI shells: 36

    shell_index |# of DWIs |bvalue [s/mm^2] |gradient strength [mT/m] |delta [ms] |Delta[ms] |TE[ms]
    0           |2         |0               |0                        |17.0       |48.0      |83.0
    1           |1         |500             |23                       |17.0       |48.0      |83.0
    2           |1         |999             |33                       |17.0       |48.0      |83.0
    3           |1         |2000            |47                       |17.0       |48.0      |83.0
    4           |1         |3999            |67                       |17.0       |48.0      |83.0
    5           |2         |0               |0                        |17.0       |60.0      |95.0
    6           |1         |500             |21                       |17.0       |60.0      |95.0
    7           |1         |1000            |29                       |17.0       |60.0      |95.0
    8           |1         |2000            |42                       |17.0       |60.0      |95.0
    9           |1         |4000            |59                       |17.0       |60.0      |95.0
    10          |2         |0               |0                        |17.0       |80.0      |115.0
    11          |1         |500             |18                       |17.0       |80.0      |115.0
    12          |1         |1000            |25                       |17.0       |80.0      |115.0
    13          |1         |2000            |36                       |17.0       |80.0      |115.0
    14          |1         |4000            |51                       |17.0       |80.0      |115.0
    15          |2         |0               |0                        |17.0       |100.0     |135.0
    16          |1         |499             |16                       |17.0       |100.0     |135.0
    17          |1         |1000            |22                       |17.0       |100.0     |135.0
    18          |1         |1999            |32                       |17.0       |100.0     |135.0
    19          |1         |4000            |45                       |17.0       |100.0     |135.0
    20          |2         |0               |0                        |17.0       |120.0     |155.0
    21          |1         |499             |14                       |17.0       |120.0     |155.0
    22          |1         |1000            |20                       |17.0       |120.0     |155.0
    23          |1         |1999            |29                       |17.0       |120.0     |155.0
    24          |1         |4000            |41                       |17.0       |120.0     |155.0
    25          |2         |0               |0                        |17.0       |140.0     |175.0
    26          |1         |500             |13                       |17.0       |140.0     |175.0
    27          |1         |999             |18                       |17.0       |140.0     |175.0
    28          |1         |2000            |26                       |17.0       |140.0     |175.0
    29          |1         |3999            |37                       |17.0       |140.0     |175.0
    30          |2         |0               |0                        |17.0       |160.0     |195.0
    31          |1         |500             |12                       |17.0       |160.0     |195.0
    32          |1         |1000            |17                       |17.0       |160.0     |195.0
    33          |1         |2000            |25                       |17.0       |160.0     |195.0
    34          |1         |4000            |35                       |17.0       |160.0     |195.0
    35          |2         |0               |0                        |17.0       |180.0     |215.0
    36          |1         |500             |11                       |17.0       |180.0     |215.0
    37          |1         |1000            |16                       |17.0       |180.0     |215.0
    38          |1         |2000            |23                       |17.0       |180.0     |215.0
    39          |1         |4000            |33                       |17.0       |180.0     |215.0
    40          |2         |0               |0                        |17.0       |195.0     |230.0
    41          |1         |500             |11                       |17.0       |195.0     |230.0
    42          |1         |1000            |15                       |17.0       |195.0     |230.0
    43          |1         |2000            |22                       |17.0       |195.0     |230.0
    44          |1         |4000            |31                       |17.0       |195.0     |230.0

To estimate the robustness of the model's parameter estimates, we simulate 20 Rician noise instances on the data at SNR=30.

# Fit time-dependent AxCaliber model to simulated data

```python
temporal_AxCaliber_fit = temporal_AxCaliber.fit(
    acquisition_scheme, signal, solver='mix',
    maxiter=100)
```

    Using parallel processing with 8 workers.
    Setup MIX optimizer in 6.103515625e-05 seconds
    Fitting of 50 voxels complete in 551.341907024 seconds.
    Average of 11.0268381405 seconds per voxel.

```python
A_s = temporal_AxCaliber_fit.fitted_parameters['G3TemporalZeppelin_1_A']
fr_s = temporal_AxCaliber_fit.fitted_parameters['partial_volume_0']
D_inf_s = temporal_AxCaliber_fit.fitted_parameters['G3TemporalZeppelin_1_lambda_inf']
alpha_s = temporal_AxCaliber_fit.fitted_parameters['DD1GammaDistributed_1_DD1Gamma_1_alpha']
beta_s = temporal_AxCaliber_fit.fitted_parameters['DD1GammaDistributed_1_DD1Gamma_1_beta']
```

# Visualize fitted parameters

```python
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
A_kernel = stats.gaussian_kde(A_s)
D_inf_kernel = stats.gaussian_kde(D_inf_s)
AD_s = 2 * alpha_s * beta_s
AD_kernel = stats.gaussian_kde(AD_s)
fr_kernel = stats.gaussian_kde(fr_s)

ind_A = np.linspace(A_s.min(), A_s.max(), 50)
ind_D_inf = np.linspace(D_inf_s.min(), D_inf_s.max(), 50)
ind_AD = np.linspace(AD_s.min(), AD_s.max(), 50)
ind_fr = np.linspace(fr_s.min(), fr_s.max(), 50)
```

We use the gaussian_kde function to show the distribution of the fitted parameters. For each figure, the red vertical line is the ground truth while the green vertical one represents the mean value of the estimated parameter.

```python
plt.title('Axonal density', fontsize=15)
plt.plot(ind_fr, fr_kernel(ind_fr))
plt.axvline(fr_s.mean(), c='g', label='mean $f_r$ = '+str(np.round(fr_s.mean(), 2)))
plt.axvline(0.79, c='r', label='true = 0.79')
plt.ylabel('Probability density', fontsize=15)
plt.xlabel('$f_r$', fontsize=15)
plt.legend(fontsize=15, loc=2)
plt.show()
```

```python
plt.title('Mean Axon Diameter', fontsize=15)
plt.hist(1e6 * AD_s, normed=True, bins=20)
plt.axvline(1e6 * AD_s.mean(), c='g', label='mean diameter = '+str(np.round(1e6 * AD_s.mean(), 2)))
plt.axvline(0.79, c='r', label='true = 1.7')
plt.ylabel('Probability density', fontsize=15)
plt.xlabel('$f_r$', fontsize=15)
plt.legend(fontsize=15, loc=1)
plt.show()
```

Notice that the estimated mean axon diameter either defaults to "Stick-like" behavior or finds axon diameter much too large. The mean is therefore mostly outlier driven.

```python
plt.plot(ind_A*1e12, A_kernel(ind_A)*1e12)
plt.axvline(A_s.mean()*1e12, c='g', label='mean $A$ = '+str(np.round(A_s.mean()*1e12, 2)))
plt.ylabel('Probability density', fontsize=15)
plt.xlabel('A [$mm^2$]', fontsize=15)
plt.title('Characteristic coefficient A', fontsize=15)
plt.legend(fontsize=15)
plt.show()
```

```python
plt.title('Bulk diffusion constant', fontsize=15)
plt.plot(ind_D_inf*1e9, D_inf_kernel(ind_D_inf)*1e9)
plt.axvline(D_inf_s.mean()*1e9, c='g', label='mean $D_{\infty}$ = '+str(np.round(D_inf_s.mean()*1e9, 2)))
plt.ylabel('Probability density', fontsize=15)
plt.xlabel('$D_{\infty}$  [$mm^2/s$]', fontsize=15)
plt.legend(fontsize=15)
plt.show()
```

Except for the axon diameter estimation, our results are comparable to De Santis et al results (of the 3rd simulation presented in table 1). Indeed,
the mean of the estmated axonal density $f_r$ is close to the ground truth, however, with this scheme, the axonal diameter is badly estimated.

As for the characteristic coefficient A, the estimated value is very close to De Santis et al, while its variance is a bit larger. And the mean of the estimated bulk diffision constant $D_{\infty}$ is 3 times bigger. Further study is needed to explain the differences between De Santis et al's findings and ours.

## References
- Assaf, Yaniv, et al. "AxCaliber: a method for measuring axon diameter distribution from diffusion MRI." Magnetic resonance in medicine 59.6 (2008): 1347-1354.
- Novikov, Dmitry S., et al. "Revealing mesoscopic structural universality with diffusion." Proceedings of the National Academy of Sciences 111.14 (2014): 5088-5093.
- Huang, Susie Y., et al. "The impact of gradient strength on in vivo diffusion MRI estimates of axon diameter." NeuroImage 106 (2015): 464-472.
- Burcaw, Lauren M., Els Fieremans, and Dmitry S. Novikov. "Mesoscopic structure of neuronal tracts from time-dependent diffusion." NeuroImage 114 (2015): 18-37.
- De Santis, Silvia, Derek K. Jones, and Alard Roebroeck. "Including diffusion time dependence in the extra-axonal space improves in vivo estimates of axonal diameter and density in human white matter." NeuroImage 130 (2016): 91-103.


## **Viscoelastic wave equation implementation on a staggered grid**

This is a first attempt at implementing the viscoelastic wave equation as described in [1]. See also the FDELMODC implementation by Jan Thorbecke [2].

In the following example, a three dimensional toy problem will be introduced consisting of a single Ricker source located at (100, 50, 35) in a 200 m $\times$ 100 m $\times$ 100 *m* domain.

```python
# Required imports:
import numpy as np
import sympy as sp

from devito import *
from examples.seismic.source import RickerSource, TimeAxis
from examples.seismic import ModelViscoelastic, plot_image
```

The model domain is now constructed. It consists of an upper layer of water, 50 m in depth, and a lower rock layer separated by a 4 m thick sediment layer.

```python
# Domain size:
extent = (200., 100., 100.) # 200 x 100 x 100 m domain
h = 1.0 # Desired grid spacing
shape = (int(extent[0]/h+1), int(extent[1]/h+1), int(extent[2]/h+1))

# Model physical parameters:
vp = np.zeros(shape)
qp = np.zeros(shape)
vs = np.zeros(shape)
qs = np.zeros(shape)
rho = np.zeros(shape)

# Set up three horizontally separated layers:
vp[:,:,:int(0.5*shape[2])+1] = 1.52
qp[:,:,:int(0.5*shape[2])+1] = 10000.
vs[:,:,:int(0.5*shape[2])+1] = 0.
qs[:,:,:int(0.5*shape[2])+1] = 0.
rho[:,:,:int(0.5*shape[2])+1] = 1.05

vp[:,:,int(0.5*shape[2])+1:int(0.5*shape[2])+1+int(4/h)] = 1.6
qp[:,:,int(0.5*shape[2])+1:int(0.5*shape[2])+1+int(4/h)] = 40.
vs[:,:,int(0.5*shape[2])+1:int(0.5*shape[2])+1+int(4/h)] = 0.4
qs[:,:,int(0.5*shape[2])+1:int(0.5*shape[2])+1+int(4/h)] = 30.
rho[:,:,int(0.5*shape[2])+1:int(0.5*shape[2])+1+int(4/h)] = 1.3

vp[:,:,int(0.5*shape[2])+1+int(4/h):] = 2.2
qp[:,:,int(0.5*shape[2])+1+int(4/h):] = 100.
vs[:,:,int(0.5*shape[2])+1+int(4/h):] = 1.2
qs[:,:,int(0.5*shape[2])+1+int(4/h):] = 70.
rho[:,:,int(0.5*shape[2])+1+int(4/h):] = 2.
```

Now create a Devito vsicoelastic model generating an appropriate computational grid along with absorbing boundary layers:

```python
# Create model
origin = (0, 0, 0)
spacing = (h, h, h)
so = 4 # FD space order (Note that the time order is by default 1).
nbl = 20 # Number of absorbing boundary layers cells
model = ModelViscoelastic(space_order=so, vp=vp, qp=qp, vs=vs, qs=qs,
                          b=1/rho, origin=origin, shape=shape, spacing=spacing,
                          nbl=nbl)
```

    Operator `initdamp` run in 0.04 s
    Operator `padfunc` run in 0.01 s
    Operator `padfunc` run in 0.01 s
    Operator `padfunc` run in 0.04 s
    Operator `padfunc` run in 0.01 s
    Operator `padfunc` run in 0.01 s

```python
# As pointed out in Thorbecke's implementation and documentation, the viscoelastic wave euqation is
# not always stable with the standard elastic CFL condition. We enforce a smaller critical dt here
# to ensure the stability.
model.dt_scale = .9
```

The source frequency is now set along with the required model parameters:

```python
# Source freq. in MHz (note that the source is defined below):
f0 = 0.12

# Thorbecke's parameter notation
l = model.lam
mu = model.mu
ro = model.b

k = 1.0/(l + 2*mu)
pi = l + 2*mu

t_s = (sp.sqrt(1.+1./model.qp**2)-1./model.qp)/f0
t_ep = 1./(f0**2*t_s)
t_es = (1.+f0*model.qs*t_s)/(f0*model.qs-f0**2*t_s)
```

```python
# Time step in ms and time range:
t0, tn = 0., 30.
dt = model.critical_dt
time_range = TimeAxis(start=t0, stop=tn, step=dt)
```

Generate Devito time functions for the velocity, stress and memory variables appearing in the viscoelastic model equations. By default, the initial data of each field will be set to zero.

```python
# PDE fn's:
x, y, z = model.grid.dimensions
damp = model.damp

# Staggered grid setup:

# Velocity:
v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=so)

# Stress:
tau = TensorTimeFunction(name='t', grid=model.grid, space_order=so, time_order=1)

# Memory variable:
r = TensorTimeFunction(name='r', grid=model.grid, space_order=so, time_order=1)

s = model.grid.stepping_dim.spacing # Symbolic representation of the model grid spacing
```

And now the source and PDE's are constructed:

```python
# Source
src = RickerSource(name='src', grid=model.grid, f0=f0, time_range=time_range)
src.coordinates.data[:] = np.array([100., 50., 35.])

# The source injection term
src_xx = src.inject(field=tau[0, 0].forward, expr=src*s)
src_yy = src.inject(field=tau[1, 1].forward, expr=src*s)
src_zz = src.inject(field=tau[2, 2].forward, expr=src*s)

# Particle velocity
u_v = Eq(v.forward, model.damp * (v + s*ro*div(tau)))

# Stress equations:
u_t = Eq(tau.forward,  model.damp *  (s*r.forward + tau +
                                      s * (l * t_ep / t_s * diag(div(v.forward)) +
                                           mu * t_es / t_s * (grad(v.forward) + grad(v.forward).T))))

# Memory variable equations:
u_r = Eq(r.forward, damp * (r - s / t_s * (r + l * (t_ep/t_s-1) * diag(div(v.forward)) +
                                           mu * (t_es/t_s-1) * (grad(v.forward) + grad(v.forward).T) )))
```

We now create and then run the operator:

```python
# Create the operator:
op = Operator([u_v, u_r, u_t] + src_xx + src_yy + src_zz,
               subs=model.spacing_map)
```

```python
#NBVAL_IGNORE_OUTPUT

# Execute the operator:
op(dt=dt)
```

    Operator `Kernel` run in 23.92 s

    PerformanceSummary([(PerfKey(name='section0', rank=None),
                         PerfEntry(time=0.014005, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section1', rank=None),
                         PerfEntry(time=23.899532, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section2', rank=None),
                         PerfEntry(time=0.002672999999999997, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])

Before plotting some results, let us first look at the shape of the data stored in one of our time functions:

```python
v[0].data.shape
```

    (2, 241, 141, 141)

Since our functions are first order in time, the time dimension is of length 2. The spatial extent of the data includes the absorbing boundary layers in each dimension (i.e. each spatial dimension is padded by 20 grid points to the left and to the right).

The total number of instances in time considered is obtained from:

```python
time_range.num
```

    150

Hence 223 time steps were executed. Thus the final time step will be stored in index given by:

```python
np.mod(time_range.num,2)
```

    0

Now, let us plot some 2D slices of the fields `vx` and `szz` at the final time step:

```python
#NBVAL_SKIP

# Mid-points:
mid_x = int(0.5*(v[0].data.shape[1]-1))+1
mid_y = int(0.5*(v[0].data.shape[2]-1))+1

# Plot some selected results:

plot_image(v[0].data[1, :, mid_y, :], cmap="seismic")
plot_image(v[0].data[1, mid_x, :, :], cmap="seismic")

plot_image(tau[2, 2].data[1, :, mid_y, :], cmap="seismic")
plot_image(tau[2, 2].data[1, mid_x, :, :], cmap="seismic")
```

```python
#NBVAL_IGNORE_OUTPUT

assert np.isclose(norm(v[0]), 0.102959, atol=1e-4, rtol=0)
```

# References

[1] Johan O. A. Roberston, *et.al.* (1994). "Viscoelatic finite-difference modeling" GEOPHYSICS, 59(9), 1444-1456.

[2] https://janth.home.xs4all.nl/Software/fdelmodcManual.pdf


# Consumer Choice and Intertemporal Choice

We setup and solve a very simple generic one-period consumer choice problem over two goods.  We later specialize to the case of intertemporal trade over two periods and choice over lotteries.  The consumer is assumed to have time-consistent preofereces.  Later, in a separate notebook, we look at a a three period model where consumers have time-inconsistent (quasi-hyperbolic) preferences.

[The code](#codesection) to generate the static and interactive figures is at the end of this notebook (and should be run first to make this interactive).

## Choice over two goods

A consumer chooses a consumption bundle to maximize Cobb-Douglas utility

$$U(c_1,c_2) = c_1^\alpha c_2^{\beta}$$

subject to the budget constraint

$$p_1 c_1 + p_2 c_2 \leq I $$

To plot the budget constraint we rearrange to get:

$$c_2 = \frac{I}{p_2} - \frac{p_1}{p_2} c_1$$

Likewise, to draw the indifference curve defined by $u(c_1,c_2) = \bar u$ we solve for $c_2$ to get:

$$c_2 = \left( \frac{\bar u}{c_1^\alpha}\right)^\frac{1}{\beta}$$

For Cobb-Douglas utility the marginal rate of substitution (MRS) between $c_2$ and $c_1$ is:

$$MRS = \frac{U_1}{U_2} = \frac{\alpha}{\beta} \frac{c_2}{c_1}$$

where $U_1 =\frac{\partial U}{\partial c_1}$ and $U_2 =\frac{\partial U}{\partial c_2}$

```python
consume_plot()
```

## The consumer's optimum

$$L(c_1,c_2) = U(c_1,c_2) + \lambda (I - p_1 c_1  - p_2 c_2) $$

Differentiate with respect to $c_1$ and $c_2$ and $\lambda$ to get:

$$ U_1 = \lambda{p_1}$$

$$ U_2 = \lambda{p_2}$$

$$ I = p_1 c_1  + p_2 c_2$$

Dividing the first equation by the second we get the familiar necessary tangency condition for an interior optimum:

$$MRS = \frac{U_1}{U_2} =\frac{p_1}{p_2}$$

Using our earlier expression for the MRS of a Cobb-Douglas indifference curve, substituting this into the budget constraint and rearranging then allows us to solve for the **Marshallian demand functions**:

$$c_1(p_1,p_2,I)=\frac{\alpha}{\alpha+\beta} \frac{I}{p_1}$$

$$c_1(p_1,p_2,I)=\frac{\beta}{\alpha+\beta} \frac{I}{p_2}$$

Interactive plot with sliders (visible if if running on a notebook server):

```python
interact(consume_plot,p1=(pmin,pmax,0.1),p2=(pmin,pmax,0.1), I=(Imin,Imax,10),alpha=(0.05,0.95,0.05));
```

    interactive(children=(FloatSlider(value=1.0, description='p1', max=4.0, min=1.0), FloatSlider(value=1.0, descr…

## The expenditure function

The indirect utility function:

$$v(p_1,p_2,I) = u(c_1(p_1,p_2,I),c_2(p_1,p_2,I))$$

$$c_1(p_1,p_2,I)=\alpha \frac{I}{p_1}$$

$$c_1(p_1,p_2,I)=(1-\alpha)\frac{I}{p_2}$$

$$v(p_1,p_2,I) = I \cdot  \alpha^\alpha (1-\alpha)^{1-\alpha} \cdot
\left [ \frac{p_2}{p_1} \right ]^\alpha \frac{1}{p_2} $$

$$E(p_z,p_2,\bar u) = \frac{\bar u}{\alpha^\alpha (1-\alpha)^{1-\alpha} \cdot
\left [ \frac{p_2}{p_1} \right ]^\alpha \frac{1}{p_2}}$$

To minimize expenditure needed to achieve level of utility $\bar u$ we solve:

$$\min_{c_1,c_2} p_1 c_1 + p_2 c_2 $$

s.t.

$$u(c_1, c_2) =\bar u$$

The first order conditions are identical to those we got for the utility maximization problem, and hence we have the same tangency

$$\frac{U_1}{U_2} = \frac{\alpha}{1-\alpha} \frac{c_2}{c_1} = \frac{p_1}{p_2}$$

From this we can solve:   $$c_2 = \frac{p_1}{p_2} \frac{1-\alpha}{\alpha} c_1  $$

Now substitute this into the constraint to get:

$$ u \left ( c_1, \frac{p_1}{p_2} \frac{1-\alpha}{\alpha} c_1  \right )=\bar u$$

$$ u \left ( c_1, \frac{p_1}{p_2} \frac{1-\alpha}{\alpha} c_1  \right )=\bar u$$

## Intertemporal Consumption choices

We now look at the special case of intertemporal consumption, or consumption of the same good (say corn) over two periods.  As modeled below the consumer's income is now given by the market value of and endowment bundle $(y_1,y_2)$.

The variables $c_1$ and $c_2$ now refer to consumption of (say corn) in period 1 and period 2.

As is typical of intertemporal maximization problems we will use a time-additive utility function.  The consumer who has access to a competitive financial market (they can borrow or save at interest rate $r$) maximizes:

$$U(c_1,c_2) = u(c_1) + \delta u(c_2)$$

subject to the intertemporal budget constraint:

$$ c_1 + \frac{c_2}{1+r}  = y_1 + \frac{y_2}{1+r} $$

This is just like an ordinary utility maximization problem with prices $p_1 = 1$ and $p_2 =\frac{1}{1+r}$.  Think of it this way, the price of corn is \$1 per unit in each period, but \$1 in period 1 can be placed into savings that will grow to $(1+r)$ dollars in period 2.  That means that from the standpoint of period 1 owning one unit of corn (or one dollar worth of corn) in period 2 is the equivalent of owning $\frac{1}{1+r}$ units of corn today (because placed in savings that amount of period 1 corn would grow to $\frac{1+r}{1+r} = 1$ units of corn in period 2).

The first order necessary condition for an interior optimum is:

$$u'(c_1^*) = \delta u'(c_2^*)$$

Let's adopt the widely used Constant-Relative Risk Aversion (CRRA) felicity function of the form:

$$
\begin{equation}
u\left(c_{t}\right)=\begin{cases}
\frac{c^{1-\rho}}{1-\rho}, & \text{if } \rho>0 \text{ and } \rho \neq 1 \\
ln\left(c\right) & \text{if } \rho=1
\end{cases}
\end{equation}
$$

The first order condition then becomes simply

$${c_1^*}^{-\rho} = \delta (1+r) {c_2^*}^{-\rho}$$

or

$$c_2^* = \left [\delta (1+r) \right]^\frac{1}{\rho}c_1^* $$

From the binding budget constraint we also have

$$c_2^* = E[y](1+r)-c_1^*(1+r)$$

where $E[y] = y_1 + \frac{y_2}{1+r}$

Solving for $c_1^*$ (from the FOC and this binding budget):

$$c_1^*  = \frac{E[y]}{1+\frac{\left [\delta (1+r) \right]^\frac{1}{\rho}}{1+r}}$$

If we simplify to the simple case where $\delta =\frac{1}{1+r}$, where the consumer discounts future consumption at the same rate as the market interest rate then at an optimum the consumer will keep their consumption flat at $c_2^* = c_1^*$.  If we specialize further and assume that $r=0$ then the consumer will set  $c_2^* = c_1^* =\frac{y_1+y_2}{2}$

## Saving and borrowing visualized

Let us visualize the situation.  The consumer has CRRA preferences as described above (summarized by parameters $\delta$ and $\rho$ and starts with an income endowment $(y_1, y_2)$. The market cost of funds is $r$.

### A savings case
In the diagram below the consumer is seen to be saving, i.e.  $s_1^* = y_1 - c_1^* >0$.

```python
consume_plot2(r, delta, rho, y1, y2)
```

Interactive plot with sliders (visible if if running on a notebook server):

```python
interact(consume_plot2, r=(rmin,rmax,0.1), rho=fixed(rho), delta=(0.5,1,0.1), y1=(10,100,1), y2=(10,100,1));
```

    interactive(children=(FloatSlider(value=0.0, description='r', max=1.0), FloatSlider(value=0.7, description='de…

```python
c1e, c2e, uebar = find_opt2(r, rho, delta, y1, y2)
```

In this particular case the consumer consumes:

```python
c1e, c2e
```

    (50.0, 50.0)

Her endowment is

```python
y1,y2
```

    (80, 20)

And she therefore saves

```python
y1-c1e
```

    30.0

in period 1.

### A borrowing case
In the diagram below the consumer is seen to be borrowing, i.e.  $s_1^* = y_1 - c_1^* <0$.

```python
y1,y2 = 20,80
consume_plot2(r, delta, rho, y1, y2)
```

## Code section
Run the code below first to re-generate the figures and interactions above.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, fixed
```

#### Code for simple consumer choice

```python
def U(c1, c2, alpha):
    return (c1**alpha)*(c2**(1-alpha))

def budgetc(c1, p1, p2, I):
    return (I/p2)-(p1/p2)*c1

def indif(c1, ubar, alpha):
    return (ubar/(c1**alpha))**(1/(1-alpha))
```

```python
def find_opt(p1,p2,I,alpha):
    c1 = alpha * I/p1
    c2 = (1-alpha)*I/p2
    u = U(c1,c2,alpha)
    return c1, c2, u
```

Parameters for default plot

```python
alpha = 0.5
p1, p2 = 1, 1
I = 100

pmin, pmax = 1, 4
Imin, Imax = 10, 200
cmax = (3/4)*Imax/pmin
```

```python
def consume_plot(p1=p1, p2=p2, I=I, alpha=alpha):

    c1 = np.linspace(0.1,cmax,num=100)
    c1e, c2e, uebar = find_opt(p1, p2 ,I, alpha)
    idfc = indif(c1, uebar, alpha)
    budg = budgetc(c1, p1, p2, I)

    fig, ax = plt.subplots(figsize=(8,8))
    ax.plot(c1, budg, lw=2.5)
    ax.plot(c1, idfc, lw=2.5)
    ax.vlines(c1e,0,c2e, linestyles="dashed")
    ax.hlines(c2e,0,c1e, linestyles="dashed")
    ax.plot(c1e,c2e,'ob')
    ax.set_xlim(0, cmax)
    ax.set_ylim(0, cmax)
    ax.set_xlabel(r'$c_1$', fontsize=16)
    ax.set_ylabel('$c_2$', fontsize=16)
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)
    ax.grid()
    plt.show()
```

#### Code for intertemporal Choice model

```python
def u(c, rho):
    return (1/rho)* c**(1-rho)

def U2(c1, c2, rho, delta):
    return u(c1, rho) + delta*u(c2, rho)

def budget2(c1, r, y1, y2):
    Ey = y1 + y2/(1+r)
    return Ey*(1+r) - c1*(1+r)

def indif2(c1, ubar, rho, delta):
    return  ( ((1-rho)/delta)*(ubar - u(c1, rho)) )**(1/(1-rho))
```

```python
def find_opt2(r, rho, delta, y1, y2):
    Ey = y1 + y2/(1+r)
    A = (delta*(1+r))**(1/rho)
    c1 = Ey/(1+A/(1+r))
    c2 = c1*A
    u = U2(c1, c2, rho, delta)
    return c1, c2, u
```

Parameters for default plot

```python
rho = 0.5
delta = 1
r = 0
y1, y2 = 80, 20

rmin, rmax = 0, 1
cmax = 150
```

```python
def consume_plot2(r, delta, rho, y1, y2):

    c1 = np.linspace(0.1,cmax,num=100)
    c1e, c2e, uebar = find_opt2(r, rho, delta, y1, y2)
    idfc = indif2(c1, uebar, rho, delta)
    budg = budget2(c1,  r, y1, y2)

    fig, ax = plt.subplots(figsize=(8,8))
    ax.plot(c1, budg, lw=2.5)
    ax.plot(c1, idfc, lw=2.5)
    ax.vlines(c1e,0,c2e, linestyles="dashed")
    ax.hlines(c2e,0,c1e, linestyles="dashed")
    ax.plot(c1e,c2e,'ob')
    ax.vlines(y1,0,y2, linestyles="dashed")
    ax.hlines(y2,0,y1, linestyles="dashed")
    ax.plot(y1,y2,'ob')
    ax.text(y1-6,y2-6,r'$y^*$',fontsize=16)
    ax.set_xlim(0, cmax)
    ax.set_ylim(0, cmax)
    ax.set_xlabel(r'$c_1$', fontsize=16)
    ax.set_ylabel('$c_2$', fontsize=16)
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)
    ax.grid()
    plt.show()
```

### Interactive plot

```python
interact(consume_plot2, r=(rmin,rmax,0.1), rho=fixed(rho), delta=(0.5,1,0.1), y1=(10,100,1), y2=(10,100,1));
```

```python

```


##Задача по численным методам

> Задача : Программирование полинома Лагранжа и полинома Ньютона

---
####Многочлен Лагранжа
>

####Полином Ньютона
>

---

#Содержание работы:

1.   Реализация полинома Лагранжа
2.   Реализация многочлена Ньютона
3.   Проверка конторльных значений
4.   Графическое отображение данных
5.   Расчёт формул

___

Импорт необходимых для визуализации и математических операций библиотек

```python
import pandas as pd
import numpy as np
import seaborn as sns
from math import factorial as f
```

##Задача 1 | Вариант 3

```python
data_table = pd.DataFrame(
    {
      'Xi': [0, 0.2, 0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8],
      'Yi': [1, 1.099, 1.0780, 1.1708, 1.2937, 1.4427, 1.6144, 1.8061 , 2.0161, 2.2429]
    }
)
```

#Реализация интерполяционного многочлена Лагранжа

```python
def Lagrang(a,n=9):
  """
  Решение системы методом Лагранжа

  Input: a - искомое, n - степень
  Output: значение Y
  """
  n+=1
  x = data_table.Xi
  y = data_table.Yi

  summ=0
  for j in range(n):
      p1=1 #numerator
      p2=1 #denominator
      for i in range(n):
          if not i==j:
              p1*=(a-x[i])
              p2*=(x[j]-x[i])
      summ+=y[j]*p1/p2
  return summ
```

```python
Lagrang(0.8,1)
```

    1.3959999999999995

#Реализация полинома Ньютона

###Первая интерполяционная формула

```python

def coefficient(x, y):
    m = len(x)
    a = np.copy(y)
    for k in range(1, m):
        a[k:m] = (a[k:m] - a[k - 1])/(x[k:m] - x[k - 1])

    return a

def Newton_forward(x_estim,n=9):
    """
    Решение системы методом Ньютона первой интерпол-й формулой

    Input: x_estim - искомое, n - степень
    Output: значение Y
    """
    x,y = data_table.Xi, data_table.Yi

    a = coefficient(x, y)
    p = a[n]

    for k in range(1, n+1):
        p = a[n - k] + (x_estim - x[n - k])*p

    return p
```

###Вторая интерполяционнная формула

```python
#@title
deltas = pd.read_excel('/content/Deltas.xlsx')
deltas
```

      y
      delta_1
      delta_2
      delta_3
      delta_4
      delta_5
      delta_6
      delta_7
      delta_8
      delta_9

      0
      1.0000
      0.0990
      -0.1200
      0.2338
      -0.3175
      0.3972
      -0.4763
      0.5555
      -0.6345
      0.7122

      1
      1.0990
      -0.0210
      0.1138
      -0.0837
      0.0797
      -0.0791
      0.0792
      -0.0790
      0.0777
      NaN

      2
      1.0780
      0.0928
      0.0301
      -0.0040
      0.0006
      0.0001
      0.0002
      -0.0013
      NaN
      NaN

      3
      1.1708
      0.1229
      0.0261
      -0.0034
      0.0007
      0.0003
      -0.0011
      NaN
      NaN
      NaN

      4
      1.2937
      0.1490
      0.0227
      -0.0027
      0.0010
      -0.0008
      NaN
      NaN
      NaN
      NaN

      5
      1.4427
      0.1717
      0.0200
      -0.0017
      0.0002
      NaN
      NaN
      NaN
      NaN
      NaN

      6
      1.6144
      0.1917
      0.0183
      -0.0015
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      7
      1.8061
      0.2100
      0.0168
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      8
      2.0161
      0.2268
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      9
      2.2429
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

```python
def Newton_back(x_estim,n=1):
    """
    Решение системы методом Ньютона второй интерпол-й формулой

    Input: x_estim - искомое, n - степень
    Output: значение Y
    """
    x,y = data_table.Xi,data_table.Yi

    h = 0.2

    p = 0
    p = p + y
    for k in range(1, n+1):
      p1 = deltas[f'delta_{k}'][0]
      p2 = f(k) * h**k
      x_sum = sum([x_estim - x[i] for i in range(0,k)])
      p = p + (p1/p2)*x_sum

    return p
```

```python
Newton_back(0.2,1)
```

    1.1782

###Таблица получившихся значений

```python
#@title
data_table['Lagrang'] = [Lagrang(i) for i in data_table.Xi]
data_table['Newton_forward'] = [Newton_forward(i) for i in data_table.Xi]
data_table['Newton_back'] = [Newton_back(i) for i in data_table.Xi]
data_table
```

      Xi
      Yi
      Lagrang
      Newton
      Newton_forward
      Newton_back

      0
      0.0
      1.0000
      1.0000
      1.0000
      1.0000
      1.000

      1
      0.2
      1.0990
      1.0990
      1.0990
      1.0990
      1.099

      2
      0.4
      1.0780
      1.0780
      1.0780
      1.0780
      1.198

      3
      0.6
      1.1708
      1.1708
      1.1708
      1.1708
      1.297

      4
      0.8
      1.2937
      1.2937
      1.2937
      1.2937
      1.396

      5
      1.0
      1.4427
      1.4427
      1.4427
      1.4427
      1.495

      6
      1.2
      1.6144
      1.6144
      1.6144
      1.6144
      1.594

      7
      1.4
      1.8061
      1.8061
      1.8061
      1.8061
      1.693

      8
      1.6
      2.0161
      2.0161
      2.0161
      2.0161
      1.792

      9
      1.8
      2.2429
      2.2429
      2.2429
      2.2429
      1.891

#Проверка контрольных значений

```python
#@title
control_args = pd.DataFrame(
    {
      'Xi': [0.36, 0.84, 1.76],
      'Formula': ['[40-1.33*3]*0.01','[80+1.57*3]*0.01','[180-1.15*3]*0.01'],
      'Newton_forward': [Newton_forward(0.36),Newton_forward(0.84),Newton_forward(1.76)],
      'Newton_back': [Newton_back(0.36),Newton_back(0.84),Newton_back(1.76)],
      'Lagrang': [Lagrang(0.36),Lagrang(0.84),Lagrang(1.76)]
    }
)
control_args
```

      Xi
      Formula
      Newton_forward
      Newton_back
      Lagrang

      0
      0.36
      [40-1.33*3]*0.01
      1.069270
      1.1782
      1.069270

      1
      0.84
      [80+1.57*3]*0.01
      1.321209
      1.4158
      1.321209

      2
      1.76
      [180-1.15*3]*0.01
      2.186328
      1.8712
      2.186328

#Графическое отображение полученных результатов

Графическое отображение данных таблицы Xi и Yi

```python
sns.pointplot(data = data_table, x='Xi', y='Yi')
```

```python
sns.pointplot(data = data_table, x='Xi', y='Newton_forward')
```

```python
sns.pointplot(data = data_table, x='Xi', y='Newton_back')
```

```python
sns.pointplot(data = data_table, x='Xi', y='Lagrang')
```

#Расчёт формул

\begin{equation}
P1(x) = f(x1)+(x-x1) \frac{f(x2)-f(x1)}{x2-x1}
\end{equation}

\begin{equation}
P1(0.36) = 1.099 + (0.36 - 0.2)\frac {1.078 - 1.099}{0.4 - 0.2} = 1.0822
\end{equation}

---

\begin{equation}
P2(x) = \frac{1169}{240}x^{3}-\frac{1769}{400}x^{2}+\frac{1777}{1500}x+1
\end{equation}

\begin{equation}
P2(0.36) = \frac{1169}{240}0.36^{3}-\frac{1769}{400}0.36^{2}+\frac{1777}{1500}0.36+1 = 1,0805776
\end{equation}