# Aim of this notebook

* To construct the singular curve of universal type to finalize the solution of the optimal control problem

# Preamble

```python
from sympy import *
init_printing(use_latex='mathjax')

# Plotting
%matplotlib inline
## Make inline plots raster graphics
from IPython.display import set_matplotlib_formats
## Import modules for plotting and data analysis
import matplotlib.pyplot as plt
from matplotlib import gridspec,rc,colors
import matplotlib.ticker as plticker
## Parameters for seaborn plots
import seaborn as sns
sns.set(style='white',font_scale=1.25,
        rc={"xtick.major.size": 6, "ytick.major.size": 6,
            'text.usetex': False, 'font.family': 'serif', 'font.serif': ['Times']})

import pandas as pd
pd.set_option('mode.chained_assignment',None)

import numpy as np
from scipy.optimize import fsolve, root
from scipy.integrate import ode
backend = 'dopri5'
import warnings

# Timer
import time

from copy import deepcopy

from itertools import cycle
palette_size = 10;
clrs = sns.color_palette("Reds",palette_size)
iclrs = cycle(clrs) # iterated colors

# Suppress warnings
import warnings
warnings.filterwarnings("ignore")
```

# Parameter values

* Birth rate and const of downregulation are defined below in order to fit some experim. data

```python
d = .13 # death rate
α = .3 # low equilibrium point at expression of the main pathway (high equilibrium is at one)
θ = .45 # threshold value for the expression of the main pathway
κ = 40 # robustness parameter
```

* Symbolic variables - the list insludes μ & μbar, because they will be varied later

```python
σ, φ0, φ, x, μ, μbar = symbols('sigma, phi0, phi, x, mu, mubar')
```

* Main functions

```python
A = 1-σ*(1-θ)
Eminus = (α*A-θ)**2/2
ΔE = A*(1-α)*((1+α)*A/2-θ)
ΔEf = lambdify(σ,ΔE)
```

* Birth rate and cost of downregulation

```python
b = (0.1*(exp(κ*(ΔEf(1)))+1)-0.14*(exp(κ*ΔEf(0))+1))/(exp(κ*ΔEf(1))-exp(κ*ΔEf(0))) # birth rate
χ = 1-(0.14*(exp(κ*ΔEf(0))+1)-b*exp(κ*ΔEf(0)))/b
b, χ
```

$$\left ( 0.140168330860362, \quad 0.325961223954473\right )$$

```python
c_relative = 0.05
c = c_relative*(b-d)/b+(1-c_relative)*χ/(exp(κ*ΔEf(0))+1) # cost of resistance
c
```

$$0.00476805892072291$$

* Hamiltonian *H* and a part of it ρ that includes the control variable σ

```python
h = b*(χ/(exp(κ*ΔE)+1)*(1-x)+c*x)
H = -φ0 + φ*(b*(χ/(exp(κ*ΔE)+1)-c)*x*(1-x)+μ*(1-x)/(exp(κ*ΔE)+1)-μbar*exp(-κ*Eminus)*x) + h
ρ = (φ*(b*χ*x+μ)+b*χ)/(exp(κ*ΔE)+1)*(1-x)-φ*μbar*exp(-κ*Eminus)*x
H, ρ
```

$$\left ( \phi \left(\frac{\mu \left(- x + 1\right)}{e^{40 \left(- 0.385 \sigma + 0.7\right) \left(- 0.3575 \sigma + 0.2\right)} + 1} - \bar{\mu} x e^{- 20 \left(- 0.165 \sigma - 0.15\right)^{2}} + x \left(-0.000668330860361588 + \frac{0.045689440686899}{e^{40 \left(- 0.385 \sigma + 0.7\right) \left(- 0.3575 \sigma + 0.2\right)} + 1}\right) \left(- x + 1\right)\right) - \phi_{0} + 0.000668330860361588 x + \frac{0.045689440686899 \left(- x + 1\right)}{e^{40 \left(- 0.385 \sigma + 0.7\right) \left(- 0.3575 \sigma + 0.2\right)} + 1}, \quad - \bar{\mu} \phi x e^{- 20 \left(- 0.165 \sigma - 0.15\right)^{2}} + \frac{\left(- x + 1\right) \left(\phi \left(\mu + 0.045689440686899 x\right) + 0.045689440686899\right)}{e^{40 \left(- 0.385 \sigma + 0.7\right) \left(- 0.3575 \sigma + 0.2\right)} + 1}\right )$$

* Same but for no treatment (σ = 0)

```python
h0 = h.subs(σ,0)
H0 = H.subs(σ,0)
ρ0 = ρ.subs(σ,0)
H0, ρ0
```

$$\left ( \phi \left(0.00368423989943599 \mu \left(- x + 1\right) - 0.637628151621773 \bar{\mu} x - 0.0005 x \left(- x + 1\right)\right) - \phi_{0} + 0.0005 x + 0.000168330860361587, \quad - 0.637628151621773 \bar{\mu} \phi x + 0.00368423989943599 \left(- x + 1\right) \left(\phi \left(\mu + 0.045689440686899 x\right) + 0.045689440686899\right)\right )$$

* Machinery: definition of the Poisson brackets

```python
PoissonBrackets = lambda H1, H2: diff(H1,x)*diff(H2,φ)-diff(H1,φ)*diff(H2,x)
```

* Necessary functions and defining the right hand side of dynamical equations

```python
ρf = lambdify((x,φ,σ,μ,μbar),ρ)
ρ0f = lambdify((x,φ,μ,μbar),ρ0)
dxdτ = lambdify((x,φ,σ,μ,μbar),-diff(H,φ))
dφdτ = lambdify((x,φ,σ,μ,μbar),diff(H,x))
dVdτ = lambdify((x,σ),h)
dρdσ = lambdify((σ,x,φ,μ,μbar),diff(ρ,σ))
dδρdτ = lambdify((x,φ,σ,μ,μbar),-PoissonBrackets(ρ0-ρ,H))
def ode_rhs(t,state,μ,μbar):
    x, φ, V, δρ = state
    σs = [0,1]
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σstar = 1.;
    if ρf(x,φ,σstar,μ,μbar) < ρ0f(x,φ,μ,μbar):
        sgm = 0
    else:
        sgm = σstar
    return [dxdτ(x,φ,sgm,μ,μbar),dφdτ(x,φ,sgm,μ,μbar),dVdτ(x,sgm),dδρdτ(x,φ,σstar,μ,μbar)]
```

```python
def get_primary_field(name, experiment,μ,μbar):
    solutions = {}
    solver = ode(ode_rhs).set_integrator(backend)
    τ0 = experiment['τ0']
    tms = np.linspace(τ0,experiment['T_end'],1e3+1)
    for x0 in experiment['x0']:
        δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
        solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
        sol = []; k = 0;
        while (solver.t < experiment['T_end']) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
            solver.integrate(tms[k])
            sol.append([solver.t]+list(solver.y))
            k += 1
        solutions[x0] = {'solution': sol}
    for x0, entry in solutions.items():
        entry['τ'] = [entry['solution'][j][0] for j in range(len(entry['solution']))]
        entry['x'] = [entry['solution'][j][1] for j in range(len(entry['solution']))]
        entry['φ'] = [entry['solution'][j][2] for j in range(len(entry['solution']))]
        entry['V'] = [entry['solution'][j][3] for j in range(len(entry['solution']))]
        entry['δρ'] = [entry['solution'][j][4] for j in range(len(entry['solution']))]
    return solutions
def get_δρ_value(tme,x0,μ,μbar):
    solver = ode(ode_rhs).set_integrator(backend)
    δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
    solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
    while (solver.t < tme) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tme)
        sol = [solver.t]+list(solver.y)
    return solver.y[3]
def get_δρ_ending(params,μ,μbar):
    tme, x0 = params
    solver = ode(ode_rhs).set_integrator(backend)
    δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
    solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
    δτ = 1.0e-8; tms = [tme,tme+δτ]
    _k = 0; sol = []
    while (_k<len(tms)):# and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append(solver.y)
        _k += 1
    #print(sol)
    return(sol[0][3],(sol[1][3]-sol[0][3])/δτ)
def get_state(tme,x0,μ,μbar):
    solver = ode(ode_rhs).set_integrator(backend)
    δρ0 = ρ0.subs(x,x0).subs(φ,0)-ρ.subs(x,x0).subs(φ,0).subs(σ,1.)
    solver.set_initial_value([x0,0,0,δρ0],0.).set_f_params(μ,μbar)
    δτ = 1.0e-8; tms = [tme,tme+δτ]
    _k = 0; sol = []
    while (solver.t < tms[-1]) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append(solver.y)
        _k += 1
    return(list(sol[0])+[(sol[1][3]-sol[0][3])/δτ])
```

# Machinery for the universal line

* To find the universal singular curve we need to define two parameters

```python
γ0 = PoissonBrackets(PoissonBrackets(H,H0),H)
γ1 = PoissonBrackets(PoissonBrackets(H0,H),H0)
```

* The dynamics

```python
dxdτSingExpr = -(γ0*diff(H0,φ)+γ1*diff(H,φ))/(γ0+γ1)
dφdτSingExpr = (γ0*diff(H0,x)+γ1*diff(H,x))/(γ0+γ1)
dVdτSingExpr = (γ0*h0+γ1*h)/(γ0+γ1)
σSingExpr = γ1*σ/(γ0+γ1)
```

* Machinery for Python: lambdify the functions above

```python
dxdτSing = lambdify((x,φ,σ,μ,μbar),dxdτSingExpr)
dφdτSing = lambdify((x,φ,σ,μ,μbar),dφdτSingExpr)
dVdτSing = lambdify((x,φ,σ,μ,μbar),dVdτSingExpr)
σSing = lambdify((x,φ,σ,μ,μbar),σSingExpr)
```

```python
def ode_rhs_Sing(t,state,μ,μbar):
    x, φ, V = state
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σstar = 1.;
    #print([σstar,σSing(x,φ,σstar,μ,μbar)])
    return [dxdτSing(x,φ,σstar,μ,μbar),dφdτSing(x,φ,σstar,μ,μbar),dVdτSing(x,φ,σstar,μ,μbar)]
# def ode_rhs_Sing(t,state,μ,μbar):
#     x, φ, V = state
#     if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
#         σstar = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
#     else:
#         σstar = 1.;
#     σTrav = fsolve(lambda σ: dxdτ(x,φ,σ,μ,μbar)-dxdτSing(x,φ,σstar,μ,μbar),.6)[0]
#     print([σstar,σTrav])
#     return [dxdτSing(x,φ,σstar,μ,μbar),dφdτSing(x,φ,σstar,μ,μbar),dVdτ(x,σTrav)]
def get_universal_curve(end_point,tmax,Nsteps,μ,μbar):
    tms =  np.linspace(end_point[0],tmax,Nsteps);
    solver = ode(ode_rhs_Sing).set_integrator(backend)
    solver.set_initial_value(end_point[1:4],end_point[0]).set_f_params(μ,μbar)
    _k = 0; sol = []
    while (solver.t < tms[-1]):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    return sol
def get_σ_universal(tme,end_point,μ,μbar):
    δτ = 1.0e-8; tms = [tme,tme+δτ]
    solver = ode(ode_rhs_Sing).set_integrator(backend)
    solver.set_initial_value(end_point[1:4],end_point[0]).set_f_params(μ,μbar)
    _k = 0; sol = []
    while (solver.t < tme+δτ):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    x, φ = sol[0][:2]
    sgm = fsolve(lambda σ: dxdτ(x,φ,σ,μ,μbar)-(sol[1][0]-sol[0][0])/δτ,θ/2)[0]
    return sgm
def get_state_universal(tme,end_point,μ,μbar):
    solver = ode(ode_rhs_Sing).set_integrator(backend)
    solver.set_initial_value(end_point[1:4],end_point[0]).set_f_params(μ,μbar)
    solver.integrate(tme)
    return [solver.t]+list(solver.y)
```

```python
def ode_rhs_with_σstar(t,state,μ,μbar):
    x, φ, V = state
    if (dρdσ(1.,x,φ,μ,μbar)<0) and (dρdσ(θ,x,φ,μ,μbar)>0):
        σ = fsolve(dρdσ,.8,args=(x,φ,μ,μbar,))[0]
    else:
        σ = 1.;
    return [dxdτ(x,φ,σ,μ,μbar),dφdτ(x,φ,σ,μ,μbar),dVdτ(x,σ)]
def ode_rhs_with_given_σ(t,state,σ,μ,μbar):
    x, φ, V = state
    return [dxdτ(x,φ,σ,μ,μbar),dφdτ(x,φ,σ,μ,μbar),dVdτ(x,σ)]
def get_trajectory_with_σstar(starting_point,tmax,Nsteps,μ,μbar):
    tms = np.linspace(starting_point[0],tmax,Nsteps)
    solver = ode(ode_rhs_with_σstar).set_integrator(backend)
    solver.set_initial_value(starting_point[1:],starting_point[0]).set_f_params(μ,μbar)
    sol = []; _k = 0;
    while solver.t < max(tms) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    return sol
def get_trajectory_with_given_σ(starting_point,tmax,Nsteps,σ,μ,μbar):
    tms = np.linspace(starting_point[0],tmax,100)
    solver = ode(ode_rhs_with_given_σ).set_integrator(backend)
    solver.set_initial_value(starting_point[1:],starting_point[0]).set_f_params(σ,μ,μbar)
    sol = []; _k = 0;
    while solver.t < max(tms) and (solver.y[0]<=1.) and (solver.y[0]>=0.):
        solver.integrate(tms[_k])
        sol.append([solver.t]+list(solver.y))
        _k += 1
    return sol
def get_state_with_σstar(tme,starting_point,μ,μbar):
    solver = ode(ode_rhs_with_σstar).set_integrator(backend)
    solver.set_initial_value(starting_point[1:4],starting_point[0]).set_f_params(μ,μbar)
    solver.integrate(tme)
    return [solver.t]+list(solver.y)
def get_finalizing_point_from_universal_curve(tme,tmx,end_point,μ,μbar):
    unv_point = get_state_universal(tme,end_point,μ,μbar)
    return get_state_with_σstar(tmx,unv_point,μ,μbar)[1]
```

# Field of optimal trajectories as the solution of the Bellman equation

* μ & μbar are varied by *T* and *T*bar ($\mu=1/T$ and $\bar\mu=1/\bar{T}$)

```python
tmx = 180.
end_switching_curve = {'t': 24., 'x': .9/.8}
# for Τ, Τbar in zip([28]*5,[14,21,28,35,60]):
for Τ, Τbar in zip([28],[60]):
    μ = 1./Τ; μbar = 1./Τbar
    print("Parameters: μ = %.5f, μbar = %.5f"%(μ,μbar))
    end_switching_curve['t'], end_switching_curve['x'] = fsolve(get_δρ_ending,(end_switching_curve['t'],.8*end_switching_curve['x']),args=(μ,μbar),xtol=1.0e-12)
    end_point = [end_switching_curve['t']]+get_state(end_switching_curve['t'],end_switching_curve['x'],μ,μbar)
    print("Ending point for the switching line: τ = %.1f days, x = %.1f%%" % (end_point[0], end_point[1]*100))
    print("Checking the solution - should give zero values: ")
    print(get_δρ_ending([end_switching_curve['t'],end_switching_curve['x']],μ,μbar))
    print("* Constructing the primary field")
    experiments = {
        'sol1': { 'T_end': tmx, 'τ0': 0., 'x0': list(np.linspace(0,end_switching_curve['x']-(1e-3),10))+list(np.linspace(end_switching_curve['x']+(1e-6),1.,10)) } }
    primary_field = []
    for name, values in experiments.items():
        primary_field.append(get_primary_field(name,values,μ,μbar))
    print("* Constructing the switching curve")
    switching_curve = []
    x0s = np.linspace(end_switching_curve['x'],1,21); _y = end_switching_curve['t']

    for x0 in x0s:
        tme = fsolve(get_δρ_value,_y,args=(x0,μ,μbar))[0]
        if (tme>0):
            switching_curve = switching_curve+[[tme,get_state(tme,x0,μ,μbar)[0]]]
            _y = tme
    print("* Constructing the universal curve")
    universal_curve = get_universal_curve(end_point,tmx,25,μ,μbar)
    print("* Finding the last characteristic")
    #time0 = time.time()
    tuniv = fsolve(get_finalizing_point_from_universal_curve,tmx-40.,args=(tmx,end_point,μ,μbar,))[0]
    #print("The proccess to find the last characteristic took %0.1f minutes" % ((time.time()-time0)/60.))
    univ_point = get_state_universal(tuniv,end_point,μ,μbar)
    print("The last point on the universal line:")
    print(univ_point)
    last_trajectory = get_trajectory_with_σstar(univ_point,tmx,50,μ,μbar)
    print("Final state:")
    final_state = get_state_with_σstar(tmx,univ_point,μ,μbar)
    print(final_state)
    print("Fold-change in tumor size: %.2f"%(exp((b-d)*tmx-final_state[-1])))
    # Plotting
    plt.rcParams['figure.figsize'] = (6.75, 4)
    _k = 0
    for solutions in primary_field:
        for x0, entry in solutions.items():
            plt.plot(entry['τ'], entry['x'], 'k-', linewidth=.9, color=clrs[_k%palette_size])
            _k += 1
    plt.plot([x[0] for x in switching_curve],[x[1] for x in switching_curve],linewidth=2,color="red")
    plt.plot([end_point[0]],[end_point[1]],marker='o',color="red")
    plt.plot([x[0] for x in universal_curve],[x[1] for x in universal_curve],linewidth=2,color="red")
    plt.plot([x[0] for x in last_trajectory],[x[1] for x in last_trajectory],linewidth=.9,color="black")
    plt.xlim([0,tmx]); plt.ylim([0,1]);
    plt.xlabel("time, days"); plt.ylabel("fraction of resistant cells")
    plt.show()
    print()
```

```python
import csv
from numpy.linalg import norm
File = open("../figures/draft/sensitivity_mu-low_cost.csv", 'w')
File.write("T,Tbar,mu,mubar,sw_start_x,sw_end_t,sw_end_x,univ_point_t,univ_point_x,outcome,err_sw_t,err_sw_x\n")
writer = csv.writer(File,lineterminator='\n')

end_switching_curve0 = {'t': 40.36, 'x': .92}
end_switching_curve_prev_t = end_switching_curve0['t']
tuniv = tmx-30.
Ts = np.arange(90,2,-2) #Τbars;
Τbars = np.arange(90,2,-2) #np.arange(120,1,-1) #need to change here if more
for Τ in Ts:
    μ = 1./Τ
    end_switching_curve = deepcopy(end_switching_curve0)
    for Τbar in Τbars:
        μbar = 1./Τbar
        print("* Parameters: T = %.1f, Tbar = %.1f (μ = %.5f, μbar = %.5f)"%(Τ,Τbar,μ,μbar))
        success = False; err = 1.
        while (not success)|(norm(err)>1e-6):
            end_switching_curve = {'t': 2*end_switching_curve['t']-end_switching_curve_prev_t-.001,
                                   'x': end_switching_curve['x']-0.002}
            sol = root(get_δρ_ending,(end_switching_curve['t'],end_switching_curve['x']),args=(μ,μbar))
            end_switching_curve_prev_t = end_switching_curve['t']
            end_switching_curve_prev_x = end_switching_curve['x']
            end_switching_curve['t'], end_switching_curve['x'] = sol.x
            success = sol.success
            err = get_δρ_ending([end_switching_curve['t'],end_switching_curve['x']],μ,μbar)
            if (not success):
                print("! Trying again...", sol.message)
            elif (norm(err)>1e-6):
                print("! Trying again... Convergence is not sufficient")
        else:
            end_point = [end_switching_curve['t']]+get_state(end_switching_curve['t'],end_switching_curve['x'],μ,μbar)
            print("Ending point: t = %.2f, x = %.2f%%"%(end_switching_curve['t'],100*end_switching_curve['x'])," Checking the solution:",err)
            universal_curve = get_universal_curve(end_point,tmx,25,μ,μbar)
            tuniv = root(get_finalizing_point_from_universal_curve,tuniv,args=(tmx,end_point,μ,μbar)).x
            err_tuniv = get_finalizing_point_from_universal_curve(tuniv,tmx,end_point,μ,μbar)
            univ_point = get_state_universal(tuniv,end_point,μ,μbar)
            print("tuniv = %.2f"%tuniv,"xuniv = %.2f%%"%(100*univ_point[1])," Checking the solution: ",err_tuniv)
            final_state = get_state_with_σstar(tmx,univ_point,μ,μbar)
            outcome = exp((b-d)*tmx-final_state[-1])
            print("Fold-change in tumor size: %.2f"%(outcome))
            output = [Τ,Τbar,μ,μbar,end_switching_curve['x'],end_point[0],end_point[1]]+list(univ_point[0:2])+[outcome]+list(err)+[err_tuniv]
            writer.writerow(output)
        if (Τbar==Τ):
            end_switching_curve0 = deepcopy(end_switching_curve)

File.close()
```

    * Parameters: T = 90.0, Tbar = 90.0 (μ = 0.01111, μbar = 0.01111)
    Ending point: t = 45.07, x = 89.94%  Checking the solution: (1.500588033692019e-08, 3.476372657289835e-07)
    tuniv = 132.46 xuniv = 59.61%  Checking the solution:  5.967226712755291e-12
    Fold-change in tumor size: 0.75
    * Parameters: T = 90.0, Tbar = 88.0 (μ = 0.01111, μbar = 0.01136)
    Ending point: t = 44.84, x = 89.75%  Checking the solution: (-4.4182813374515356e-08, -2.122730270429693e-07)
    tuniv = 133.19 xuniv = 58.86%  Checking the solution:  2.0560081415155196e-11
    Fold-change in tumor size: 0.75
    * Parameters: T = 90.0, Tbar = 86.0 (μ = 0.01111, μbar = 0.01163)
    Ending point: t = 44.61, x = 89.56%  Checking the solution: (-3.208999700922589e-08, -4.837822060798036e-08)
    tuniv = 133.26 xuniv = 58.68%  Checking the solution:  -2.2218338280310945e-13
    Fold-change in tumor size: 0.74
    * Parameters: T = 90.0, Tbar = 84.0 (μ = 0.01111, μbar = 0.01190)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 44.42, x = 89.35%  Checking the solution: (-1.7456313536702116e-08, -4.85957360218573e-10)
    tuniv = 133.43 xuniv = 58.40%  Checking the solution:  -7.042168931326032e-12
    Fold-change in tumor size: 0.73
    * Parameters: T = 90.0, Tbar = 82.0 (μ = 0.01111, μbar = 0.01220)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 44.26, x = 89.14%  Checking the solution: (-1.4734552235557343e-08, -2.8232416783752662e-08)
    tuniv = 133.68 xuniv = 58.05%  Checking the solution:  4.1918031545851164e-13
    Fold-change in tumor size: 0.73
    * Parameters: T = 90.0, Tbar = 80.0 (μ = 0.01111, μbar = 0.01250)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    Ending point: t = 44.07, x = 88.92%  Checking the solution: (-8.029672270788626e-14, 4.387915344337429e-14)
    tuniv = 133.88 xuniv = 57.74%  Checking the solution:  2.0053206057496498e-12
    Fold-change in tumor size: 0.72
    * Parameters: T = 90.0, Tbar = 78.0 (μ = 0.01111, μbar = 0.01282)
    Ending point: t = 43.88, x = 88.69%  Checking the solution: (-1.1360138903789702e-13, -9.26835580552298e-15)
    tuniv = 134.12 xuniv = 57.40%  Checking the solution:  2.2367316332427833e-12
    Fold-change in tumor size: 0.71
    * Parameters: T = 90.0, Tbar = 76.0 (μ = 0.01111, μbar = 0.01316)
    Ending point: t = 43.70, x = 88.45%  Checking the solution: (5.704471065030555e-13, 1.1283833066796716e-13)
    tuniv = 134.35 xuniv = 57.04%  Checking the solution:  1.786303049922111e-11
    Fold-change in tumor size: 0.71
    * Parameters: T = 90.0, Tbar = 74.0 (μ = 0.01111, μbar = 0.01351)
    Ending point: t = 43.58, x = 88.20%  Checking the solution: (-4.291315846561832e-08, -2.4588433002014685e-07)
    tuniv = 134.78 xuniv = 56.52%  Checking the solution:  -3.242885127097139e-11
    Fold-change in tumor size: 0.70
    * Parameters: T = 90.0, Tbar = 72.0 (μ = 0.01111, μbar = 0.01389)
    Ending point: t = 43.29, x = 87.93%  Checking the solution: (-1.2070606510000817e-08, 4.586701921684145e-08)
    tuniv = 134.80 xuniv = 56.34%  Checking the solution:  1.7019989584365902e-11
    Fold-change in tumor size: 0.69
    * Parameters: T = 90.0, Tbar = 70.0 (μ = 0.01111, μbar = 0.01429)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 43.15, x = 87.66%  Checking the solution: (-2.6160683786572956e-08, 1.560834341846963e-07)
    tuniv = 134.97 xuniv = 56.03%  Checking the solution:  2.7964866533558563e-11
    Fold-change in tumor size: 0.68
    * Parameters: T = 90.0, Tbar = 68.0 (μ = 0.01111, μbar = 0.01471)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    ! Trying again... Convergence is not sufficient
    Ending point: t = 42.90, x = 87.37%  Checking the solution: (-5.159488584102032e-09, -2.8221341993403435e-09)
    tuniv = 135.35 xuniv = 55.54%  Checking the solution:  1.2086567857672037e-11
    Fold-change in tumor size: 0.67
    * Parameters: T = 90.0, Tbar = 66.0 (μ = 0.01111, μbar = 0.01515)
    Ending point: t = 42.69, x = 87.06%  Checking the solution: (1.0631762312841636e-13, 8.074820615520624e-15)
    tuniv = 135.61 xuniv = 55.14%  Checking the solution:  2.126285258974292e-13
    Fold-change in tumor size: 0.66
    * Parameters: T = 90.0, Tbar = 64.0 (μ = 0.01111, μbar = 0.01562)
    Ending point: t = 42.47, x = 86.74%  Checking the solution: (-4.949341669009379e-13, -3.4686110504017124e-14)
    tuniv = 135.88 xuniv = 54.72%  Checking the solution:  3.627508016190717e-12
    Fold-change in tumor size: 0.65
    * Parameters: T = 90.0, Tbar = 62.0 (μ = 0.01111, μbar = 0.01613)
    Ending point: t = 42.25, x = 86.41%  Checking the solution: (-8.210271646805657e-09, -2.653205257062396e-08)
    tuniv = 136.17 xuniv = 54.28%  Checking the solution:  -4.2805495693160056e-11
    Fold-change in tumor size: 0.64
    * Parameters: T = 90.0, Tbar = 60.0 (μ = 0.01111, μbar = 0.01667)
    Ending point: t = 42.18, x = 86.05%  Checking the solution: (-3.6315179201646606e-07, -6.884988621264846e-07)
    tuniv = 136.88 xuniv = 53.46%  Checking the solution:  -6.426512100254911e-12
    Fold-change in tumor size: 0.63
    * Parameters: T = 90.0, Tbar = 58.0 (μ = 0.01111, μbar = 0.01724)
    Ending point: t = 41.60, x = 85.68%  Checking the solution: (-2.7422823533809055e-07, 7.685335971202687e-07)
    tuniv = 136.22 xuniv = 53.82%  Checking the solution:  3.228244060959895e-12
    Fold-change in tumor size: 0.62
    * Parameters: T = 90.0, Tbar = 56.0 (μ = 0.01111, μbar = 0.01786)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 41.54, x = 85.28%  Checking the solution: (-1.9238840843054784e-07, -3.8565084510199194e-08)
    tuniv = 137.04 xuniv = 52.90%  Checking the solution:  6.580153089075225e-13
    Fold-change in tumor size: 0.61
    * Parameters: T = 90.0, Tbar = 54.0 (μ = 0.01111, μbar = 0.01852)
    Ending point: t = 41.30, x = 84.86%  Checking the solution: (-2.5181731908319653e-07, -1.2109103604608673e-07)
    tuniv = 137.39 xuniv = 52.37%  Checking the solution:  -8.087754424512816e-12
    Fold-change in tumor size: 0.60
    * Parameters: T = 90.0, Tbar = 52.0 (μ = 0.01111, μbar = 0.01923)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 41.02, x = 84.42%  Checking the solution: (-2.3217573557971024e-07, -5.3501486154558194e-09)
    tuniv = 137.63 xuniv = 51.92%  Checking the solution:  -3.159347783388e-13
    Fold-change in tumor size: 0.59
    * Parameters: T = 90.0, Tbar = 50.0 (μ = 0.01111, μbar = 0.02000)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 40.76, x = 83.94%  Checking the solution: (-1.9683236342948334e-07, -9.333056881518742e-08)
    tuniv = 138.00 xuniv = 51.36%  Checking the solution:  -4.535136155503494e-12
    Fold-change in tumor size: 0.57
    * Parameters: T = 90.0, Tbar = 48.0 (μ = 0.01111, μbar = 0.02083)
    Ending point: t = 40.57, x = 83.44%  Checking the solution: (-1.6986952957187867e-07, -5.937813112708818e-07)
    tuniv = 138.60 xuniv = 50.58%  Checking the solution:  1.6946999359390702e-12
    Fold-change in tumor size: 0.56
    * Parameters: T = 90.0, Tbar = 46.0 (μ = 0.01111, μbar = 0.02174)
    Ending point: t = 40.18, x = 82.90%  Checking the solution: (-2.052556857542744e-08, -3.832735644174615e-08)
    tuniv = 138.64 xuniv = 50.27%  Checking the solution:  5.440217720753537e-12
    Fold-change in tumor size: 0.55
    * Parameters: T = 90.0, Tbar = 44.0 (μ = 0.01111, μbar = 0.02273)
    Ending point: t = 39.87, x = 82.32%  Checking the solution: (-4.675436557168061e-08, 2.344675673238222e-08)
    tuniv = 138.96 xuniv = 49.70%  Checking the solution:  -3.5191710656690134e-12
    Fold-change in tumor size: 0.53
    * Parameters: T = 90.0, Tbar = 42.0 (μ = 0.01111, μbar = 0.02381)
    Ending point: t = 39.57, x = 81.70%  Checking the solution: (3.16021351882273e-13, 3.547443119729024e-14)
    tuniv = 139.33 xuniv = 49.08%  Checking the solution:  5.929548518857075e-12
    Fold-change in tumor size: 0.51
    * Parameters: T = 90.0, Tbar = 40.0 (μ = 0.01111, μbar = 0.02500)
    Ending point: t = 39.25, x = 81.03%  Checking the solution: (2.190340044556326e-12, 2.7199495881430006e-13)
    tuniv = 139.70 xuniv = 48.43%  Checking the solution:  3.744206333866629e-12
    Fold-change in tumor size: 0.50
    * Parameters: T = 90.0, Tbar = 38.0 (μ = 0.01111, μbar = 0.02632)
    Ending point: t = 38.91, x = 80.31%  Checking the solution: (-1.5346066526789813e-12, -2.300380801677517e-13)
    tuniv = 140.09 xuniv = 47.75%  Checking the solution:  1.6792997548087385e-11
    Fold-change in tumor size: 0.48
    * Parameters: T = 90.0, Tbar = 36.0 (μ = 0.01111, μbar = 0.02778)
    Ending point: t = 38.65, x = 79.53%  Checking the solution: (-6.913985615860486e-08, -6.348607297051996e-07)
    tuniv = 140.77 xuniv = 46.81%  Checking the solution:  1.9024965630665136e-11
    Fold-change in tumor size: 0.46
    * Parameters: T = 90.0, Tbar = 34.0 (μ = 0.01111, μbar = 0.02941)
    Ending point: t = 38.25, x = 78.68%  Checking the solution: (-2.852119772249931e-07, -3.7745928476030216e-07)
    tuniv = 141.06 xuniv = 46.16%  Checking the solution:  -3.151593569450384e-11
    Fold-change in tumor size: 0.44
    * Parameters: T = 90.0, Tbar = 32.0 (μ = 0.01111, μbar = 0.03125)
    Ending point: t = 37.83, x = 77.75%  Checking the solution: (-1.2062591330166447e-07, -1.00640664320317e-07)
    tuniv = 141.37 xuniv = 45.47%  Checking the solution:  -2.4147350785597155e-14
    Fold-change in tumor size: 0.42
    * Parameters: T = 90.0, Tbar = 30.0 (μ = 0.01111, μbar = 0.03333)
    Ending point: t = 37.53, x = 76.73%  Checking the solution: (-2.8665355121959573e-07, -9.429755962722306e-07)
    tuniv = 142.15 xuniv = 44.36%  Checking the solution:  1.964924056796491e-11
    Fold-change in tumor size: 0.40
    * Parameters: T = 90.0, Tbar = 28.0 (μ = 0.01111, μbar = 0.03571)
    Ending point: t = 37.04, x = 75.60%  Checking the solution: (-3.180299506094764e-08, -2.2730274921880306e-07)
    tuniv = 142.33 xuniv = 43.70%  Checking the solution:  -8.42874381401515e-12
    Fold-change in tumor size: 0.38
    * Parameters: T = 90.0, Tbar = 26.0 (μ = 0.01111, μbar = 0.03846)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    Ending point: t = 36.58, x = 74.35%  Checking the solution: (2.4854697835453854e-14, 2.1627748210006694e-15)
    tuniv = 142.73 xuniv = 42.82%  Checking the solution:  5.454664497861472e-12
    Fold-change in tumor size: 0.36
    * Parameters: T = 90.0, Tbar = 24.0 (μ = 0.01111, μbar = 0.04167)
    Ending point: t = 36.13, x = 72.95%  Checking the solution: (-1.5706732450575765e-12, -1.1535429460899775e-13)
    tuniv = 143.24 xuniv = 41.81%  Checking the solution:  8.141577689801949e-13
    Fold-change in tumor size: 0.33
    * Parameters: T = 90.0, Tbar = 22.0 (μ = 0.01111, μbar = 0.04545)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 35.66, x = 71.38%  Checking the solution: (-1.8512894913559898e-07, -1.2054321219349409e-07)
    tuniv = 143.80 xuniv = 40.68%  Checking the solution:  -7.840277038706489e-12
    Fold-change in tumor size: 0.31
    * Parameters: T = 90.0, Tbar = 20.0 (μ = 0.01111, μbar = 0.05000)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 35.16, x = 69.59%  Checking the solution: (-1.7801499899729428e-07, -9.54414172713925e-08)
    tuniv = 144.34 xuniv = 39.51%  Checking the solution:  2.7110674816199776e-12
    Fold-change in tumor size: 0.28
    * Parameters: T = 90.0, Tbar = 18.0 (μ = 0.01111, μbar = 0.05556)
    Ending point: t = 34.64, x = 67.55%  Checking the solution: (2.079499309429406e-12, -2.0424540928974056e-12)
    tuniv = 144.88 xuniv = 38.24%  Checking the solution:  1.6504573532513667e-11
    Fold-change in tumor size: 0.25
    * Parameters: T = 90.0, Tbar = 16.0 (μ = 0.01111, μbar = 0.06250)
    Ending point: t = 34.11, x = 65.17%  Checking the solution: (-5.3865412264115804e-08, -1.6911522136666092e-07)
    tuniv = 145.52 xuniv = 36.79%  Checking the solution:  -1.8587342602571777e-11
    Fold-change in tumor size: 0.22
    * Parameters: T = 90.0, Tbar = 14.0 (μ = 0.01111, μbar = 0.07143)
    Ending point: t = 33.56, x = 62.37%  Checking the solution: (-8.825600847828154e-08, -4.486002029246736e-08)
    tuniv = 146.08 xuniv = 35.23%  Checking the solution:  -1.9994908506681952e-12
    Fold-change in tumor size: 0.19
    * Parameters: T = 90.0, Tbar = 12.0 (μ = 0.01111, μbar = 0.08333)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    Ending point: t = 33.04, x = 59.02%  Checking the solution: (-8.141105174838086e-09, -9.549380956790263e-09)
    tuniv = 146.65 xuniv = 33.46%  Checking the solution:  2.1415022533055605e-12
    Fold-change in tumor size: 0.16
    * Parameters: T = 90.0, Tbar = 10.0 (μ = 0.01111, μbar = 0.10000)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    ! Trying again... Convergence is not sufficient
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last five Jacobian evaluations.
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last five Jacobian evaluations.
    Ending point: t = 32.62, x = 54.90%  Checking the solution: (-8.000396304670048e-08, -8.077356279074904e-07)
    tuniv = 147.35 xuniv = 31.32%  Checking the solution:  1.088650003477909e-12
    Fold-change in tumor size: 0.13
    * Parameters: T = 90.0, Tbar = 8.0 (μ = 0.01111, μbar = 0.12500)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    ! Trying again... Convergence is not sufficient
    ! Trying again... Convergence is not sufficient
    Ending point: t = 32.37, x = 49.70%  Checking the solution: (-6.181892025487087e-07, 2.1994305265542234e-07)
    tuniv = 147.35 xuniv = 29.06%  Checking the solution:  7.816948477401553e-12
    Fold-change in tumor size: 0.09
    * Parameters: T = 90.0, Tbar = 6.0 (μ = 0.01111, μbar = 0.16667)
    Ending point: t = 33.03, x = 42.83%  Checking the solution: (-4.392043748412426e-11, -3.379696485473721e-12)
    tuniv = 146.81 xuniv = 26.12%  Checking the solution:  2.8822083608659455e-12
    Fold-change in tumor size: 0.06
    * Parameters: T = 90.0, Tbar = 4.0 (μ = 0.01111, μbar = 0.25000)
    Ending point: t = 39.07, x = 33.13%  Checking the solution: (1.3181056644332069e-11, 1.1043843454560326e-12)
    tuniv = 140.82 xuniv = 22.37%  Checking the solution:  3.2190292098555062e-12
    Fold-change in tumor size: 0.04
    * Parameters: T = 88.0, Tbar = 90.0 (μ = 0.01136, μbar = 0.01111)
    Ending point: t = 44.65, x = 90.00%  Checking the solution: (-3.5862911667972276e-08, -1.7209235027035767e-07)
    tuniv = 133.29 xuniv = 59.47%  Checking the solution:  6.832201471240751e-12
    Fold-change in tumor size: 0.77
    * Parameters: T = 88.0, Tbar = 88.0 (μ = 0.01136, μbar = 0.01136)
    Ending point: t = 45.13, x = 89.81%  Checking the solution: (3.391989841021889e-07, 8.982145107514915e-07)
    tuniv = 132.50 xuniv = 60.02%  Checking the solution:  1.4414159536096927e-11
    Fold-change in tumor size: 0.76
    * Parameters: T = 88.0, Tbar = 86.0 (μ = 0.01136, μbar = 0.01163)
    Ending point: t = 44.30, x = 89.62%  Checking the solution: (-3.291758030172231e-08, -1.1589169569118912e-07)
    tuniv = 133.65 xuniv = 58.91%  Checking the solution:  1.9604838585873807e-12
    Fold-change in tumor size: 0.75
    * Parameters: T = 88.0, Tbar = 84.0 (μ = 0.01136, μbar = 0.01190)
    Ending point: t = 44.70, x = 89.41%  Checking the solution: (2.958585994336961e-07, 8.534979315181747e-07)
    tuniv = 133.01 xuniv = 59.33%  Checking the solution:  -5.468681063547365e-13
    Fold-change in tumor size: 0.75
    * Parameters: T = 88.0, Tbar = 82.0 (μ = 0.01136, μbar = 0.01220)
    Ending point: t = 43.91, x = 89.20%  Checking the solution: (-1.9854571570555537e-08, 5.497660726709136e-10)
    tuniv = 133.99 xuniv = 58.35%  Checking the solution:  5.178969232633168e-13
    Fold-change in tumor size: 0.74
    * Parameters: T = 88.0, Tbar = 80.0 (μ = 0.01136, μbar = 0.01250)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 43.73, x = 88.98%  Checking the solution: (8.845180261108721e-14, 1.3685186751677977e-14)
    tuniv = 134.22 xuniv = 58.02%  Checking the solution:  -2.7149463233122617e-12
    Fold-change in tumor size: 0.73
    * Parameters: T = 88.0, Tbar = 78.0 (μ = 0.01136, μbar = 0.01282)
    Ending point: t = 43.55, x = 88.75%  Checking the solution: (-6.1587206620877626e-12, -2.102131377463342e-12)
    tuniv = 134.45 xuniv = 57.68%  Checking the solution:  1.9423074260060957e-12
    Fold-change in tumor size: 0.73
    * Parameters: T = 88.0, Tbar = 76.0 (μ = 0.01136, μbar = 0.01316)
    ! Trying again... Convergence is not sufficient
    Ending point: t = 43.36, x = 88.51%  Checking the solution: (1.3957021980206055e-13, 1.1081655463635516e-14)
    tuniv = 134.68 xuniv = 57.33%  Checking the solution:  -1.2531264220738692e-11
    Fold-change in tumor size: 0.72
    * Parameters: T = 88.0, Tbar = 74.0 (μ = 0.01136, μbar = 0.01351)
    Ending point: t = 43.18, x = 88.26%  Checking the solution: (-3.0954356230088475e-08, -9.57077747225905e-10)
    tuniv = 134.92 xuniv = 56.97%  Checking the solution:  -1.6111209588665076e-12
    Fold-change in tumor size: 0.71
    * Parameters: T = 88.0, Tbar = 72.0 (μ = 0.01136, μbar = 0.01389)
    ! Trying again... The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
    Ending point: t = 42.99, x = 88.00%  Checking the solution: (-2.152531973503716e-08, -4.2778175766388595e-08)
    tuniv = 135.20 xuniv = 56.57%  Checking the solution:  5.475820491485095e-11
    Fold-change in tumor size: 0.70
    * Parameters: T = 88.0, Tbar = 70.0 (μ = 0.01136, μbar = 0.01429)

* Here I investigate the dependence of $\mathrm{FoldChange}(T,\bar T)$. I fix $T$ at 15,30,45,60 days, and then I vary $\bar T$ between zero and $4T$. The example below is just a simulation for only one given value of $T$.

```python
import csv
from numpy.linalg import norm
File = open("../results/sensitivity1.csv", 'w')
File.write("T,Tbar,mu,mubar,sw_end_t,sw_end_x,univ_point_t,univ_point_x,outcome,err_sw_t,err_sw_x\n")
writer = csv.writer(File,lineterminator='\n')

end_switching_curve = {'t': 23.36, 'x': .9592}
end_switching_curve_prev_t = end_switching_curve['t']
tuniv = tmx-30.
Τ = 15
Τbars_step = .5; Tbars = np.arange(Τ*4,0,-Τbars_step)
for Τbar in Tbars:
    μ = 1./Τ; μbar = 1./Τbar
    print("* Parameters: T = %.1f, Tbar = %.1f (μ = %.5f, μbar = %.5f)"%(Τ,Τbar,μ,μbar))
    success = False; err = 1.
    while (not success)|(norm(err)>1e-6):
        end_switching_curve = {'t': 2*end_switching_curve['t']-end_switching_curve_prev_t-.001,
                               'x': end_switching_curve['x']-0.002}
        sol = root(get_δρ_ending,(end_switching_curve['t'],end_switching_curve['x']),args=(μ,μbar))
        end_switching_curve_prev_t = end_switching_curve['t']
        end_switching_curve_prev_x = end_switching_curve['x']
        end_switching_curve['t'], end_switching_curve['x'] = sol.x
        success = sol.success
        err = get_δρ_ending([end_switching_curve['t'],end_switching_curve['x']],μ,μbar)
        if (not success):
            print("! Trying again...", sol.message)
        elif (norm(err)>1e-6):
            print("! Trying again... Convergence is not sufficient")
    else:
        end_point = [end_switching_curve['t']]+get_state(end_switching_curve['t'],end_switching_curve['x'],μ,μbar)
        print("Ending point: t = %.2f, x = %.2f%%"%(end_switching_curve['t'],100*end_switching_curve['x'])," Checking the solution:",err)
        universal_curve = get_universal_curve(end_point,tmx,25,μ,μbar)
        tuniv = root(get_finalizing_point_from_universal_curve,tuniv,args=(tmx,end_point,μ,μbar)).x
        err_tuniv = get_finalizing_point_from_universal_curve(tuniv,tmx,end_point,μ,μbar)
        univ_point = get_state_universal(tuniv,end_point,μ,μbar)
        print("tuniv = %.2f"%tuniv," Checking the solution: ",err_tuniv)
        final_state = get_state_with_σstar(tmx,univ_point,μ,μbar)
        outcome = exp((b-d)*tmx-final_state[-1])
        print("Fold-change in tumor size: %.2f"%(outcome))
        output = [Τ,Τbar,μ,μbar,end_switching_curve['t'],end_switching_curve['x']]+list(univ_point[0:2])+[outcome]+list(err)+[err_tuniv]
        writer.writerow(output)

File.close()
```

* The results are aggregated in a file **sensitivity1_agg.csv**.

```python
df = pd.DataFrame.from_csv("../figures/draft/sensitivity1_agg.csv").reset_index().drop(['err_sw_t','err_sw_x','err_tuniv'],1)
df['Tratio'] = df['Tbar']/df['T']
df.head()
```

```python

```

scripts/.ipynb_checkpoints/B2. (lower cost of resistance) Solution of the control problem including the universal line and Sensitivity analysis [Python]-checkpoint.ipynb


# Classical High Order Methods

### Table of contents
 1. Chapter 2: Classical Methods
    1. [Section 1.1: Runge Kutta](#RK)
    1. [Section 1.2: Multistep methods](#multistep)

```python
# If you do not have numpy, matplotlib, scipy or nodepy, run this cell
!pip install numpy
# This is the basic package in python with all the numerical functions

!pip install scipy
# This package has some functions to deal with polynomials

!pip install matplotlib
# This package allows to  plot

!pip install nodepy
# This package has some interesting features for RK methods
```

```python
# We need a couple of packages in this chapter
import numpy as np
# This is the basic package in python with all the numerical functions

import matplotlib.pyplot as plt
# This package allows to  plot

from nodepy import rk
#This package already implemented some functions for Runge Kutta and multistep methods
```

* How to obtain generalizations of the Euler methods?
* How can we obtain higher order of accuracy $\Delta t^p$?

## Runge Kutta Methods

The basic idea of Runge--Kutta methods is to introduce further stages inside the computation of one step, in order to achieve higher order of accuracy.

### $\theta$ method

For example consider the following method:

$$
y^{*}=y^n +\theta \Delta t F(t^n,y^n)\\
y^{n+1} = y^n +\Delta t \left( \frac{2\theta-1}{2\theta}F(t^n,y^n) + \frac{1}{2\theta} F(t^n+\theta\Delta t,y^*) \right).
$$

We introduced 1 extra stage. Do we get an extra order?

#### Taylor expansion for local truncation
Suppose that $y^n=y(t^n)$, define $t^*=t^n+\theta \Delta t$, and suppose that $F$ does not depends explicitly on time,

$$
\begin{align}
y(t^{n+1})&=y^n + \Delta t y'(t^n) + \frac{\Delta t^2}{2} y''(t^n) + \mathcal{O}(\Delta t^3)\\
y(t^{*})&=y^n + \theta\Delta t y'(t^n) + \frac{\theta^2\Delta t^2}{2} y''(t^n) + \mathcal{O}(\Delta t^3)\\
y^{*} &= y^n + \theta \Delta t F(y^n) = y(t^*)+ \mathcal{O}(\Delta t^2)\\
y^{n+1} &= y^n +\Delta t \left( \frac{2\theta-1}{2\theta}F(t^n,y^n) + \frac{1}{2\theta} F(y^*) \right)\\
&= y^n +\Delta t \left( \frac{2\theta-1}{2\theta}y'(t^n) + \frac{1}{2\theta} F(y^n+\theta \Delta t y'(t^n)) \right) + \mathcal{O}(\Delta t^3)\\
&= y^n +\Delta t \left( \frac{2\theta-1}{2\theta}y'(t^n) + \frac{1}{2\theta} F(y^n)+\frac{1}{2\theta} \frac{dF}{dy}(y^n)\theta \Delta t y'(t^n)  \right) + \mathcal{O}(\Delta t^3)\\
&= y^n +\Delta t \left( \frac{2\theta-1}{2\theta}y'(t^n) + \frac{1}{2\theta} y'(t^n)+\frac{1}{2\theta} \frac{dF}{dt}(y^n)\theta \Delta t   \right) + \mathcal{O}(\Delta t^3)\\
&= y^n +\Delta t \left( y'(t^n)+\frac{1}{2\theta} \frac{d}{dt}(y'(t^n))\theta \Delta t   \right) + \mathcal{O}(\Delta t^3)\\
&= y^n +\Delta t \left( y'(t^n)+\frac{1}{2} y''(t^n) \Delta t   \right) + \mathcal{O}(\Delta t^3)=y(t^{n+1})+\mathcal{O}(\Delta t^3).
\end{align}
$$

So, second order in the **global** error.

### General formulation
A general Runge--Kutta method with $S$ stages can be written using a Butcher tableau
$$
\begin{array}
{c|c}
c&A\\
\hline
& b^T
\end{array}
$$
where $A\in \mathbb R^{S\times S},\, b,c\in\mathbb R^S$, in the following formulation

$$
\begin{cases}
y^{(k)}=y^n + \Delta t \sum_{j=1}^S a_{kj} F(t^n+c_j\Delta t,y^{(j)}), \quad k=1,\dots, S,\\
y^{n+1} = y^n+ \Delta t \sum_{j=1}^S b_{j} F(t^n+c_j\Delta t,y^{(j)})
\end{cases}
$$

To be an **explicit** scheme $A$ must be a strictly low-triangular matrix, i.e., $a_{ij}=0$ if $j\geq i$.

#### Example $\theta$ scheme
The $\theta$ scheme we have seen before can be written as

$$
\begin{array}
{c|cc}
0\\
\theta & \theta\\
\hline
& \frac{2\theta -1}{2\theta} &\frac{1}{2\theta}
\end{array}
$$

We see that the scheme is explicit, because there is nothing on the top right of $A$.

### Order of explicit RK schemes
How do we know that a RK scheme has a certain order?
For $\theta $ method we have written the Taylor expansion up to the order we were expecting and we matched the coefficients of the exact solution with the one of the approximated solutions at all the steps.

If one wants to generalize this conditions using the Taylor expansion, it takes long, but you can get to similar results

Where the first condition is for order 1 RK methods,

the first 2 conditions are for order 2 RK methods,

the first 4 conditions are for order 3 RK methods,

all the 8 conditions are for order 4 RK methods.

* How can the conditions be computed without doing the Taylor expansions?

#### Trees
A very smart way of computing all the conditions is given by combinatorial arguments that can be represented as trees.
You can find the proof in [Butcher](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119121534)

A tree is an object with nodes and branches as in figure, with **leaves** on top (nodes without further branches outward going) and a **root** at the bottom (the node with only branches outgoing).

The **order** of a tree is the number of nodes in that tree.

A RK scheme is of order $p$ if all the trees of order less or equal to $p$ verify certain conditions.

All the conditions that must verified are of the type $\Phi(a_{ij},b_i,c_j)=\frac{1}{\gamma}$ polynomial in the coefficients of the Butcher tableau equal to the inverse of an integer.
For every tree is very easy to compute such polynomial $\Phi$ and the integer $\gamma$.

Here is an example:

For order 4 we have all these trees and conditions

Reminder: an explicit RK method with $S$ stages has $\frac{S^2+3S-2}{2}$ coefficients.

| Stages/Order  | Trees | Conditions | Explicit RK coefficient |
|---|---|---|---|
| 1  | 1  | 1  | 1  |
| 2  | 1  | 2  | 4  |
| 3  | 2  | 4  | 8  |
| 4  | 4  | 8  | 13  |
| 5  | 9  | 17  | 19  |
| 6  | 20 | 37  | 26  |
| 7  | 48 | 85     | 34  |
| 8  | 115 | 200   | 43  |
| 9  | 286 | 486   | 53  |
| 10 | 719 | 1205  | 64  |

Moreover, the solution of these conditions should be admissible. For example, there is no 5 stages 5th order explicit RK method, one needs 6 stages. As well, some of the relations may be redundant. Building higher order explicit Runge Kutta method is a challenge.

An extract of Butcher

#### Theorem 324C [Butcher](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119121534)
For any positive integer $p$, an explicit RK method exists with order $p$ and $S$ stages, where
\begin{equation}
S=\begin{cases}
\frac{3p^2-10p+24}{8}, \quad & p \text{ even,}\\
\frac{3p^2-4p+9}{8}, \quad & p \text{ odd.}
\end{cases}
\end{equation}

| Order  | Stages  |
|---|---|
| 1  | 1  |
| 2  | 2  |
| 3  | 3  |
| 4  | 4  |
| 5  | 8  |
| 6  | 9  |
| 7  | 16 |
| 8  | 17 |
| 9  | 27 |
| 10 | 28 |

Examples of explicit RK4
$$
\begin{array}
{c|cccc}
0\\
\frac{1}{2} & \frac{1}{2}\\
\frac{1}{2} &0 &\frac{1}{2} \\
1& 0& 0& 1\\
\hline
& \frac{1}{6} &\frac{1}{3} &\frac{1}{3} &\frac{1}{6}
\end{array} \qquad  \begin{array}
{c|cccc}
0\\
\frac{1}{4} & \frac{1}{4}\\
\frac{1}{2} &0 &\frac{1}{2} \\
1& 1& -2& 2\\
\hline
& \frac{1}{6} &0 &\frac{2}{3} &\frac{1}{6}
\end{array}
$$

An explicit RK5
$$
\begin{array}
{c|cccccc}
0\\
\frac{1}{4} & \frac{1}{4}\\
\frac{1}{4} &\frac{1}{8} &\frac{1}{8} \\
\frac{1}{2}& 0& 0& \frac{1}{2}\\
\frac{3}{4}& \frac{3}{16}& -\frac{3}{8}& \frac{3}{8}&\frac{9}{16}\\
1& -\frac{3}{7}& \frac{8}{7}& \frac{6}{7} & -\frac{12}{7} & \frac{8}{7}\\
\hline
& \frac{7}{90}&0 &\frac{32}{90} &\frac{12}{90} &\frac{32}{90}&\frac{7}{90}
\end{array}
$$

### Implicit RK schemes
The implicit RK schemes are defined as usual RK schemes and have at least a nonzero element in the upper triangular part of $A$.
They have the advantage of requiring **less stages** (with respect to explicit RK) to obtain the order of accuracy required.

For example

$$
\begin{array}
{c|cc}
\frac{1}{2}-\frac{\sqrt{3}}{6} & \frac{1}{4} &\frac{1}{4}-\frac{\sqrt{3}}{6}\\
\frac{1}{2}+\frac{\sqrt{3}}{6}  &\frac{1}{4}+\frac{\sqrt{3}}{6} & \frac{1}{4}\\
\hline
& \frac{1}{2} &\frac{1}{2}
\end{array}
$$

has order 4.

There are clever ways of building implicit RK methods using well known high order accurate quadrature rules.

Take a quadrature rule of order $p$ with $S$ quadrature points $c_i \in [0,1]$, provided with $S$ interpolation polynomials $\varphi_i$, such that $\varphi_i(c_j)=\delta_{i,j}$ and

$$
y(t) = \sum_{i=1}^S \varphi_i(t) y(t^n+c_i\Delta t) + \mathcal{O}(\Delta t^{p+1}), \qquad \forall t \in [t^n,t^{n+1}].
$$

We can then define the $a_{ij}$ approximating

$$
y^{(i)}=y^n+\int_{t^n}^{t^n+c_i\Delta t} F(t,y(t))dt\approx y^n+\sum_{j} \int_{t^n}^{t^n+c_i\Delta t} \varphi_j(t) dt F(t,y^{(j)})
$$

so that

$$
a_{ij}:=\int_{t^n}^{t^n+c_i\Delta t} \varphi_j(t) dt
$$

and similarly

$$
b_{j}:=\int_{t^n}^{t^{n+1}} \varphi_j(t) dt.
$$

This guarantees a high order quadrature rule also in the final step, keeping the right accuracy order in every step of the RK method.

Using Gauss--Legendre polynomials one gets $p=2s$, examples

$$
\begin{array}
{c|c}
\frac{1}{2} & \frac{1}{2}\\
\hline
s=1, p=2 & 1
\end{array}
$$

$$
\begin{array}
{c|cc}
\frac{1}{2}-\frac{\sqrt{3}}{6} & \frac{1}{4} &\frac{1}{4}-\frac{\sqrt{3}}{6}\\
\frac{1}{2}+\frac{\sqrt{3}}{6}  &\frac{1}{4}+\frac{\sqrt{3}}{6} & \frac{1}{4}\\
\hline
s=2, p=4 & \frac{1}{2} &\frac{1}{2}
\end{array}
$$

$$
\begin{array}
{c|ccc}
\frac{1}{2}-\frac{\sqrt{15}}{10} & \frac{5}{36} &\frac{2}{9}-\frac{\sqrt{15}}{15} &\frac{5}{36} -\frac{\sqrt{15}}{30} \\
\frac{1}{2} & \frac{5}{36}+\frac{\sqrt{15}}{24}  &\frac{2}{9}&\frac{5}{36} -\frac{\sqrt{15}}{24} \\
\frac{1}{2}+\frac{\sqrt{15}}{10}+\frac{\sqrt{15}}{30} & \frac{5}{36} &\frac{2}{9}+\frac{\sqrt{15}}{15} &\frac{5}{36}  \\
\hline
s=3, p=6 & \frac{5}{18} &\frac{4}{9} &\frac{5}{18}
\end{array}
$$

Using Gauss--Lobatto or Radau points one decrease the order $p=2s-1$, but increases stability.

### Stability
As we have seen before for Euler, the stability can be evaluated on Dahlquist's equation

$$ y'=qy$$

with Re$(q)\leq 0$. Considering $z=\Delta t q$ we want to write the RK methods on this problem as

$$
y^{n+1}=R(z)y^n
$$

and find the stability region $\mathcal S=\lbrace z\in \mathbb C : |R(z)|\leq 1\rbrace$.

It is actually very simple to write the stability function $R$ for these problems.
Let $Y\in \mathbb R^S$ be the solution of the RK method at all the stages, namely, it solves

$$
Y=\mathbf{1}y^n + \Delta t q A Y
$$

or

$$
(I-zA)Y=\mathbf{1}y^n \quad \Leftrightarrow Y=(I-zA)^{-1}\mathbf{1} y^n
$$

with $\mathbf 1$ the vector of dimension $S$ with all entries equal to 1.

Substituting this into the final update, we have

$$
y^{n+1} = y^n + z b^{T}Y=y^n + z b^T(I-zA)^{-1}\mathbf{1} y^n = (1+ z b^T(I-zA)^{-1}\mathbf{1}) y^n$$

which gives a way of computing the stability region very efficiently with

$$
R(z) =1+ z b^T(I-zA)^{-1}\mathbf{1}.
$$

```python
A=np.array([[0,0],[1/2,0]])
b=np.array([0,1])
rk2 = rk.ExplicitRungeKuttaMethod(A,b)

A=np.array([[0]])
b=np.array([1])
rk1 = rk.ExplicitRungeKuttaMethod(A,b)

A=np.array([[0,0,0],[2/3,0,0],[1/3,1/3,0]])
b=np.array([1/4,0,3/4])
rk3 = rk.ExplicitRungeKuttaMethod(A,b)

rk44=rk.loadRKM('RK44')

rk1.plot_stability_region(bounds=[-5,1,-5,5])
rk2.plot_stability_region(bounds=[-5,1,-5,5])
rk3.plot_stability_region(bounds=[-5,1,-5,5])
rk44.plot_stability_region(bounds=[-5,1,-5,5])

print(rk44.A)
print(rk44.b)
```

### Stability for implicit method
Implicit methods are able to deal with stiff problems, hence, other types of stability are more interesting in this situation.

##### A-stability
A methods is A-stable if $|R(z)|\leq 1$ whenever Re$(z)\leq 0$.

##### L-stability
A method is L-stable if it is A-stable and $R(\infty)=0$.

This gives better stability conditions in case of stiff problems.

Radau and Lobatto implicit RK methods are L-stable, while Gauss--Legendre are only A-stable.

One can write down the conditions for RK scheme to have these stabilities. [see Butcher](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119121534)

```python
for method in ["LobattoIIIC2","LobattoIIIC3","LobattoIIIC4", "GL2","GL3","SDIRK23"]:
    imRK=rk.loadRKM(method)
    print(imRK)
    imRK.plot_stability_region()
    plt.show()
```

```python
import numpy as np

## Linear scalar Dahlquist's equation
def linear_scalar_flux(u,t=0,k_coef=10):
    ff=np.zeros(np.shape(u))
    ff[0]= -k_coef*u[0]
    return ff

def linear_scalar_exact_solution(u0,t,k_coef=10):
    return np.array([np.exp(-k_coef*u0[0]*t)])

def linear_scalar_jacobian(u,t=0,k_coef=10):
    Jf=np.zeros((len(u),len(u)))
    Jf[0,0]=-k_coef
    return Jf

#nonlinear problem y'=-ky|y| +1
def nonlinear_scalar_flux(u,t=0,k_coef=10):
    ff=np.zeros(np.shape(u))
    ff[0]=-k_coef*abs(u[0])*u[0] +1
    return ff

def nonlinear_scalar_exact_solution(u0,t,k_coef = 10):
        sqrtk = np.sqrt(k_coef)
        ustar = 1 / sqrtk
        if u0[0] >= ustar:
            uex=np.array([1./np.tanh(sqrtk * t + np.arctanh(1/sqrtk /u0[0])) / sqrtk])
        elif u0[0] < 0 and t < - np.atan(sqrtk * u0[0]) / sqrtk:
            uex=np.array([np.tan(sqrtk * t + np.arctan(sqrtk * u0[0])) / sqrtk])
        else:
            uex=np.array([np.tanh(sqrtk * t + np.arctanh(sqrtk * u0[0])) / sqrtk])
        return uex

def nonlinear_scalar_jacobian(u,t=0,k_coef=10):
    Jf=np.zeros((len(u),len(u)))
    Jf[0,0]=-k_coef*abs(u[0])
    return Jf

# SYSTEMS

# linear systems
def linear_system2_flux(u,t=0):
    d=np.zeros(len(u))
    d[0]= -5*u[0] + u[1]
    d[1]= 5*u[0] -u[1]
    return d

def linear_system2_exact_solution(u0,t):
    A=np.array([[-5,1],[5,-1]])
    u_e=u0+(1-np.exp(-6*t))/6*np.dot(A,u0)
    return u_e

def linear_system2_jacobian(u,t=0):
    Jf=np.array([[-5,1],[5,-1]])
    return Jf

linear_system2_matrix = np.array([[-5,1],[5,-1]])

def linear_system2_production_destruction(u,t=0):
    p=np.zeros((len(u),len(u)))
    d=np.zeros((len(u),len(u)))
    p[0,1]=u[1]
    d[1,0]=u[1]
    p[1,0]=5*u[0]
    d[0,1]=5*u[0]
    return p,d

#lin system 3 x3

def linear_system3_flux(u,t=0):
    d=np.zeros(len(u))
    d[0]= -u[0] + 3*u[1]
    d[1]= -3*u[1] + 5*u[2]
    d[2]= -5*u[2]
    return d

def linear_system3_exact_solution(u0,t=0):
    u_e = np.zeros(len(u0))
    u_e[0] = 15.0/8.0*u0[2]*(np.exp(-5*t) - 2*np.exp(-3*t)+np.exp(-t))
    u_e[1] = 5.0/2.0*u0[2]*(-np.exp(-5*t) + np.exp(-3*t))
    u_e[2] = u0[2]*np.exp(-5*t)
    return u_e
def linear_system3_jacobian(u,t=0):
    Jf=np.zeros((len(u),len(u)))
    Jf[0,0]=-1.
    Jf[0,1]=3
    Jf[1,1] = -3
    Jf[1,2] = 5
    Jf[2,2] = -5
    return Jf

## Nonlinear 3x3 system production destruction
def nonlinear_system3_flux(u,t=0):
    ff=np.zeros(len(u))
    ff[0]= -u[0]*u[1]/(u[0]+1)
    ff[1]= u[0]*u[1]/(u[0]+1) -0.3*u[1]
    ff[2]= 0.3*u[1]
    return ff

def nonlinear_system3_production_destruction(u,t=0):
    p=np.zeros((len(u),len(u)))
    d=np.zeros((len(u),len(u)))
    p[1,0]=u[0]*u[1]/(u[0]+1)
    d[0,1]=p[1,0]
    p[2,1]=0.3*u[1]
    d[1,2]=p[2,1]
    return p,d

# SIR Model
def SIR_flux(u,t=0,beta=3,gamma=1):
    ff=np.zeros(len(u))
    N=np.sum(u)
    ff[0]=-beta*u[0]*u[1]/N
    ff[1]=+beta*u[0]*u[1]/N - gamma*u[1]
    ff[2]= gamma*u[1]
    return ff

def SIR_jacobian(u,t=0,beta=3,gamma=1):
    Jf=np.zeros((len(u),len(u)))
    N=np.sum(u)
    Jf[0,0]=-beta*u[1]/N
    Jf[0,1]=-beta*u[0]/N
    Jf[1,0]= beta*u[1]/N
    Jf[1,1]= beta*u[0]/N - gamma
    Jf[2,1] = gamma
    return Jf

def SIR_production_destruction(u,t=0,beta=3,gamma=1):
    p=np.zeros((len(u),len(u)))
    d=np.zeros((len(u),len(u)))
    N=np.sum(u)
    p[1,0]=beta*u[0]*u[1]/N
    d[0,1]=p[1,0]
    p[2,1]=gamma*u[1]
    d[1,2]=p[2,1]
    return p,d

# Nonlinear_oscillator
def nonLinearOscillator_flux(u,t=0,alpha=0.):
    ff=np.zeros(np.shape(u))
    n=np.sqrt(np.dot(u,u))
    ff[0]=-u[1]/n-alpha*u[0]/n
    ff[1]=u[0]/n - alpha*u[1]/n
    return ff

def nonLinearOscillator_exact_solution(u0,t):
    u_ex=np.zeros(np.shape(u0))
    n=np.sqrt(np.dot(u0,u0))
    u_ex[0]=np.cos(t/n)*u0[0]-np.sin(t/n)*u0[1]
    u_ex[1]=np.sin(t/n)*u0[0]+np.cos(t/n)*u0[1]
    return u_ex

# Non linear oscillator damped
def nonLinearOscillatorDamped_flux(u,t,alpha=0.01):
    ff=np.zeros(np.shape(u))
    n=np.sqrt(np.dot(u,u))
    ff[0]=-u[1]/n-alpha*u[0]/n
    ff[1]=u[0]/n - alpha*u[1]/n
    return ff

def nonLinearOscillatorDamped_exact_solution(u0,t,alpha=0.01):
    u_ex=np.zeros(np.shape(u0))
    n0=np.sqrt(np.dot(u0,u0))
    n=n0*np.exp(-alpha*t)
    u_ex[0]=n/n0*(np.cos(t/n)*u0[0]-np.sin(t/n)*u0[1])
    u_ex[1]=n/n0*(np.sin(t/n)*u0[0]+np.cos(t/n)*u0[1])
    return u_ex

# pendulum
def pendulum_flux(u,t=0):
    ff=np.zeros(np.shape(u))
    ff[0]=u[1]
    ff[1]=-np.sin(u[0])
    return ff

def pendulum_jacobian(u,t=0):
    Jf=np.zeros((2,2))
    Jf[0,1]=1.
    Jf[1,0]=np.cos(u[0])
    return Jf

def pendulum_entropy(u,t=0):
    return np.array(0.5*u[1]**2.-np.cos(u[0]), dtype=np.float)

def pendulum_entropy_variables(u,t=0):
    v=np.zeros(np.shape(u))
    v[0]=np.sin(u[0])
    v[1]=u[1]
    return v

# Robertson
def Robertson_flux(u,t=0,alpha=10**4,beta=0.04, gamma=3*10**7):
    ff=np.zeros(np.shape(u))
    ff[0] = alpha*u[1]*u[2]-beta*u[0]
    ff[1] = beta*u[0]-alpha*u[1]*u[2] - gamma*u[1]**2
    ff[2] = gamma*u[1]**2
    return ff

def Robertson_jacobian(u,t=0,alpha=10**4,beta=0.04, gamma=3*10**7):
    Jf=np.zeros((3,3))
    Jf[0,0]= -beta
    Jf[0,1]= alpha*u[2]
    Jf[0,2]= alpha*u[1]
    Jf[1,0]= beta
    Jf[1,1]= -alpha*u[2]-2*gamma*u[1]
    Jf[1,2]= -alpha*u[1]
    Jf[2,1] = 2*gamma*u[1]
    return Jf

def Robertson_production_destruction(u,t=0,alpha=10**4,beta=0.04, gamma=3*10**7):
    p=np.zeros((len(u),len(u)))
    d=np.zeros((len(u),len(u)))
    p[0,1]=alpha*u[1]*u[2]
    d[1,0]=p[0,1]
    p[1,0]=beta*u[0]
    d[0,1]=p[1,0]
    p[2,1]=gamma*u[1]**2
    d[1,2]=p[2,1]
    return p,d

# Lotka:
def lotka_flux(u,t=0,alpha=1,beta=0.2,delta=0.5,gamma=0.2):
    ff=np.zeros(np.shape(u))
    ff[0]=alpha*u[0]-beta*u[0]*u[1]
    ff[1]=delta*beta*u[0]*u[1]-gamma*u[1]
    return ff

def lotka_jacobian(u,t=0,alpha=1,beta=0.2,delta=0.5,gamma=0.2):
    Jf=np.zeros((2,2))
    Jf[0,0] = alpha -beta*u[1]
    Jf[0,1] = -beta*u[0]
    Jf[1,0] = delta*beta*u[1]
    Jf[1,1] = delta*beta*u[0] -gamma
    return Jf

#3 bodies problem in 2D: U=(x_1,x_2,v_1,v_2,y_1,y_2,w_1,w_2,z_1,z_2,s_1,s_2)
# where x is the 2D position of body1 and v is speed body1 sun
# y, w are position and velocity body2 earth
# z, s are position and velocity body3 mars

def threeBodies_flux(u,t=0):
    m1=1.98892*10**30
    m2=5.9722*10**24
    m3=6.4185*10**23
    G=6.67*10**(-11)
    f=np.zeros(np.shape(u))
    x=u[0:2]
    v=u[2:4]
    y=u[4:6]
    w=u[6:8]
    z=u[8:10]
    s=u[10:12]
    dxy3=np.linalg.norm(x-y)**3
    dxz3=np.linalg.norm(x-z)**3
    dyz3=np.linalg.norm(y-z)**3
    f[0:2]=v
    f[2:4]=-m2*G/dxy3*(x-y)-m3*G/dxz3*(x-z)
    f[4:6]=w
    f[6:8]=-m1*G/dxy3*(y-x)-m3*G/dyz3*(y-z)
    f[8:10]=s
    f[10:12]=-m1*G/dxz3*(z-x)-m2*G/dyz3*(z-y)
    return f

class ODEproblem:
    def __init__(self,name):
        self.name=name
        if self.name=="linear_scalar":
            self.u0 = np.array([1.])
            self.T_fin= 2.
            self.k_coef=10
            self.matrix=np.array([-self.k_coef])
        elif self.name=="nonlinear_scalar":
            self.k_coef=10
            self.u0 = np.array([1.1/np.sqrt(self.k_coef)])
            self.T_fin= 1.
        elif self.name=="linear_system2":
            self.u0 = np.array([0.9,0.1])
            self.T_fin= 1.
            self.matrix = np.array([[-5,1],[5,-1]])
        elif self.name=="linear_system3":
            self.u0 = np.array([0,0.,10.])
            self.T_fin= 10.
        elif self.name=="nonlinear_system3":
            self.u0 = np.array([9.98,0.01,0.01])
            self.T_fin= 30.
        elif self.name=="SIR":
            self.u0 = np.array([1000.,1,10**-20])
            self.T_fin= 10.
        elif self.name=="nonLinearOscillator":
            self.u0 = np.array([1.,0.])
            self.T_fin= 50
        elif self.name=="nonLinearOscillatorDamped":
            self.u0 = np.array([1.,0.])
            self.T_fin= 50
        elif self.name=="pendulum":
            self.u0 = np.array([2.,0.])
            self.T_fin= 50
        elif self.name=="Robertson":
            self.u0 = np.array([1.,10**-20,10**-20])
            self.T_fin= 10.**10.
        elif self.name=="lotka":
            self.u0 = np.array([1.,2.])
            self.T_fin= 100.
        elif self.name=="threeBodies":
            self.u0 = np.array([0,0,0,0,149*10**9,0,0,30*10**3,-226*10**9,0,0,-24.0*10**3])
            self.T_fin= 10.**8.
        else:
            raise ValueError("Problem not defined")

    def flux(self,u,t=0):
        if self.name=="linear_scalar":
            return linear_scalar_flux(u,t,self.k_coef)
        elif self.name=="nonlinear_scalar":
            return nonlinear_scalar_flux(u,t,self.k_coef)
        elif self.name=="linear_system2":
            return linear_system2_flux(u,t)
        elif self.name=="linear_system3":
            return linear_system3_flux(u,t)
        elif self.name=="nonlinear_system3":
            return nonlinear_system3_flux(u,t)
        elif self.name=="SIR":
            return SIR_flux(u,t)
        elif self.name=="nonLinearOscillator":
            return nonLinearOscillator_flux(u,t)
        elif self.name=="nonLinearOscillatorDamped":
            return nonLinearOscillatorDamped_flux(u,t)
        elif self.name=="pendulum":
            return pendulum_flux(u,t)
        elif self.name=="Robertson":
            return Robertson_flux(u,t)
        elif self.name=="lotka":
            return lotka_flux(u,t)
        elif self.name=="threeBodies":
            return threeBodies_flux(u,t)
        else:
            raise ValueError("Flux not defined for this problem")

    def jacobian(self,u,t=0):
        if self.name=="linear_scalar":
            return linear_scalar_jacobian(u,t,self.k_coef)
        elif self.name=="nonlinear_scalar":
            return nonlinear_scalar_jacobian(u,t,self.k_coef)
        elif self.name=="linear_system2":
            return linear_system2_jacobian(u,t)
        elif self.name=="linear_system3":
            return linear_system3_jacobian(u,t)
        elif self.name=="pendulum":
            return pendulum_jacobian(u,t)
        elif self.name=="SIR":
            return SIR_jacobian(u,t)
        elif self.name=="Robertson":
            return Robertson_jacobian(u,t)
        elif self.name=="lotka":
            return lotka_jacobian(u,t)
        else:
            raise ValueError("Jacobian not defined for this problem")

    def exact(self,u,t):
        if self.name=="linear_scalar":
            return linear_scalar_exact_solution(u,t,self.k_coef)
        elif self.name=="nonlinear_scalar":
            return nonlinear_scalar_exact_solution(u,t,self.k_coef)
        elif self.name=="linear_system2":
            return linear_system2_exact_solution(u,t)
        elif self.name=="linear_system3":
            return linear_system3_exact_solution(u,t)
        elif self.name=="nonLinearOscillator":
            return nonLinearOscillator_exact_solution(u,t)
        elif self.name=="nonLinearOscillatorDamped":
            return nonLinearOscillatorDamped_exact_solution(u,t)
        else:
            raise ValueError("Exact solution not defined for this problem")

    def exact_solution_times(self,u0,tt):
        exact_solution=np.zeros((len(u0),len(tt)))
        for it, t in enumerate(tt):
            exact_solution[:,it]=self.exact(u0,t)
        return exact_solution

    def prod_dest(self,u,t=0):
        if self.name=="linear_system2":
            return linear_system2_production_destruction(u,t)
        if self.name=="nonlinear_system3":
            return nonlinear_system3_production_destruction(u,t)
        elif self.name=="Robertson":
            return Robertson_production_destruction(u,t)
        elif self.name=="SIR":
            return SIR_production_destruction(u,t)
        else:
            raise ValueError("Prod Dest not defined for this problem")

```

```python
## explicit RK method
def explicitRK(flux, tspan, y_0, A, b, c):
    # Solving u'=F(u,t)
    # input: flux=F, tspan is a vector of times determining the RK steps
    # input: y_0 the initial condition
    # input: A,b,c are matrix and vectors of RK methods
    N_time=len(tspan)  # N+1
    dim=len(y_0)          # S
    y=np.zeros((dim,N_time))    # initializing the variable of solutions
    y[:,0]=y_0                 # first timestep
    S=np.shape(A)[0]
    u=np.zeros((dim,S))       # Internal stages
    Fu=np.zeros((dim,S))       # Flux at internal stages
    for n in range(N_time-1):    # n=0,..., N-1
        delta_t=tspan[n+1]-tspan[n]
        for k in range(S):
            u[:,k]=y[:,n]
            for j in range(k):
                u[:,k] =u[:,k]+ FILL WITH THE FORMULA    ## update the value of u^{(k)} adding all the contributions
            Fu[:,k] = FILL IN WITH THE FORMULA           ## Compute the flux at the new found value
        y[:,n+1]=y[:,n]
        for j in range(S):
            y[:,n+1]=y[:,n+1]+  FILL IN WITH FORMULA     ## Final update of y^{n+1}
    return tspan, y
```

```python
# Test the RK44 method on a linear system 2x2
import matplotlib.pyplot as plt
pr=ODEproblem("linear_system2")
t_span=np.linspace(0,pr.T_fin,4)
rk44=rk.loadRKM('RK44')
tt,uu=explicitRK(pr.flux,t_span,pr.u0,rk44.A,rk44.b,rk44.c)
plt.plot(tt,uu[0,:])
plt.plot(tt,uu[1,:])
```

```python
# Exercise
# Check the order of accuracy for the rk1,2,3,4 we have defined before
def error(tt,yy):
    errors=np.zeros(len(tt))
    for it, t in enumerate(tt):
        errors[it]=np.linalg.norm(yy[:,it]-pr.exact(yy[:,0],t))
    return np.mean(errors)

Ns=[2**k for k in range(1,10)]
solvers=[rk1,rk2,rk3,rk44]

errorEx=np.zeros((len(solvers),len(Ns)))
dts=    np.zeros(len(Ns))

for iN, N in enumerate(Ns):
    tspan=np.linspace(0,pr.T_fin,N)
    dts[iN]=tspan[1]-tspan[0]
    for iS, rk in enumerate(solvers):
        tt,yy=explicitRK(pr.flux,tspan,pr.u0,rk.A,rk.b,rk.c)
        errorEx[iS,iN]=error(tt,yy)

## Plot the error with respect to the timestep dt
plt.figure()
for iS, rk in enumerate(solvers):
    plt.loglog(FILL IN WITH THE VARIABLES ,label=rk.name)                ## Error of RK
    plt.loglog(dts,dts**(rk.order()),":", label="order %d"%(rk.order())) ## Reference order
plt.legend()
```

## Multistep Methods

One of the cons of RK is the need of computing many evaluations of the flux $F$ at each timestep. This can computationally cost a lot.
One can instead exploit the knowledge of the previous states $y^{n-1},y^{n-2},\dots,y^{n-k}$ to obtain $y^n$.
Multistep methods interpolate previous states to extrapolate the new solution.

Two types
* Adams methods: use the values of $\lbrace F(t^{n-j},y^{n-j})\rbrace_{j=1}^k$
* General methods: use also the values of $\lbrace y^{n-j}\rbrace_{j=1}^k$

Example of a general linear multistep method

$$
y^{n+1}+\sum_{j=1}^k \alpha_j y^{n-j+1} = \Delta t \sum_{j=0}^k \beta_j F(t^{n-j+1},y^{n-j+1})
$$

### Adams Bashforth methods
**Main idea**

$$
y(t^{n+1})=y(t^{n})+\int_{t^{n}}^{t^{n+1}} F(t,y(t))dt \approx y^{n+1} = y^{n}+\int_{t^{n}}^{t^{n+1}} P_n (t) dt
$$

In particular, choose $P_n=P_{n,k}$ to be the interpolant polynomial of degree $\leq k-1$ such that

$$
P_{n,k}(t^{n-j+1})=F^{n-j+1}:=F(t^{n-j+1},y^{n-j+1}),\quad j=1,\dots, k.
$$

Using Lagrange interpolation polynomials, we can write

$$
P_{n,k}(t):=\sum_{j=1}^k F^{n-j+1} L_{n,j,k}(t), \quad \text{with } L_{n,j,k}(t)=\prod_{i=1,i\neq j}^{k} \frac{ t-t^{n+1-i}}{t^{n+1-j}-t^{n+1-i}}
$$

Defining

$$
b_{n,j,k}:=\frac{1}{t^{n+1}-t^{n}} \int_{t^{n}}^{t^{n+1}} L_{n,j,k}(t) dt, \quad j=1,\dots,k
$$

the method reads

$$
y^{n+1}:=y^n + \Delta t \sum_{j=1}^k b_{n,j,k} F^{n-j+1}, \quad \text{for } n\geq k\\
F^{n+1}:=F(t^{n+1},y^{n+1}).
$$

* Explicit method
* $k=0$ Explicit Euler
* Error at each step (we can aim to) $\mathcal O (\Delta t^{k+1})$, global error is $\mathcal O (\Delta t^{k})$

#### Starting values
Starting with the initial value problem, to compute $y^1$ we do not know $y^{-1}, \dots, y^{-k+1}$. One can proceed in different ways in this case.
* Start with $k$ steps of RK methods
* Start with an exact solution (if available)
* Change $k$ along the method (order of accuracy could be lost -> Change the step size)
* Few implicit steps and then explicit

#### Adams Bashforth parameters

$$
\begin{array}
{c|cccc}
k & b_{n,1,k} & b_{n,2,k} & b_{n,3,k}  & b_{n,4,k} &b_{n,5,k}\\
\hline
1& 1\\
2& \frac{3}{2} & -\frac{1}{2}\\
3& \frac{23}{12} & -\frac{4}{3} & \frac{5}{12} \\
4& \frac{55}{24}& -\frac{59}{24} & \frac{37}{24} &-\frac{3}{8} \\
5& \frac{1901}{720}& -\frac{1387}{360} & \frac{109}{30} &-\frac{637}{360} & \frac{251}{720}
\end{array}
$$

### Adams Moulton methods (implicit)

In particular, choose $P_n=P_{n,k}$ to be the interpolant polynomial of degree $\leq k$ such that

$$
P_{n,k}(t^{n-j})=F^{n-k}:=F(t^{n-j},y^{n-j}),\quad j=0,\dots, k.
$$

Using Lagrange interpolation polynomials, we can write

$$
P_{n,k}(t):=\sum_{j=0}^k F^{n-j+1} L_{n,j,k}(t), \quad \text{with } L_{n,j,k}(t)=\prod_{i=0,i\neq j}^{k} \frac{ t-t^{n-i+1}}{t^{n-j+1}-t^{n-i+1}}
$$

Defining

$$
b_{n,j,k}:=\frac{1}{t^{n+1}-t^{n}} \int_{t^{n}}^{t^{n+1}} L_{n,j,k}(t) dt, \quad j=0,\dots,k
$$

the method reads

$$
y^{n+1}:=y^n + \Delta t \sum_{j=0}^k b_{n,j,k} F^{n-j+1}, \quad \text{for } n\geq k\\
F^{n+1}=F(t^{n+1},y^{n+1}),
$$

i.e., at each step we have to solve

$$
y^{n+1}-\Delta t b_{n,0,k} F(t^{n+1},y^{n+1}):=y^n + \Delta t \sum_{j=1}^k b_{n,j,k} F^{n-j+1}, \quad \text{for } n\geq k\\
$$

Unique solution if $\Delta t b_{n,0,k} L< 1$ ($L$ Lipschitz continuity constant of $F$), solution can be obtained with the fixed point iteration method.

* Implicit method
* $k=0$ Implicit Euler
* Error at each step $\mathcal O (\Delta t^{k+2})$, global error is $\mathcal O (\Delta t^{k+1})$
* Same problems with starting values

#### Adams Moulton parameters

$$
\begin{array}
{c|ccccc|c}
k & b_{n,0,k} & b_{n,1,k} & b_{n,2,k} & b_{n,3,k}  & b_{n,4,k} & Notes\\
\hline
0& 1 &&&&& \text{Implicit Euler}\\
1& \frac{1}{2} & \frac{1}{2} &&&&\text{ Crank-Nicolson}\\
2& \frac{5}{12} & \frac{2}{3} & -\frac{1}{12}  \\
3& \frac{3}{8}& \frac{19}{24} & -\frac{5}{24} & \frac{1}{24} \\
4& \frac{251}{720}& \frac{323}{360} & -\frac{11}{30} & \frac{53}{360} & -\frac{19}{720}
\end{array}
$$

#### How to render Adams Moulton explicit?

Use Adams Bashforth (AB) to compute $y^{n+1,*}$, then update the value with Adams Moulton (AM) formula, where the obtained value can be substituted into $F(t^{n+1},y^{n+1,*})$. In particular, we use a $k^*$ for Adam Bashforth that is equal to $k$ of AM, this means that we use during AB one extra value of $F$ with respect to the ones we use in AM.

```python
# Be carefull with notation
import nodepy.linear_multistep_method as lm
AB3=lm.Adams_Bashforth(3)
print(AB3.beta)

AM3=lm.Adams_Moulton(3)
print(AM3.beta)

plt.figure()
AB3.plot_stability_region()

plt.figure()
AM3.plot_stability_region();
```

```python
# Let's see some methods
import nodepy.linear_multistep_method as lm
for order in range(2,10):
    print("Order %d"%(order))
    AB=lm.Adams_Bashforth(order)
    AM =lm.Adams_Moulton(order-1)
    print("Adam Bashforth")
    plt.figure()
    AB.plot_stability_region()
    plt.show()
    print("Adam Moulton")
    plt.figure()
    AM.plot_stability_region()
    plt.show()
```

#### Implement the Adams Bashforth method

Pass in input the coefficients $b_j$ of the scheme and the initial $k$ values needed.

```python
# Exercise
## explicit Adams Bashforth method
def multiAB(flux, tspan, y_0, b):
    # Solving u'=F(u,t)
    # input: flux=F, tspan is a vector of times determining the RK steps
    # input: y_0 the initial condition with the first k values of the solution
    # input: b are k+1 b_j coefficients where the last one is 0
    N_time=len(tspan)  # N+1
    dim=y_0.shape[0]          # S
    y=np.zeros((dim,N_time))    # initializing the variable of solutions
    k = len(b)-1                # size of AB
    if y_0.shape[1] < k:
        raise ValueError("Input vector is too small")
    y[:,:k]=y_0                  # first timesteps
    n0=k-1                       # last index assigned
    Fu=np.zeros((dim,k))         # Flux at internal stages
    for j in range(k):
        Fu[:,j]=flux(y[:,j])
    for n in range(n0,N_time-1):    # n=0,..., N-1
        delta_t=tspan[n+1]-tspan[n]
        y[:,n+1]=y[:,n]
        for j in range(k):
            y[:,n+1]=y[:,n+1]+ FILL IN       ## Update values
        Fu[:,:k-1] = Fu[:,1:]                # Shift of fluxes (I don't need anymore the first one)
        Fu[:,k-1] = flux(y[:,n+1])           # Compute the new flux
    return tspan, y
```

```python
#Test the Adams Bashforth method
order = 2
N=100 #5
AB=lm.Adams_Bashforth(order)
pr=ODEproblem("linear_system2")
t_span=np.linspace(0,pr.T_fin,N)
y_0 = np.zeros((len(pr.u0),order))
for j in range(order):
    y_0[:,j] = pr.exact(pr.u0,t_span[j])

tt,uu=multiAB(pr.flux,t_span,y_0,AB.beta)
plt.plot(tt,uu[0,:])
plt.plot(tt,uu[1,:])
```

```python
## TEST CONVERGENCE!
pr=ODEproblem("linear_system2")
def error(tt,yy):
    errors=np.zeros(len(tt))
    for it, t in enumerate(tt):
        errors[it]=np.linalg.norm(yy[:,it]-pr.exact(yy[:,0],t))
    return np.mean(errors)

Ns=[2**k for k in range(5,10)]
maxOrder=6

errorEx=np.zeros((maxOrder,len(Ns)))
dts=    np.zeros(len(Ns))

for iN, N in enumerate(Ns):                                        # loop on the number of timesteps
    tspan=np.linspace(0,pr.T_fin,N)
    dts[iN]=tspan[1]-tspan[0]
    for iorder in range(maxOrder):                                 # loop on the order
        order=iorder+1
        AB=lm.Adams_Bashforth(   FILL IN WITH THE CORRECT NUMBER    # Load the AB method of order order
        y_0 = np.zeros((len(pr.u0),order))
        for j in range(order):                                     # loop to initialize the first steps
            y_0[:,j] = pr.exact(pr.u0,tspan[j])
        tt,yy=multiAB(    FILL IN THE CORRECT VARIABLES            # Compute the simulation
        errorEx[iorder,iN]=error(tt,yy)                            # compute the error

plt.figure()
for iorder in range(maxOrder):
    order=iorder+1
    plt.loglog(dts,errorEx[iorder,:],label="AB"+str(order))
    plt.loglog(dts,dts**(order)*errorEx[iorder,2]/dts[2]**order,":", label="order %d"%(order))
plt.legend()
```

**Pro exercise** Code the combined Adams Moulton Bashforth method
Example $k$th order:

$$
y^{*,n+1}=y^n +\Delta t \sum_{j=1}^k b^{AB}_{n,j,k} F(y^{n+1-j})\\
y^{n+1} = y^{n} +\Delta t b^{AM}_{n,0,k-1} F(y^{*,n+1}) + \Delta t \sum_{j=1}^{k-1} b^{AM}_{n,j,k-1} F(y^{n+1-j})
$$

```python
### ADAMS moulton Bashforth
# We combine the two methods, in order to get something positive
## explicit Adams Bashforth method
def multiAMB(flux, tspan, y_0, bAB, bAM):
    # Solving u'=F(u,t)
    # input: flux=F, tspan is a vector of times determining the RK steps
    # input: y_0 the initial condition with the first k values of the solution
    # input: bAB are k+1 b_j coefficients where the last one is 0 of Adam Bashforth
    # input: bAB are k b_j coefficients of Adams Moulton
    N_time=len(tspan)  # N+1
    dim=y_0.shape[0]          # S
    y=np.zeros((dim,N_time))    # initializing the variable of solutions
    k = len(bAB)-1                # size of AB
    if y_0.shape[1] < k:
        raise ValueError("Input vector is too small")
    y[:,:k]=y_0                  # first timesteps
    n0=k-1                       # last index assigned
    Fu=np.zeros((dim,k))         # Flux at internal stages

    CODE THE METHOD DESCRIBED ABOVE

    return tspan, y
```

```python
# Test if it works
order = 2
N=100 #5
AB=lm.Adams_Bashforth(order)
AM=lm.Adams_Moulton(order-1)
pr=ODEproblem("linear_system2")
t_span=np.linspace(0,pr.T_fin,N)
y_0 = np.zeros((len(pr.u0),order))
for j in range(order):
    y_0[:,j] = pr.exact(pr.u0,t_span[j])

tt,uu=multiAMB(pr.flux,t_span,y_0,AB.beta, AM.beta)
plt.plot(tt,uu[0,:])
plt.plot(tt,uu[1,:])
```

```python
## TEST CONVERGENCE on the usual problem and usual timesteps
pr=ODEproblem("linear_system2")
def error(tt,yy):
    errors=np.zeros(len(tt))
    for it, t in enumerate(tt):
        errors[it]=np.linalg.norm(yy[:,it]-pr.exact(yy[:,0],t))
    return np.mean(errors)

Ns=[2**k for k in range(5,10)]
orders=range(2,7)
maxOrder=len(orders)

errorEx=np.zeros((maxOrder,len(Ns)))
dts=    np.zeros(len(Ns))

# Compute the error for orders and Ns as above and
# plot it in a reasonable scale in order to compare it with a reference order
```

Chapter 3 Classical High Order Methods.ipynb