# Unrestricted Open-Shell Hartree-Fock

In the first two tutorials in this module, we wrote programs which implement a closed-shell formulation of Hartree-Fock theory using restricted orbitals, aptly named Restricted Hartree-Fock (RHF).  In this tutorial, we will abandon strictly closed-shell systems and the notion of restricted orbitals, in favor of a more general theory known as Unrestricted Hartree-Fock (UHF) which can accommodate more diverse molecules.  In UHF, the orbitals occupied by spin up ($\alpha$) electrons and those occupied by spin down ($\beta$) electrons no longer have the same spatial component, e.g.,

$$\chi_i({\bf x}) = \begin{cases}\psi^{\alpha}_j({\bf r})\alpha(\omega) \\ \psi^{\beta}_j({\bf r})\beta(\omega)\end{cases},$$

meaning that they will not have the same orbital energy.  This relaxation of orbital constraints allows for more variational flexibility, which leads to UHF always being able to find a lower total energy solution than RHF.

## I. Theoretical Overview
In UHF, we seek to solve the coupled equations

\begin{align}
{\bf F}^{\alpha}{\bf C}^{\alpha} &= {\bf SC}^{\alpha}{\bf\epsilon}^{\alpha} \\
{\bf F}^{\beta}{\bf C}^{\beta} &= {\bf SC}^{\beta}{\bf\epsilon}^{\beta},
\end{align}

which are the unrestricted generalizations of the restricted Roothan equations, called the Pople-Nesbet-Berthier equations.  Here, the one-electron Fock matrices are given by

\begin{align}
F_{\mu\nu}^{\alpha} &= H_{\mu\nu} + (\mu\,\nu\mid\lambda\,\sigma)[D_{\lambda\sigma}^{\alpha} + D_{\lambda\sigma}^{\beta}] - (\mu\,\lambda\,\mid\nu\,\sigma)D_{\lambda\sigma}^{\beta}\\
F_{\mu\nu}^{\beta} &= H_{\mu\nu} + (\mu\,\nu\mid\,\lambda\,\sigma)[D_{\lambda\sigma}^{\alpha} + D_{\lambda\sigma}^{\beta}] - (\mu\,\lambda\,\mid\nu\,\sigma)D_{\lambda\sigma}^{\alpha},
\end{align}

where the density matrices $D_{\lambda\sigma}^{\alpha}$ and $D_{\lambda\sigma}^{\beta}$ are given by

\begin{align}
D_{\lambda\sigma}^{\alpha} &= C_{\sigma i}^{\alpha}C_{\lambda i}^{\alpha}\\
D_{\lambda\sigma}^{\beta} &= C_{\sigma i}^{\beta}C_{\lambda i}^{\beta}.
\end{align}

Unlike for RHF, the orbital coefficient matrices ${\bf C}^{\alpha}$ and ${\bf C}^{\beta}$ are of dimension $M\times N^{\alpha}$ and $M\times N^{\beta}$, where $M$ is the number of AO basis functions and $N^{\alpha}$ ($N^{\beta}$) is the number of $\alpha$ ($\beta$) electrons.  The total UHF energy is given by

\begin{align}
E^{\rm UHF}_{\rm total} &= E^{\rm UHF}_{\rm elec} + E^{\rm BO}_{\rm nuc},\;\;{\rm with}\\
E^{\rm UHF}_{\rm elec} &= \frac{1}{2}[({\bf D}^{\alpha} + {\bf D}^{\beta}){\bf H} +
{\bf D}^{\alpha}{\bf F}^{\alpha} + {\bf D}^{\beta}{\bf F}^{\beta}].
\end{align}

## II. Implementation

In any SCF program, there will be several common elements which can be abstracted from the program itself into separate modules, classes, or functions to 'clean up' the code that will need to be written explicitly; examples of this concept can be seen throughout the Psi4Julia reference implementations.  For the purposes of this tutorial, we can achieve some degree of code cleanup without sacrificing readabilitiy and clarity by focusing on abstracting only the parts of the code which are both
- Lengthy subroutines, and
- Used repeatedly.

In our UHF program, let's use what we've learned in the last tutorial by also implementing DIIS convergence accelleration for our SCF iterations.  With this in mind, two subroutines in particular would benefit from abstraction are

1. Orthogonalize & diagonalize Fock matrix
2. Extrapolate previous trial vectors for new DIIS solution vector

Before we start writing our UHF program, let's try to write functions which can perform the above tasks so that we can use them in our implementation of UHF.  Recall that defining functions in Julia has the following syntax:
~~~julia
function function_name(args; kwargs)
    # function block
    return_values
end
~~~
A thorough discussion of defining functions in Julia can be found [here](https://docs.julialang.org/en/v1/manual/functions/index.html "Go to Julia docs").  First, let's write a function which can diagonalize the Fock matrix and return the orbital coefficient matrix **C** and the density matrix **D**.  From our RHF tutorial, this subroutine is executed with:
~~~julia
F_p =  A * F * A
e, C_p = eigen(Hermitian(F_p))
C = A * C_p
C_occ = C[:, 1:ndocc]
D = C_occ * C_occ'
~~~
Examining this code block, there are three quantities which must be specified beforehand:
- Fock matrix, **F**
- Orthogonalization matrix, ${\bf A} = {\bf S}^{-1/2}$
- Number of doubly occupied orbitals, `ndocc`

However, since the orthogonalization matrix **A** is a static quantity (only built once, then left alone) we may choose to leave **A** as a *global* quantity, instead of an argument to our function.  In the cell below, using the code snippet given above, write a function `diag_F()` which takes **F** and the number of orbitals `norb` as arguments, and returns **C** and **D**:

```julia
# ==> Define function to diagonalize F <==
function diag_F(F, norb, A)
    F_p = A * F * A
    e, C_p = eigen(Hermitian(F_p))
    C = A * C_p
    C_occ = C[:, 1:norb]
    D = C_occ * C_occ'
    C, D
end
```

Next, let's write a function to perform DIIS extrapolation and generate a new solution vector.  Recall that the DIIS-accellerated SCF algorithm is:
#### Algorithm 1: DIIS within a generic SCF Iteration
1. Compute **F**, append to list of previous trial vectors
2. Compute AO orbital gradient **r**, append to list of previous residual vectors
3. Compute RHF energy
3. Check convergence criteria
    - If RMSD of **r** sufficiently small, and
    - If change in SCF energy sufficiently small, break
4. Build **B** matrix from previous AO gradient vectors
5. Solve Pulay equation for coefficients $\{c_i\}$
6. Compute DIIS solution vector **F_DIIS** from $\{c_i\}$ and previous trial vectors
7. Compute new orbital guess with **F_DIIS**

In our function, we will perform steps 4-6 of the above algorithm.  What information will we need to provide our function in order to do so?  To build **B** (step 4 above) in the previous tutorial, we used:
~~~julia
# Build B matrix
B_dim = length(F_list) + 1
B = zeros(B_dim, B_dim)
B[end,   :] .= -1
B[:  , end] .= -1
B[end, end]  = 0
for i in eachindex(F_list), j in eachindex(F_list)
    B[i, j] = dot(DIIS_RESID[i], DIIS_RESID[j])
end
~~~
Here, we see that we must have all previous DIIS residual vectors (`DIIS_RESID`), as well as knowledge about how many previous trial vectors there are (for the dimension of **B**).  To solve the Pulay equation (step 5 above):
~~~julia
# Build RHS of Pulay equation
rhs = zeros(B_dim)
rhs[end] = -1

# Solve Pulay equation for c_i's with NumPy
coeff = B \ rhs
~~~
For this step, we only need the dimension of **B** (which we computed in step 4 above) and a Julia routine, so this step doesn't require any additional arguments.  Finally, to build the DIIS Fock matrix (step 6):
~~~julia
# Build DIIS Fock matrix
F = zeros(size(F_list[0]))
for x in 1:length(coeff) - 1
    F += coeff[x] * F_list[x]
end
~~~
Clearly, for this step, we need to know all the previous trial vectors (`F_list`) and the coefficients we generated in the previous step.  In the cell below, write a funciton `diis_xtrap()` according to Algorithm 1 steps 4-6, using the above code snippets, which takes a list of previous trial vectors `F_list` and residual vectors `DIIS_RESID` as arguments and returns the new DIIS solution vector `F_DIIS`:

```julia
# ==> Build DIIS Extrapolation Function <==
function diis_xtrap(F_list, DIIS_RESID)
    # Build B matrix
    B_dim = length(F_list) + 1
    B = zeros(B_dim, B_dim)
    B[end,   :] .= -1
    B[:  , end] .= -1
    B[end, end]  =  0
    for i in eachindex(F_list), j in eachindex(F_list)
       B[i, j] = dot(DIIS_RESID[i],  DIIS_RESID[j])
    end

    # Build RHS of Pulay equation
    rhs = zeros(B_dim)
    rhs[end] = -1

    # Solve Pulay equation for c_i's with Julia
    coeff = B \ rhs

    # Build DIIS Fock matrix
    F = zeros(size(F_list[1]))
    for i in 1:length(coeff) - 1
       F += coeff[i] * F_list[i]
    end
    F
end
```

We are now ready to begin writing our UHF program!  Let's begin by importing  Psi4 , NumPy, TensorOperations, LinearAlgebra, and defining our molecule & basic options:

```julia
# ==> Import Psi4 & NumPy <==
using PyCall: pyimport
psi4 = pyimport("psi4")
np   = pyimport("numpy") # used only to cast to Psi4 arrays
using TensorOperations: @tensor
using LinearAlgebra: Diagonal, Hermitian, eigen, tr, norm, dot
using Printf: @printf
```

```julia
# ==> Set Basic Psi4 Options <==
# Memory specification
psi4.set_memory(Int(5e8))
numpy_memory = 2

# Set output file
psi4.core.set_output_file("output.dat", false)

# Define Physicist's water -- don't forget C1 symmetry!
mol = psi4.geometry("""
O
H 1 1.1
H 1 1.1 2 104
symmetry c1
""")

# Set computation options
psi4.set_options(Dict("basis"         => "cc-pvdz",
                      "scf_type"      => "pk",
                      "e_convergence" => 1e-8,
                      "guess"         => "core",
                      "reference"     => "uhf"))
```

You may notice that in the above `psi4.set_options()` block, there are two additional options -- namely, `'guess': 'core'` and `'reference': 'uhf'`.  These options make sure that when we ultimately check our program against  Psi4, the options  Psi4  uses are identical to our implementation.  Next, let's define the options for our UHF program; we can borrow these options from our RHF implementation with DIIS accelleration that we completed in our last tutorial.

```julia
# ==> Set default program options <==
# Maximum SCF iterations
MAXITER = 40
# Energy convergence criterion
E_conv = 1.0e-6
D_conv = 1.0e-3
```

Static quantities like the ERI tensor, core Hamiltonian, and orthogonalization matrix have exactly the same form in UHF as in RHF.  Unlike in RHF, however, we will need the number of $\alpha$ and $\beta$ electrons.  Fortunately, both these values are available through querying the Wavefunction object.  In the cell below, generate these static objects and compute each of the following:
- Number of basis functions, `nbf`
- Number of alpha electrons, `nalpha`
- Number of beta electrons, `nbeta`
- Number of doubly occupied orbitals, `ndocc` (Hint: In UHF, there can be unpaired electrons!)

```julia
# ==> Compute static 1e- and 2e- quantities with Psi4 <==
# Class instantiation
wfn = psi4.core.Wavefunction.build(mol, psi4.core.get_global_option("basis"))
mints = psi4.core.MintsHelper(wfn.basisset())

# Overlap matrix
S = np.asarray(mints.ao_overlap()) # we only need a copy

# Number of basis Functions, alpha & beta orbitals, and # doubly occupied orbitals
nbf = wfn.nso()
nalpha = wfn.nalpha()
nbeta = wfn.nbeta()
ndocc = min(nalpha, nbeta)

println("Number of basis functions: ", nbf)
println("Number of singly occupied orbitals: ", abs(nalpha-nbeta))
println("Number of doubly occupied orbitals: ", ndocc)

# Memory check for ERI tensor
I_size = nbf^4 * 8.e-9
println("\nSize of the ERI tensor will be $I_size GB.")
memory_footprint = I_size * 1.5
if I_size > numpy_memory
    psi4.core.clean()
    throw(OutOfMemoryError("Estimated memory utilization ($memory_footprint GB) exceeds " *
                           "allotted memory limit of $numpy_memory GB."))
end

# Build ERI Tensor
I = np.asarray(mints.ao_eri()) # we only need a copy

# Build core Hamiltonian
T = np.asarray(mints.ao_kinetic()) # we only need a copy
V = np.asarray(mints.ao_potential()) # we only need a copy
H = T + V;

# Construct AO orthogonalization matrix A
A = mints.ao_overlap()
A.power(-0.5, 1.e-16) # ≈ Julia's A^(-0.5) after psi4view()
A = np.asarray(A);
```

Unlike the static quantities above, the CORE guess in UHF is slightly different than in RHF.  Since the $\alpha$ and $\beta$ electrons do not share spatial orbitals, we must construct a guess for *each* of the $\alpha$ and $\beta$ orbitals and densities.  In the cell below, using the function `diag_F()`, construct the CORE guesses and compute the nuclear repulsion energy:

(Hint: The number of $\alpha$ orbitals is the same as the number of $\alpha$ electrons!)

```julia
# ==> Build alpha & beta CORE guess <==
Ca, Da = diag_F(H, nalpha, A)
Cb, Db = diag_F(H, nbeta, A)

# Get nuclear repulsion energy
E_nuc = mol.nuclear_repulsion_energy()
```

We are almost ready to perform our SCF iterations; beforehand, however, we must initiate variables for the current & previous SCF energies, and the lists to hold previous residual vectors and trial vectors for the DIIS procedure.  Since, in UHF, there are Fock matrices ${\bf F}^{\alpha}$ and ${\bf F}^{\beta}$ for both $\alpha$ and $\beta$ orbitals, we must apply DIIS to each of these matrices separately.  In the cell below, define empty lists to hold previous Fock matrices and residual vectors for both $\alpha$ and $\beta$ orbitals:

```julia
# ==> Pre-Iteration Setup <==
# SCF & Previous Energy
SCF_E = 0.0
E_old = 0.0
```

We are now ready to write the SCF iterations.  The algorithm for UHF-SCF iteration, with DIIS convergence accelleration, is:
#### Algorithm 2: DIIS within UHF-SCF Iteration
1. Build ${\bf F}^{\alpha}$ and ${\bf F}^{\beta}$, append to trial vector lists
2. Compute the DIIS residual for $\alpha$ and $\beta$, append to residual vector lists
3. Compute UHF energy
4. Convergence check
    - If average of RMSD of $\alpha$ and $\beta$ residual sufficiently small, and
    - If change in UHF energy sufficiently small, break
5. DIIS extrapolation of ${\bf F}^{\alpha}$ and ${\bf F}^{\beta}$ to form new solution vector
6. Compute new ${\alpha}$ and ${\beta}$ orbital & density guesses

In the cell below, write the UHF-SCF iteration according to Algorithm 2:

(Hint: Use your functions `diis_xtrap()` and `diag_F` for Algorithm 2 steps 5 & 6, respectively)

```julia
SCF_E = let SCF_E = SCF_E, E_old = E_old, Da = Da, Db = Db, A = A, I = I, H = H, S = S

   # Trial & Residual Vector Lists -- one each for α & β
   F_list_a = []
   F_list_b = []
   R_list_a = []
   R_list_b = []

   # ==> UHF-SCF Iterations <==
   println("==> Starting SCF Iterations <==")

   # Begin Iterations
   for scf_iter in 1:MAXITER
       # Build Fa & Fb matrices
       @tensor Ja[p,q] := I[p,q,r,s] * Da[r,s]
       @tensor Jb[p,q] := I[p,q,r,s] * Db[r,s]
       @tensor Ka[p,q] := I[p,r,q,s] * Da[r,s]
       @tensor Kb[p,q] := I[p,r,q,s] * Db[r,s]
       Fa = H + (Ja + Jb) - Ka
       Fb = H + (Ja + Jb) - Kb

       # Compute DIIS residual for Fa & Fb
       diis_r_a = A * (Fa * Da * S - S * Da * Fa) * A
       diis_r_b = A * (Fb * Db * S - S * Db * Fb) * A

       # Append trial & residual vectors to lists
       push!(F_list_a, Fa)
       push!(F_list_b, Fb)
       push!(R_list_a, diis_r_a)
       push!(R_list_b, diis_r_b)

       # Compute UHF Energy
       SCF_E = 0.5*tr( H*(Da + Db) + Fa*Da + Fb*Db) + E_nuc

       dE = SCF_E - E_old
       dRMS = 0.5(norm(diis_r_a) + norm(diis_r_b))
       @printf("SCF Iteration %3d: Energy = %4.16f dE = %1.5e dRMS = %1.5e \n",
                           scf_iter,        SCF_E, SCF_E - E_old,     dRMS)

       # Convergence Check
       if abs(dE) < E_conv && dRMS < D_conv
           break
       end
       E_old = SCF_E

       # DIIS Extrapolation
       if scf_iter >= 2
           Fa = diis_xtrap(F_list_a, R_list_a)
           Fb = diis_xtrap(F_list_b, R_list_b)
       end

       # Compute new orbital guess
       Ca, Da = diag_F(Fa, nalpha, A)
       Cb, Db = diag_F(Fb, nbeta, A)

       # MAXITER exceeded?
       if scf_iter == MAXITER
           psi4.core.clean()
           throw(MethodError("Maximum number of SCF iterations exceeded."))
       end
   end
   SCF_E
end
# Post iterations
println("\nSCF converged.")
println("Final RHF Energy: $SCF_E [Eh]")
println()
```

Congratulations! You've written your very own Unrestricted Hartree-Fock program with DIIS convergence accelleration!  Finally, let's check your final UHF energy against  Psi4:

```julia
# Compare to Psi4
SCF_E_psi = psi4.energy("SCF")
SCF_E
psi4.compare_values(SCF_E_psi, SCF_E, 6, "SCF Energy")
```

## References
1. A. Szabo and N. S. Ostlund, *Modern Quantum Chemistry*, Introduction to Advanced Electronic Structure Theory. Courier Corporation, 1996.
2. I. N. Levine, *Quantum Chemistry*. Prentice-Hall, New Jersey, 5th edition, 2000.
3. T. Helgaker, P. Jorgensen, and J. Olsen, *Molecular Electronic Structure Theory*, John Wiley & Sons Inc, 2000.


# Exploring the possiblities with Python package PyDy

```python
from __future__ import print_function, division
from sympy import symbols, simplify
from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame, Point
from sympy.physics.vector import init_vprinting
init_vprinting(use_latex='mathjax', pretty_print=False)
from sympy.physics.mechanics import inertia, RigidBody
from sympy import trigsimp
from sympy.physics.mechanics import KanesMethod
from pydy.codegen.ode_function_generators import generate_ode_function
from scipy.integrate import odeint
import numpy as np
import matplotlib.pyplot as plt
```

## Reference frames

```python
inertial_frame = ReferenceFrame('I')
ship_frame = ReferenceFrame('S')
```

```python
psi = dynamicsymbols('psi') #Ship heading
X = dynamicsymbols('X') #Ship position in inertial frame
Y = dynamicsymbols('Y') #Ship position in inertial frame
x = dynamicsymbols('x') #Position in ship frame
y = dynamicsymbols('y') #Position in ship frame

```

```python
ship_frame.orient(inertial_frame, 'Axis', (psi, inertial_frame.z))
```

```python
#phi = dynamicsymbols('phi') #Ship roll
#theta = dynamicsymbols('theta') #Ship pitch
```

```python
ship_frame.dcm(inertial_frame)
```

$$\left[\begin{matrix}\operatorname{cos}\left(\psi\right) & \operatorname{sin}\left(\psi\right) & 0\\- \operatorname{sin}\left(\psi\right) & \operatorname{cos}\left(\psi\right) & 0\\0 & 0 & 1\end{matrix}\right]$$

```python
ship_frame.x
```

## Define some points

```python
starting_point = Point('Start')
ship_origo = Point('O')
ship_mass_center = Point('c_G')
x_cg = symbols('x_cg')
y_cg = symbols('y_cg')
z_cg = symbols('z_cg')

ship_mass_center.set_pos(ship_origo, x_cg*ship_frame.x + y_cg*ship_frame.y + z_cg*ship_frame.z)
```

## Kinematical Differential Equations

```python
r = dynamicsymbols('r') # Yaw rate
U = dynamicsymbols('U') # Ship speed in inertial frame
V = dynamicsymbols('V') # Ship speed in inertial frame
u = dynamicsymbols('u') # Ship speed in ship frame
v = dynamicsymbols('v') # Ship speed in ship frame

```

```python
kinematical_differential_equations = [r - psi.diff(),
                                      U - X.diff(),
                                      V - Y.diff(),
]
kinematical_differential_equations
```

$$\left [ r - \dot{\psi}, \quad U - \dot{X}, \quad V - \dot{Y}\right ]$$

## Angular Velocities

```python
ship_frame.set_ang_vel(inertial_frame,r*inertial_frame.z)
```

```python
?ship_frame.set_ang_vel
```

```python
ship_frame.ang_vel_in(inertial_frame)
```

$$r\mathbf{\hat{i}_z}$$

## Linear Velocities

```python
starting_point.set_vel(inertial_frame, 0)
ship_origo.set_vel(inertial_frame,U*inertial_frame.x + V*inertial_frame.y)
ship_origo.vel(inertial_frame)
```

$$U\mathbf{\hat{i}_x} + V\mathbf{\hat{i}_y}$$

```python
ship_mass_center.v2pt_theory(ship_origo, inertial_frame, ship_frame)
```

$$U\mathbf{\hat{i}_x} + V\mathbf{\hat{i}_y} -  y_{cg} r\mathbf{\hat{s}_x} + x_{cg} r\mathbf{\hat{s}_y}$$

## Mass

```python
ship_mass = symbols('m_S')
```

## Inertia

```python
Izz = symbols('I_zz')
```

```python
ship_dyadic = inertia(ship_frame, 0, 0, Izz)
```

```python
ship_dyadic
```

$$I_{zz}\mathbf{\hat{s}_z}\otimes \mathbf{\hat{s}_z}$$

```python
ship_dyadic.to_matrix(ship_frame)
```

$$\left[\begin{matrix}0 & 0 & 0\\0 & 0 & 0\\0 & 0 & I_{zz}\end{matrix}\right]$$

```python
ship_central_inertia = (ship_dyadic, ship_mass_center)
```

```python
ship = RigidBody('Ship', ship_mass_center, ship_frame,
                      ship_mass, ship_central_inertia)
```

## Equations of Motion

```python
coordinates = [psi,X, Y]
coordinates
```

$$\left [ \psi, \quad X, \quad Y\right ]$$

```python
speeds = [r,U, V]
speeds
```

$$\left [ r, \quad U, \quad V\right ]$$

```python
kane = KanesMethod(inertial_frame, coordinates, speeds, kinematical_differential_equations)
```

```python
dummy_force = symbols('force')
ship_force_vector = dummy_force * ship_frame.x
ship_force = (ship_mass_center, ship_force_vector)

resistance_force_symbol = symbols('R')
resistance_force_vector = resistance_force_symbol * ship_frame.x
ship_resistance = (ship_mass_center, resistance_force_vector)

```

```python
loads = [ship_force,ship_resistance]
```

```python
loads
```

    [(c_G, force*S.x), (c_G, R*S.x)]

```python
bodies = [ship]
```

```python
bodies
```

    [Ship]

```python
fr, frstar = kane.kanes_equations(bodies = bodies,loads = loads)
```

```python
trigsimp(fr + frstar)
```

$$\left[\begin{matrix}- R y_{cg} - force y_{cg} + m_{S} \left(x_{cg} \operatorname{sin}\left(\psi\right) + y_{cg} \operatorname{cos}\left(\psi\right)\right) \dot{U} - m_{S} \left(x_{cg} \operatorname{cos}\left(\psi\right) - y_{cg} \operatorname{sin}\left(\psi\right)\right) \dot{V} - \left(I_{zz} + m_{S} \left(x_{cg}^{2} + y_{cg}^{2}\right)\right) \dot{r}\\R \operatorname{cos}\left(\psi\right) + force \operatorname{cos}\left(\psi\right) + m_{S} x_{cg} r^{2} \operatorname{cos}\left(\psi\right) - m_{S} y_{cg} r^{2} \operatorname{sin}\left(\psi\right) + m_{S} \left(x_{cg} \operatorname{sin}\left(\psi\right) + y_{cg} \operatorname{cos}\left(\psi\right)\right) \dot{r} - m_{S} \dot{U}\\R \operatorname{sin}\left(\psi\right) + force \operatorname{sin}\left(\psi\right) + m_{S} x_{cg} r^{2} \operatorname{sin}\left(\psi\right) + m_{S} y_{cg} r^{2} \operatorname{cos}\left(\psi\right) - m_{S} \left(x_{cg} \operatorname{cos}\left(\psi\right) - y_{cg} \operatorname{sin}\left(\psi\right)\right) \dot{r} - m_{S} \dot{V}\end{matrix}\right]$$

```python
mass_matrix = trigsimp(kane.mass_matrix_full)
mass_matrix
```

$$\left[\begin{matrix}1 & 0 & 0 & 0 & 0 & 0\\0 & 1 & 0 & 0 & 0 & 0\\0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & I_{zz} + m_{S} \left(x_{cg}^{2} + y_{cg}^{2}\right) & - m_{S} \left(x_{cg} \operatorname{sin}\left(\psi\right) + y_{cg} \operatorname{cos}\left(\psi\right)\right) & m_{S} \left(x_{cg} \operatorname{cos}\left(\psi\right) - y_{cg} \operatorname{sin}\left(\psi\right)\right)\\0 & 0 & 0 & - m_{S} \left(x_{cg} \operatorname{sin}\left(\psi\right) + y_{cg} \operatorname{cos}\left(\psi\right)\right) & m_{S} & 0\\0 & 0 & 0 & m_{S} \left(x_{cg} \operatorname{cos}\left(\psi\right) - y_{cg} \operatorname{sin}\left(\psi\right)\right) & 0 & m_{S}\end{matrix}\right]$$

```python
forcing_vector = trigsimp(kane.forcing_full)
forcing_vector
```

$$\left[\begin{matrix}r\\U\\V\\- R y_{cg} - force y_{cg}\\R \operatorname{cos}\left(\psi\right) + force \operatorname{cos}\left(\psi\right) + m_{S} x_{cg} r^{2} \operatorname{cos}\left(\psi\right) - m_{S} y_{cg} r^{2} \operatorname{sin}\left(\psi\right)\\R \operatorname{sin}\left(\psi\right) + force \operatorname{sin}\left(\psi\right) + m_{S} x_{cg} r^{2} \operatorname{sin}\left(\psi\right) + m_{S} y_{cg} r^{2} \operatorname{cos}\left(\psi\right)\end{matrix}\right]$$

## Simulation

```python
constants = [ship_mass,
             Izz,
            x_cg,
            y_cg,
            z_cg,
            dummy_force,
            resistance_force_symbol]
```

```python
specified = []

right_hand_side = generate_ode_function(forcing_vector, coordinates,
                                        speeds, constants,
                                        mass_matrix=mass_matrix,
                                        specifieds=specified)
```

```python
?right_hand_side
```

```python
x0 = np.array([0,0,0,0,0,0])
x0
```

    array([0, 0, 0, 0, 0, 0])

```python
numerical_constants = np.array([
                             1,  # ship mass [kg]
                             1,  # ship Izz [kg*m2]
                             0,  # xg [m]
                             0,  # yg [m]
                             0,  # zg [m]
                             1,  # N
                             0.5, # N Resistance
])
```

```python
numerical_specified = np.zeros(3)

args = {'constants': numerical_constants,
        'specified': numerical_specified}

frames_per_sec = 60
final_time = 5.0

t = np.linspace(0.0, final_time, final_time * frames_per_sec)
```

    /Users/martinalexandersson/Dev/ForceMan/venv/lib/python3.6/site-packages/ipykernel_launcher.py:9: DeprecationWarning: object of type <class 'float'> cannot be safely interpreted as an integer.
      if __name__ == '__main__':

## Integrate the Equations of Motion

```python
y = odeint(right_hand_side, x0, t, args=(numerical_specified, numerical_constants))
```

```python
fig,axes = plt.subplots(nrows = 3)
fig.set_size_inches(15,5)

ax = axes[0]
ax.plot(t,y[:,0])
ax.set_title('psi')

ax = axes[1]
ax.plot(t,y[:,1])
ax.set_title('X')

ax = axes[2]
ax.plot(t,y[:,2])
ax.set_title('Y')

fig.tight_layout()
```

```python
help(right_hand_side)
```

    Help on function rhs in module pydy.codegen.ode_function_generators:

    rhs(*args)
        Returns the derivatives of the states, i.e. numerically evaluates the right
        hand side of the first order differential equation.

        x' = f(x, t, r, p)

        Parameters
        ==========
        x : ndarray, shape(6,)
            The state vector is ordered as such:
                - psi(t)
                - X(t)
                - Y(t)
                - r(t)
                - U(t)
                - V(t)
        t : float
            The current time.
        r : dictionary; ndarray, shape(0,); function

            There are three options for this argument. (1) is more flexible but
            (2) and (3) are much more efficient.

            (1) A dictionary that maps the specified functions of time to floats,
            ndarrays, or functions that produce ndarrays. The keys can be a single
            specified symbolic function of time or a tuple of symbols. The total
            number of symbols must be equal to 0. If the value is a
            function it must be of the form g(x, t), where x is the current state
            vector ndarray and t is the current time float and it must return an
            ndarray of the correct shape. For example::

              r = {a: 1.0,
                   (d, b) : np.array([1.0, 2.0]),
                   (e, f) : lambda x, t: np.array(x[0], x[1]),
                   c: lambda x, t: np.array(x[2])}

            (2) A ndarray with the specified values in the correct order and of the
            correct shape.

            (3) A function that must be of the form g(x, t), where x is the current
            state vector and t is the current time and it must return an ndarray of
            the correct shape.

            The specified inputs are, in order:
                -
        p : dictionary len(7) or ndarray shape(7,)
            Either a dictionary that maps the constants symbols to their numerical
            values or an array with the constants in the following order:
                - m_S
                - I_zz
                - x_cg
                - y_cg
                - z_cg
                - force
                - R

        Returns
        =======
        dx : ndarray, shape(6,)
            The derivative of the state vector.

```python

```


# 2. Country level spread dynamics

## Country level SEIR model

At the country level, the dynamics of the disease spread can be described using the same means as the dynamics of the incidence within the city. Individual cities will act as infected persons. In this situation, we have an obvious observable parameter responsible for the individual differences between cities — the size of the city (city population size).

The country level disease spread is due to intercity visits. Let's assume that the number of people visiting a city is directly proportional to its size. This means that the relative (to the population size of the city) human influx does not depend on the size. Another assumption is that the social capacity to infect (roughly speaking activity) brought in by one host does not depend on the size of the city (although there are more opportunities for social activity in a larger city). Under these assumptions for any of the cities, the part of population infected by the visitors turns out to depend only on the proportion of the infected in those cities the visitors arriving from. Thus, the intercity disease spread becomes independent of cities sizes, and we get ordinary [SEIR model](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SEIR_model).

The model consists of four compartments $S$usceptible, $E$xposed,$I$nfectious, and $R$ecovered. Being infected by initial group of visitors a city moves from Susceptible to Recovered according to its individual internal  dynamics depended on the city population social heterogeneity controlled by $v_s$ and inside city initial reproduction number $r_0$(see city level dinamics) and so on.   Assuming the set of cities to be fixed without the birth of new ones and the death of old ones, we obtain a system of SEIR equations:

$$
\begin{align}
\frac{dS}{dt} & = - \beta I S \\[3pt]
\frac{dE}{dt} & = \beta I S - \frac{E}{\tau_E} \\[3pt]
\frac{dI}{dt} & = \frac{E}{\tau_E} - \frac{I}{\tau_I} \\[3pt]
R &=1-S-E-I
\end{align}
$$
We suggest the city get infected and becomes Exposed at the moment when the portion of its infected population becomes equal to some value $i_0$. Up to the mentioned moment we consider the dynamics of the city infected population as defined by external infection influx only, starting from the moment we consider the dynamics of the city infected population as defined by internal dynamics only.  We also suggest  all the parameters defining the internal dynamics are the same for all the cities. In this case the internal dynamics of each infected city is the same. Calculating the dynamisc for one of the cities we use some thresholds to divide the evolution of infection into Exposed,Infectious, and Recovered parts. That way we not only simplify the model and makes it more tractable, but infer the value of the $\tau_E$ and $\tau_I$ for the SEIR equations, and also average infected population portion $\mathcal{I}$ for the infected cities.

Let each of the cities have the same the visitors influx $w\in [0,1]$ (relative to its size) in a unit of time. The out flux value is the same but formed by arbitrary persons not necessary those in influx.  That means in a time unit $w\cdot I \cdot \mathcal{I}$ infected visitors visits a city, and as long as the city stays uninfected there is no out flux of infected persons. So for the city to get infected and become Exposed it takes
$$
\frac{i_0}{w\cdot \mathcal{I} \cdot I }
$$
units of time, and probability to get infected at a unit of time is the inverse value
$$
\frac{w\cdot \mathcal{I}\cdot I }{i_0}
$$
Thus, change in amount of Susceptible cities in $dt$ time is
$$
dS=-\frac{w\cdot \mathcal{I}\cdot I }{i_0}S dt
$$
or
$$
\frac{dS}{dt}=-\frac{w \mathcal{I}}{i_0}I S
$$
That means the $\beta$ coefficient in the SEIR equasions is given by:
$$
\beta=\frac{w \mathcal{I}}{i_0}
$$

Assuming everage citizen have one visits a year and 25 time units in the year, we choose $w$ equal to 0.04.

First, we calculate the pattern of a city infection dynamics based on HCS SIR model

```python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as scst
import scipy.special as scsp
import scipy.integrate as scint
import scipy.optimize as scop
from scipy.integrate import odeint
import matplotlib.ticker as ticker
from dataclasses import dataclass
plt.rc('legend',fontsize=12, handlelength=2) # using a size in points

plt.style.use('seaborn-darkgrid')

modeling_period=20.0
t_step=0.01
m_t=np.arange(0.0, modeling_period+t_step, t_step,dtype=np.float64)
n_steps=len(m_t)-1
def M(mu,v):return np.exp(mu+0.5*v)
```

```python
def HCS_SIR(r0_0,I0,τ,vs0,vb0):
    '''
    Heterogeneous in Catching and Spreading
    SIR model
    '''
    def a_s(mus,C,vs):
        Cpvs=C+vs
        return M(mus,vs)*(1.0+Cpvs*(1.0+0.5*Cpvs))
    M0=M(0.0,vs0+vb0)
    a_s0=r0_0*a_s(0.0,0.0,vs0)/(r0_0-1.0+1.0/τ)
    α=r0_0/M0/a_s0

    def ODE(y,t):
        S,I,A,mus,mub,C,vs,vb=y
        Cpvs=C+vs
        Cpvb=C+vb
        minus_αMA=-α*M(mus+mub,Cpvs+Cpvb)*A

        dS = minus_αMA*S
        dI = -dS - I
        dA= -a_s(mus,C,vs)*dS-A/τ
        dmus = minus_αMA*Cpvs
        dmub = minus_αMA*Cpvb
        dC = dmus*Cpvb
        dvs = dmus*Cpvs
        dvb = dmub*Cpvb
        return [dS, dI,dA, dmus,dmub,dC,dvs,dvb]

    A0=a_s0*I0
    y0=[1.0-I0,I0,A0,0.0,0.0,0.0,vs0,vb0]

    sol = odeint(ODE,y0, m_t).T
    S,I,A,mus,mub,C,vs,vb=sol
    dS=-ODE(sol,m_t)[0]
    return S,I,A,dS
```

```python
τ=0.4
vs0=0.5
vb0=0.6
r0_0=5.2
i0=0.0005
S,I,_,_= HCS_SIR(r0_0,i0,τ,vs0,vb0)

infected_threshold=0.01
infected_mask=I>infected_threshold
τ_E=t_step*np.sum(np.cumsum(infected_mask)==0)
infected=I[infected_mask]
τ_I=t_step*len(infected)
I_avr=np.mean(infected)
S_HCS=S# give distinct names to a variables
I_HCS=I

fig, ax = plt.subplots(dpi=120)

plt.plot(m_t, I,color='C3', label=f'$I$')
plt.vlines(x = [τ_E,τ_E+τ_I], ymin = 0, ymax = .2,
           colors = 'C2')
plt.hlines(y = I_avr, xmin = τ_E, xmax = τ_E+τ_I,
           colors = 'C1', label='$\mathcal{I}$')
plt.text(0.01, 0.07, "Exposed", rotation=90, verticalalignment='center')
plt.text(8.3, 0.05, "Recovered",  verticalalignment='center')
plt.text(2.3, 0.02, "Infected",  verticalalignment='center')
ax.set_ylim([0.0,.2])
ax.set_xlim([0.0,20])
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.01))
ax.set_ylabel(f'$I$ value')
plt.legend(loc='center right')
plt.xlabel(f'$t$')
ax.text(8,-.025, 'Fig.1 One step approximation of the city infectios curve', ha='center')
plt.title(f'HCS SIR dynamic $v_s(0)=${vs0},$v_b(0)=${vb0}, $r_0(0)=${r0_0}')

plt.show()
w=0.04
β=w*I_avr/i0
#print(f'β={β}')
```

Now we are ready for implememting country level SEIR model (CL SEIR).

```python
def CL_SEIR(I0,β,τ_E,τ_I):
    '''
    Counry level
    SEIR model
    '''
    def ODE(y,t):
        S,E,I=y
        dS = -β*I*S
        dE = -dS-E/τ_E
        dI = E/τ_E - I/τ_I
        return [dS, dE, dI]

    y0=[1.0-I0,0.0,I0]

    sol = odeint(ODE,y0, m_t).T
    S,E,I=sol
    dS=-ODE(sol,m_t)[0]
    R=1.0-S-E-I
    return S,E,I,R,dS
```

```python
I0=0.0001
S,E,I,R,dS= CL_SEIR(I0,β,τ_E,τ_I)
def show_SEIR(S,E,I,R,dS,cap):
    fig, ax = plt.subplots(dpi=120)
    plt.plot(m_t, S, color='C0', label=f'$S$')
    if not (E is None): plt.plot(m_t, E,color='C1', label=f'$E$')
    plt.plot(m_t, I,color='C3', label=f'$I$')
    plt.plot(m_t, R,color='C2', label=f'$R$')
    plt.plot(m_t, dS, color='C4', label=f'$|dS/dt|$')
    plt.xlabel(f'$t$')
    plt.legend(loc='center right')
    plt.title(cap)
    plt.show()
show_SEIR(S,E,I,R,dS,f'CL SEIR cities dynamic $β=${β}, $τ_E=${τ_E},$τ_I=${τ_I}')
```

The values S,E,I,R, we have got, reflects the cities dynamics inside the country, not the country population dinamics. To get the country population dinamics we need to combine the cities dynamics and inside city dynamics via convolution. And finally

```python
S_pop=1.0-np.convolve(dS,1.0-S_HCS, mode='full')[:n_steps+1]*t_step
I_pop=np.convolve(dS,I_HCS, mode='full')[:n_steps+1]*t_step
R_pop=1.0-S_pop-I_pop
def show_SIR_pop(S_pop,I_pop,R_pop,model_name):
    fig, ax = plt.subplots(dpi=120)
    plt.plot(m_t, I_pop,color='C3', label=f'$I$')
    ax.set_ylim([-0.05*0.2,1.05*0.2])
    ax.yaxis.set_major_locator(ticker.MultipleLocator(0.02))
    ax.set_ylabel(f'$I$ values')
    plt.legend(loc='center left')
    plt.xlabel(f'$t$')
    ax2 = ax.twinx()
    plt.plot(m_t, S_pop, color='C0', label=f'$S$')
    plt.plot(m_t, R_pop,color='C2', label=f'$R$')
    ax2.set_ylim([-0.05,1.05])
    ax2.yaxis.set_major_locator(ticker.MultipleLocator(0.1))
    ax2.set_ylabel(f'$S,R$ values')
    ax2.grid(False)

    plt.legend(loc='center right')
    plt.title(model_name+' population dynamics')
    plt.show()

show_SIR_pop(S_pop,I_pop,R_pop,'CL SEIR')

```

## Multi-step infection curve approximation
Above we actually used the city level infection dynamics being approximated by step functition to model the country dynamics. The step function was very simple. We can use more complex multi-step functions to approximate the city level infection dynamics. To  this end let's divide the timeline on $k$ jump points $t_1,...,t_k$ and assign values $\mathcal{I}_1,\mathcal{I}_2,...,\mathcal{I}_k,0$ to the intervals $[0,t_1),[t_1,t_2),...,[t_k,\infty)$ respectively getting multi-step function $\mathcal{I}(t)$. We want $\mathcal{I}(t)$ to be a good substitute for $I_{HCS}(t)$ - actual city level infection dynamics. We are choosing $\mathcal{I}(t)$ to get mimimum of the square of $L^2$ distance on the interval $[0,t_k]$:
$$ \lVert I_{HCS}-\mathcal{I}\rVert_{0,t_k}=\int_{0}^{t_k}(I_{HCS}(t)-\mathcal{I}(t))^2 dt$$

Jump points $t_1$ and $t_k$ are getting simply by use some small cutoff value $\epsilon_0$ to make $\mathcal{I}(t)$ zero where $I_{HCS}(t)$ is below the cutoff. Jump points $t_2,...,t_{k-1}$ are defined using the procedure DECIDE taken from the article https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.170.6471&rep=rep1&type=pdf . We combine the procedure DECIDE with $L^2$ square distance and 32 iterations of binary search to get  $t_2,...,t_{k-1}$ (it is not exactly minimal solution, but good enough, and we want to keep technical part simple).

```python
def get_partition(y,k,eps0):
    n=len(y)
    for f0 in range(n):
        if y[f0]>eps0:
            break
    for fn in range(n-1,-1,-1):
        if y[fn]>eps0:
            break
    n,k=fn,k-1
    def DECIDE(eps):
        f=f0
        res=[0,f0]
        for j in range(k):
            Sum=y[f]
            Sum2=Sum*Sum
            N=1
            for i in range(f+1,n):
                Sum+=y[i]
                Sum2+=y[i]*y[i]
                N += 1
                if Sum2-Sum*Sum/N>eps:
                    f=i
                    break
            else:
                res.append(n+1)
                return res
            res.append(f)
        return False
    lo=0.0
    hi=np.var(y)*len(y)
    found=[]
    for i in range(32):
        guess=0.5*(lo+hi)
        p=DECIDE(guess)
        if p:
            found=p
            hi=guess
        else:
            lo=guess
    return np.array(found)

```

Resulting step function looks following way (values $\mathcal{I}_1,\mathcal{I}_2,...,\mathcal{I}_k$ are just means of appropriate intervals) :

```python
k=12
p=get_partition(I_HCS,k,I_HCS[0]*4)
I_step=np.array([np.mean(I_HCS[p[i]:p[i+1]]) for i in range(k)]+[0.0])
fig, ax = plt.subplots(dpi=120)
plt.plot(m_t, I_HCS,color='C1', label=f'$I$')
plt.step(t_step*p,I_step,where='post',color='C2',label='$\mathcal{I}$')
ax.set_ylim([0.0,.2])
ax.set_xlim([0.0,modeling_period])
#ax.set_xlim([0.0,0.5])
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.01))
ax.set_ylabel(f'$I$ value')
plt.legend(loc='center right')
plt.xlabel(f'$t$')
plt.title(f'HCS SIR city dynamic and its step approximation')

plt.show()
```

Equations for the country level SIR model with step approximation of infectious curve (CL STEP SIR) are:
$$
\begin{align}
\frac{dS}{dt} & = -\frac{w}{i_0} S \sum_{i=1}^{k} \mathcal{I}_iI_i \\[3pt]
\frac{dI_1}{dt} & = - \frac{dS}{dt} - \frac{I_1}{\tau_1} \\[3pt]
\frac{dI_2}{dt} & = \frac{I_1}{\tau_1} - \frac{I_2}{\tau_2} \\[3pt]
 & ... \\[3pt]
R &=1-S-E-\sum_{i=1}^{k}I_i
\end{align}
$$
where $\tau_i=t_{i}-t_{i-1}$ and $t_0=0$,

$S,I,R$ - the portion of **the cities** in appropriate (multi) compartments.

```python
def CL_STEP_SIR(I0,β,k,S_HCS_SIR,I_HCS_SIR):
    '''
    Counry level
    model
    '''
    p=get_partition(I_HCS_SIR,k,I_HCS_SIR[0]*4.0)
    k=len(p)-1
    tau=t_step*(p[1:]-p[:-1])
    I_step=np.array([np.mean(I_HCS_SIR[p[i]:p[i+1]]) for i in range(k)])
    def ODE(y,t):
        S,I=y[0],y[1:]
        dS = -β*S*np.dot(I,I_step)
        outflow=I/tau
        inflow=np.roll(outflow,1)
        inflow[0]=-dS
        dI= inflow - outflow
        return np.concatenate(([dS],  dI))
    y0=np.zeros(k+1,dtype=np.float64)
    y0[0],y0[1],y0[2],y0[3]=1.0-I0,0.125*I0,0.75*I0,0.125*I0
    sol = odeint(ODE,y0, m_t).T

    S,I=sol[0],sol[1:].T
    I_pop=np.dot(I,I_step)
    dS=β*S*I_pop
    S_step=np.array([np.mean(S_HCS_SIR[p[i]:p[i+1]]) for i in range(k)])
    S_pop=S+np.dot(I,S_step)
    I=np.sum(I,axis=1)
    R=1.0-S-I
    S_pop+=S_HCS_SIR[-1]*R
    return S,I,R,dS,S_pop,I_pop

```

The procedure above calculates not only $S,I,R$ values for the cities but also $S_{pop}$ and $I_{pop}$ - appropriate country population values. On the other hand we can get $S_{pop}$ and $I_{pop}$ via convolution as previously. The difference between values obtained by different methods reflects the quality of step approximation and should vanish as number of steps $k$ increases.

```python
I0=0.0001
k=12
S,I,R,dS,S_pop1,I_pop1= CL_STEP_SIR(I0,w/i0,k,S_HCS,I_HCS)
show_SEIR(S,None,I,R,dS,f'CL STEP SIR cities dynamics for step approimation k={k}')

S_pop2=1.0-np.convolve(dS,1.0-S_HCS, mode='full')[:n_steps+1]*t_step
I_pop2=np.convolve(dS,I_HCS, mode='full')[:n_steps+1]*t_step

fig, ax = plt.subplots(dpi=120)
plt.plot(m_t, I_pop1,color='C3', label='$I_{pop}$ (step approimation)')
plt.plot(m_t, I_pop2,color='C4', label='$I_{pop}$ (convolution)')
ax.set_ylim([-0.05*0.2,1.05*0.2])
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.02))
ax.set_ylabel(f'$I$ values')
plt.legend(loc='center right')
plt.xlabel(f'$t$')
ax2 = ax.twinx()
plt.plot(m_t, S_pop1, color='C0', label='$S_{pop}$ (step approimation)')
plt.plot(m_t, S_pop2, color='C1', label='$S_{pop}$ (convolution)')
ax2.set_ylim([-0.05,1.05])
ax2.yaxis.set_major_locator(ticker.MultipleLocator(0.1))
ax2.set_ylabel(f'$S,R$ values')
ax2.grid(False)

plt.legend(loc='upper right')
ip='I_{pop}'
plt.title(f'CL STEP SIR population dynamics k={k}.\n  The differnce between ${ip}$ is {np.sum((I_pop1-I_pop2)**2)}')
plt.show()

```

So, by increasing $k$ we get more and more "continious" solution. We can get it other way around by solving the equation:

$$\frac{dS(t)}{dt}  = -\frac{w}{i_0} S(t) \int_{0}^{t}\frac{dS(t-\tau)}{dt} I_{HCS}(\tau)d\tau$$

Here the sum in right side becomes the integral. Then by using convolution as priviously we can get $S_{pop},I_{pop},R_{pop}$. Although this way the differential equation contains only one variable the equation is not ODE, but D(delayed)DE. Even simple DDE much harder to solve and there are no standard tools for that in scipy. Besides right side contains computationally intensive convolution.

## City size dependent heterogeneous model
We started from assumption that the number of people visiting a city is directly proportional to its size. Also, we have assumed that the social capacity to infect brought in by one host does not depend on the size of the city as all parameters defining spreading inside the city.

Yet, more realistic assumption is that number of people visiting a city is more than just proportional to its population size $z$ (in some countries, for example, only capitals have international airports so everyone travelling abroad have to visit the capitals) and, say, proportional to $z^{1+\theta}$. That makes large cities more vulnerable to infection, and we will get like SEIR model with probability for the city to get infected proportional to $z^\theta$. The parameters defining internal city spread dynamics $r_0$ and $\exp(v_s(0))-1$
>$v_s(0)$ is the variance of logarithms, see "1.2. Heterogeneity in spreading the disease" and explanations below

also influenced by the city size and, say, proportional to $z^{\theta_r}$ and $z^{\theta_v}$ respecively.

Many sources suggest $\theta$ value around 1/6.  We use following paper
__[Schläpfer M, Bettencourt L, Grauwin S, Raschke M, Claxton R, Smoreda Z, West G B and Ratti C.
The scaling of human interactions with city size (2013)](http://arxiv.org/abs/1210.5215)__  to pick up the value. The paper estimate $\theta$ belonging to the range [0.08,0.15]. We choose 0.1 as a value for $\theta$, $\theta_r$ and a half of that 0.05 as a value for $\theta_v$.
>Some additional considerations can be found here _A. J. Stier, M. G. Berman, and L. Bettencourt, Covid-19 attack rate increases with city size, arXiv preprint arXiv:2003.10376, (2020)_

It is known that the distribution of cities by population roughly corresponds to Zipf's law with exponent around 1. We use 1.07  as the exponent value. As continuous functions more preferable for the differential equations we will model the cities size distribution as a continuous random variable $Z$. To this means we use bounded __[Pareto distribution](https://en.wikipedia.org/wiki/Pareto_distribution)__  with shape parameter $\alpha\in (0,1)$ equal to $2-1.07=0.93$, support $[l,h]$, and PDF
$$
f(z)=\frac{\alpha}{l^{-\alpha}-h^{-\alpha}}z^{-1-\alpha}
$$

where l and h is the population size limits (as a part of the whole country population) for smallest and largest cities , for example 1.0e-6 and 0.05. If the total number of cities is $C$, then in the interval $[z,z+dz]$ it will be
$$C f(z) dz$$
cities with population
$$C z f(z) dz$$
For the entire interval $[l, h]$, we must get the entire population, which we consider equal to 1. So, having $l$ and $h$ chosen, the total number of cities $C$ must satisfy
$$
C\int_l^hzf(z)dz=C\frac{\alpha}{l^{-\alpha}-h^{-\alpha}}\frac{h^{1-\alpha}-l^{1-\alpha}}{1-\alpha}=1
$$

or
$$
C=\frac{1-\alpha}{\alpha}\frac{l^{-\alpha}-h^{-\alpha}}{h^{1-\alpha}-l^{1-\alpha}}=\frac{1-\alpha}{\alpha}\frac{h^{\alpha}-l^{\alpha}}{h\,l^{\alpha}-l\,h^{\alpha}}
$$

Let $s(z,t)$ is the portion (at moment t) of susceptible cities of size $z$. At the beginning it is (almost) equal to one for all $z$ and moves down to zero in time as more and more cities get infected. The same way we can define $e(z,t)$ and $i(z,t)$ - the proportions of exposed and infectious cities respectively. We also needs value describing a portion of infectios population for infectious cities of given size - $\mathcal{I}(z)$ so we consider one step approximation of the city infectios curve as in Fig.1.

Than we can write down equations:
$$
\begin{align}
\frac{\mathrm{d}s(z,t)}{\mathrm{d}t} & = -\frac{w}{i_0\mathbb{E}[z^\theta]} z^\theta s(z,t) I_p(t) \\[3pt]
\frac{\mathrm{d}e(z,t)}{\mathrm{d}t} & = - \frac{\mathrm{d}s(z,t)}{\mathrm{d}t} - \frac{e(z,t)}{\tau_e(z)} \\[3pt]
\frac{\mathrm{d}i(z,t)}{\mathrm{d}t} & = \frac{e(z,t)}{\tau_e(z)} - \frac{i(z,t)}{\tau_i(z)} \\[3pt]
\end{align}
$$

Here $I_p(t)=C\,\mathbb{E}[z\, i(z,t)\mathcal{I}(z)]$ - the portion of infectious population as the part of the whole country population and $\mathbb{E}[g(z,...)]$ is expectation
$$
\mathbb{E}[g(z,...)]=\int_{l}^{h} g(z,...) f(z)\mathrm{d}z=\frac{\alpha}{l^{-\alpha}-h^{-\alpha}}\int_{l}^{h} g(z,...) z^{-1-\alpha}\mathrm{d}z
$$

Indeed, there are
$$dI=C\, i(z,t) f(z) dz$$
infectious cities in the interval $[z,z+dz]$ having population
$$dp=z\, dI=C\, z\, i(z,t) f(z) dz$$
and part $\mathcal{I}(z)$ of $dp$ is infectious. So
$$dI_p=\mathcal{I}(z) dp=C\, z\, i(z,t) \mathcal{I}(z) f(z) dz$$
integrating over $[l,h]$ gives
$$I_p=\int_{l}^{h}dI_p=C \int_{l}^{h}z\, i(z,t) \mathcal{I}(z) f(z) dz=C\,\mathbb{E}[z\, i(z,t)\mathcal{I}(z)]$$

For $s(z,t)$ we use the same trick as previosely. Let's represent $s(z,t)$ following way
$$s(z,t)=\exp(-z^\theta u(t))$$
Denoting $w/(i_0\mathbb{E}[z^\theta])$ as $\beta^*$ we get first equation without $z$

$$\frac{\mathrm{d}u(t)}{\mathrm{d}t} = \beta^* I_p(t)$$
Than second equation becomes:
$$\frac{\mathrm{d}e(z,t)}{\mathrm{d}t}  = \beta^*z^\theta\exp(-z^\theta u(t))I_p(t) - \frac{e(z,t)}{\tau_e(z)}$$
The third equation stays unchanged.

To solve the equations we need to find $\mathbb{E}[z\, i(z,t)\mathcal{I}(z)]$ at each right side estimation by calculating the integral
$$\int_{l}^{h}z\, i(z,t) \mathcal{I}(z) f(z) dz$$
using numerical integration techniques.

If we need to integrate a function $g(z)$ over $[l,h]$ the numerical integration techniques involve appropriate defining a set of nodes $z_1,...,z_N \in [l,h]$ and weights $w_1,...,w_N$ and calculatng the integral
$$\int_{l}^{h}g(z) dz \approx \sum_1^Nw_ig(z_i)$$

>Also, well-turned integration variable substitution $z=z(v)$ can greatly increase the accuracy of the numerical integration and we use sunstitution $z=v^\phi$ for $\phi=5$.

To get nodes and weights we use subdivision of $[l,h]$ into $n$ equal intervals and $d$ Gauss–Legendre quadrature nodes and weights on each of the interval, so $N=n\cdot d$.

```python
def get_nodes_and_weights(l,h,n,d):
    x,w=scsp.roots_sh_legendre(d)
    L=(h-l)/n
    return n*d,L*(np.tile(x+l/L,n)+np.repeat(range(n),d)),L*np.tile(w,n)
```

After replacing the integral by finite sum we get the equations:
$$
\begin{align}
\frac{\mathrm{d}u(t)}{\mathrm{d}t} &=  \frac{\beta^*(1-\alpha)}{h^{1-\alpha}-l^{1-\alpha}}\sum_1^N w_i z_i^{-\alpha}\, i(z_i,t)\mathcal{I}(z_i)\\[3pt]
\frac{\mathrm{d}e(z_i,t)}{\mathrm{d}t} & = - \frac{\mathrm{d}u(t)}{\mathrm{d}t}z_i^\theta\exp(-z_i^\theta u(t)) - \frac{e(z_i,t)}{\tau_e(z_i)} \\[3pt]
\frac{\mathrm{d}i(z_i,t)}{\mathrm{d}t} & = \frac{e(z_i,t)}{\tau_e(z_i)} - \frac{i(z_i,t)}{\tau_i(z_i)} \\[3pt]
\end{align}
$$

$$i=1..N$$

We also need the functions to transform values of $u(t)$ into
$$S(t)=\int_{l}^{h}s(z,t)f(z) dz=\int_{l}^{h}\exp(-z^\theta u(t))f(z) dz$$
and back from $S(0)$ (more precisely from $I(0)$, $S(0)=1-I(0)$) into $u(0)$

```python
def get_S(u,l,h,theta):
    L,H,d=1.0/l,1.0/h,alpha-1.0 #variable subztitution z=v^{-1}
    def F(u):
        f=lambda v:np.exp(-u*v**-theta)*v**d
        return scint.quad(f,H,L,epsabs=1.0e-8, epsrel=1.0e-9)[0]
    return (alpha/(l**(-alpha)-h**(-alpha)))* np.vectorize(F,otypes=[np.float64])(u)

def get_u0(I0,l,h,theta,guess=0.1):
    L,H,d=1.0/l,1.0/h,alpha-1.0 #variable subztitution z=v^{-1}
    C=(I0-1)*(l**(-alpha)-h**(-alpha))/alpha
    d=alpha-1.0
    def cost_f(u):
        f=lambda v:np.exp(-u*v**-theta)*v**d
        return C+scint.quad(f,H,L,epsabs=1.0e-9, epsrel=1.0e-10)[0]
    m=scop.root(cost_f,guess,tol= 2.0e-09)
    return np.atleast_1d(m.x)[0]
```

Following class incapsulate a city internal spread dynamics and helps us to get the dynamics for given city size $z$ (controlled by $r_0(0)(z)$ and $v_s(0)(z)$ as well as $1/\tau_E(z)$, $1/\tau_I(z)$, and $\mathcal{I}(z)$).

```python
def z_pow_theta_expectation(alpha,l,h,theta):
    return alpha*(h**alpha*l**theta-l**alpha*h**theta)/((alpha-theta)*(h**alpha-l**alpha))

class incity_dynamics:

    def __init__(self,r0_0,i0,τ,vs0,vb0,infected_threshold,l,h,alpha,theta_r,theta_v):
        self.r0_0,self.i0,self.τ,self.vb0=r0_0,i0,τ,vb0
        self.infected_threshold=infected_threshold
        self.l,self.h=l,h
        self.alpha,self.theta_r,self.theta_v=alpha,theta_r,theta_v
        self.vs_coef=(np.exp(vs0)-1.0)/z_pow_theta_expectation(alpha,l,h,theta_v)

    def get_r0(self,z): return self.r0_0*(z/self.h)**self.theta_r
    def get_vs0(self,z): return np.log(1.0+self.vs_coef*z**self.theta_v)

    def z_pow_theta_expectation(self,theta):
        return z_pow_theta_expectation(self.alpha,self.l,self.h,theta)

    def get_spread_curves(self,Z):
        def F(z):
            S,I,A,_= HCS_SIR(self.get_r0(z),self.i0,self.τ,self.get_vs0(z),self.vb0)
            return S,I,A
        return np.vectorize(F,signature='()->(n),(n),(n)')(Z)

    def get_spread_params(self,I):
        infected_mask=np.cumsum(I>self.infected_threshold,axis=1)
        i_s=infected_mask[:,-1]
        τ_E=np.maximum(t_step*np.sum(infected_mask==0,axis=1),0.001)
        τ_I=np.maximum(t_step*i_s,0.001)
        loc = i_s>0
        I_avr = np.zeros_like(i_s, dtype=np.float64)
        I_avr[loc] =np.sum(I*(I>self.infected_threshold),axis=1)[loc]/i_s[loc]
        return np.where(loc,1.0/τ_E,1000.0),1.0/τ_I,I_avr

```

>Previously (due to the city population heterogeneity), internal city spread dynamics made $r_0$ time dependent (i.e. $r_0(t)$) and the dynamics is controlled by $r_0$ initial value $r_0(0)$. Now, $r_0(0)$ become dependent from the city size $z$ too. Above we used kind of ugly notation $r_0(0)(z)$ for that. From now on we denote $r0$ as a function of $z$ and $t$ - $r_0(z,t)$ so $r_0(0)(z)$ in new notation becomes $r_0(z,0)$.

The method defining $r_0(z,0)$ for given city size $z$ is folowing.

We want
$$r_0(z,0) = c_rz^{\theta_r}$$
and we need to choose $c_r$

We knew initial infectious dynamics was exponent with argument $(R_0-1)t$ and we estimate corresponding $R_0$ as 5.2 (r0_0 in the code). It is wise to treat initial spread dynamics as caused by infection of major cities so we get $c_r$ from the equation
$$R_0 =r_0(h,0)= c_r h^{\theta_r}$$
so
$$c_r =R_0/h^{\theta_r}$$
or
$$r_0(z,0) = R_0(z/h)^{\theta_r}$$

Let's look at the $v_s(z,0)$ dependence. We described heterogeneity in 'social' ability to catch and to spread the disease of the city susceptible population as log normally distributed variable:
$$H_s(z,t) \sim LN(\mu_s(z,t),v_s(z,t))$$

its variance are given by
$$V_s(z,t)=(\exp(v_s(z,t))-1)\exp(2\mu_s(z,t)+v_s(z,t))=(\exp(v_s(z,t))-1)M^2(z,t)$$

where $M(z,t)=\exp(\mu_s(z,t)+0.5v_s(z,t))$ - expectation $H_s$.

Suppose, the variance $V_s(z,0)$ growing as $z^{\theta_v^*}$

$$V_s(z,0) \propto z^{\theta_v^*}$$

Important observation is that $r_0(z,0) \propto M(z,0)$ because it is proportional to the average ability of catching and spreading the disease. From
$$r_0(z,0) \propto z^{\theta_r}$$
follows
$$M(z,0) \propto z^{\theta_r}$$
and
$$V_s(z,0)=(\exp(v_s(z,t))-1)M^2(z,0)\propto (\exp(v_s(z,t))-1)z^{2\theta_r} \propto z^{\theta_v^*}$$
finally
$$\exp(v_s(z,0))-1 \propto z^{\theta_v^*-2\theta_r}$$

Let $\theta_v=\theta_v^*-2\theta_r$ (this value can be negative!!!) than
$$\exp(v_s(z,0))-1 \propto z^{\theta_v}$$
or
$$\exp(v_s(z,0))-1 = c_vz^{\theta_v}$$

We require
$$\mathbb{E}[\exp(v_s(z,0))-1]=\exp(v_s(0))-1$$

where $v_s(0)$ is $z$-independent value used previously to describe 'social' heterogeneity (vs0 in the code). That give us $c_v$
$$c_v=\frac{\exp(v_s(0))-1}{\mathbb{E}[z^{\theta_v}]}$$
finally
$$v_s(z,0)=\ln(1+ c_vz^{\theta_v})$$

Everything ready for country level heterogeneous SEIR model (CL H SEIR).

```python
def CL_H_SEIR(incity_dyn,I0,β,l,h,n,d,theta):
    phi=5.0 #substitute integration variable v=z^{1/phi} or z=v^{phi}, phi can be negative
    L,H=l**(1.0/phi),h**(1.0/phi)
    N,V,W=get_nodes_and_weights(min([L,H]),max([L,H]),n,d)
    Z=V**phi
    alpha=incity_dyn.alpha
    Z_pow_theta=Z**theta
    Z_pow_alpha=Z**alpha

    s_city,i_city,_=incity_dyn.get_spread_curves(Z)
    inv_tau_e,inv_tau_i,i_avr=incity_dyn.get_spread_params(i_city)

    #weights for numerical integration of a function g(z,...)
    W0=abs(phi)*W/V/Z_pow_alpha
    W1=(alpha/(l**(-alpha)-h**(-alpha)))*W0 #to get E[g(z,...)]
    W2=((1.0-alpha)/(h**(1.0-alpha)-l**(1.0-alpha)))*W0*Z #to get C*E[z*g(z,...)]
    W3=i_avr*W2 #to get C*E[I(z)*z*g(z,...)]

    beta=β/incity_dyn.z_pow_theta_expectation(theta)

    def ODE(y,t):
        u,e,i=y[0],y[1:N+1],y[N+1:]
        I_pop=np.dot(i,W3)
        du=beta*I_pop
        e_out=e*inv_tau_e
        de=du*Z_pow_theta*np.exp(-Z_pow_theta*u)-e_out
        di=e_out-i*inv_tau_i
        return np.concatenate(([du], de, di))

    y0=np.zeros(1+2*N,dtype=np.float64)
    y0[0]=get_u0(I0,alpha,l,h,theta)
    y0[N+1:]=1.0-np.exp(-Z_pow_theta*y0[0]) #i(z_i,0)=1-s(z_i,0)

    sol = odeint(ODE,y0, m_t).T

    u,e_country,i_country=sol[0],sol[1:N+1],sol[N+1:]
    du=beta*np.dot(W3,i_country)
    s_country=np.exp(-np.outer(Z_pow_theta,u))
    ds_country=du*(Z_pow_theta*s_country.T).T

    E=np.dot(W1,e_country)
    I=np.dot(W1,i_country)
    S=np.dot(W1,s_country)
    R=1.0-S-E-I
    dS=np.dot(W1,ds_country)

    L=len(m_t)
    one_minus_s_pop=np.zeros([N,L])
    i_pop=np.zeros([N,L])

    for n in range(N):
        i_pop[n]=np.convolve(ds_country[n],i_city[n], mode='full')[:L]*t_step
        one_minus_s_pop[n]=np.convolve(ds_country[n],1.0-s_city[n], mode='full')[:L]*t_step

    S_pop=1.0-np.dot(one_minus_s_pop.T,W2)
    I_pop=np.dot(i_pop.T,W2)
    R_pop=1.0-S_pop-I_pop

    return S,E,I,R,dS,S_pop,I_pop,R_pop

```

The procedure above calculates not only $S,E,I,R$ and $dS/dt$ values indicating appropriate portions of cities but also $S_p,I_p,R_p$ values for the population portions. To do this we use convolution
$$s_p(z,t)=1-(\frac{ds_{country}(z)}{dt}*(1-s_{city}(z)))(t)$$
$$i_p(z,t)=(\frac{ds_{country}(z)}{dt}*i_{city}(z))(t)$$
where $(f*g)(t)$ is convolution
$$(f*g)(t)=\int_0^tf(\tau)g(t-\tau)d\tau$$
and then integrate over $z$
$$S_p=C\int_l^hs_p(z,t)zf(z)dz \approx  \frac{(1-\alpha)}{h^{1-\alpha}-l^{1-\alpha}}\sum_1^N \frac{w_i s_p(z_i,t)}{z_i^\alpha}.$$
Let me remind you that $$C\, zf(z)=\frac{1-\alpha}{z^\alpha(h^{1-\alpha}-l^{1-\alpha})}$$
And for $I_p$
$$I_p=C\int_l^hi_p(z,t)zf(z)dz \approx \frac{(1-\alpha)}{h^{1-\alpha}-l^{1-\alpha}}\sum_1^N \frac{w_i i_p(z_i,t)}{z_i^\alpha}$$

Let's take a look on the results

```python
theta=0.1
theta_r=theta
theta_v=theta/2
alpha=0.93
l=1.0e-6
h=0.05
infected_threshold=i0*4.0
icd=incity_dynamics(r0_0,i0,τ,vs0,vb0,infected_threshold,l,h,alpha,theta_r,theta_v)
S,E,I,R,dS,S_pop,I_pop,R_pop=CL_H_SEIR(icd,I0,w/i0,l,h,5,20,theta)
show_SEIR(S,E,I,R,dS,f'CL Heterogeneous SEIR cities dynamic')
```

```python
show_SIR_pop(S_pop,I_pop,R_pop, 'CL H SEIR')
```

Definitely, it is possible to combine country level heterogeneous SEIR model (CL H SEIR) and country level SIR model with step approximation of infectious curve (CL STEP SIR), but it is just technical task.

## Taking into account the spread activity

In HCS SIR model for city level spread dynamics we proposed to take into account a 'social' ability to spread the disease and introduced $A$ctivity variable for this purpose. To incorporate it our country level model we need just replace $I$ curve on $A$ curve when defining $1/\tau_E(z)$, $1/\tau_I(z)$, and $\mathcal{I}(z)$ (to be precise it will be $1/\tau_E(z)$, $1/\tau_I(z)$, and $\mathcal{A}(z))$. And the first equation becomes

$$
\frac{\mathrm{d}u(t)}{\mathrm{d}t} =  \frac{\beta^*(1-\alpha)}{h^{1-\alpha}-l^{1-\alpha}}\sum_1^N w_i z_i^{-\alpha}\, i(z_i,t)\mathcal{A}(z_i)
$$

Here $\mathcal{A}(z_i)$  is average value of $A$ curve of 'infectious' interval — where the curve is above the threshold. I call this model CL HS SEIR.

```python
def CL_HS_SEIR(incity_dyn,I0,β,l,h,n,d,theta):
    phi=5.0 #substitute integration variable v=z^{1/phi} or z=v^{phi}, phi can be negative
    L,H=l**(1.0/phi),h**(1.0/phi)
    N,V,W=get_nodes_and_weights(min([L,H]),max([L,H]),n,d)
    Z=V**phi
    alpha=incity_dyn.alpha
    Z_pow_theta=Z**theta
    Z_pow_alpha=Z**alpha

    s_city,i_city,a_city=incity_dyn.get_spread_curves(Z)
    #!!!CHANGES HERE i_city -> a_city, i_avr -> a_avr
    inv_tau_e,inv_tau_i,a_avr=incity_dyn.get_spread_params(a_city)

    #weights for numerical integration of a function g(z,...)
    W0=abs(phi)*W/V/Z_pow_alpha
    W1=(alpha/(l**(-alpha)-h**(-alpha)))*W0 #to get E[g(z,...)]
    W2=((1.0-alpha)/(h**(1.0-alpha)-l**(1.0-alpha)))*W0*Z #to get C*E[z*g(z,...)]
    W3=a_avr*W2 #to get C*E[I(z)*z*g(z,...)]

    beta=β/incity_dyn.z_pow_theta_expectation(theta)

    def ODE(y,t):
        u,e,i=y[0],y[1:N+1],y[N+1:]
        I_pop=np.dot(i,W3)
        du=beta*I_pop
        e_out=e*inv_tau_e
        de=du*Z_pow_theta*np.exp(-Z_pow_theta*u)-e_out
        di=e_out-i*inv_tau_i
        return np.concatenate(([du], de, di))

    y0=np.zeros(1+2*N,dtype=np.float64)
    y0[0]=get_u0(I0,l,h,theta)
    y0[N+1:]=1.0-np.exp(-Z_pow_theta*y0[0]) #i(z_i,0)=1-s(z_i,0)

    sol = odeint(ODE,y0, m_t).T
    u,e,i_country=sol[0],sol[1:N+1],sol[N+1:]
    du=beta*np.dot(W3,i_country)
    s_country=np.exp(-np.outer(Z_pow_theta,u))
    ds_country=du*(Z_pow_theta*s_country.T).T

    E=np.dot(W1,e)
    I=np.dot(W1,i_country)
    S=np.dot(W1,s_country)
    R=1.0-S-E-I
    dS=np.dot(W1,ds_country)

    L=len(m_t)
    one_minus_s_pop=np.zeros([N,L])
    i_pop=np.zeros([N,L])

    for n in range(N):
        i_pop[n]=np.convolve(ds_country[n],i_city[n], mode='full')[:L]*t_step
        one_minus_s_pop[n]=np.convolve(ds_country[n],1.0-s_city[n], mode='full')[:L]*t_step

    S_pop=1.0-np.dot(one_minus_s_pop.T,W2)
    I_pop=np.dot(i_pop.T,W2)
    R_pop=1.0-S_pop-I_pop

    return S,E,I,R,dS,S_pop,I_pop,R_pop

```

```python
S,E,I,R,dS,S_pop,I_pop,R_pop=CL_HS_SEIR(icd,I0,w/i0,l,h,5,20,theta)
show_SEIR(S,E,I,R,dS,f'CL HS SEIR cities dynamic')
```

```python
show_SIR_pop(S_pop,I_pop,R_pop,'CL HS SEIR')
```


# Oil Viscosity

The viscosity of an oil can factor into many calculations that are needed in the response of a spill, such as spreading and dispersion.

## Importing and Estimating Initial Values

In the PyGnome oil library, kinematic viscosity is primarily used, although the imported oil records sometimes do contain dynamic viscosity measurements.

We would like to maintain as much of the useful imported information as we can.  So when we do encounter imported records with dynamic viscosities, we convert them to kinematic for use with the oil library.

The conversion from dynamic viscosity to kinematic viscosity is:

$$
\begin{align}
\eta0_{oil} &= \text{the dynamic viscosity of our oil in } Pa \cdot s \left( \text{aka } {kg \over m \cdot s} \right) \cr
\rho0_{oil} &= \text{the density of our oil in } kg/m^3 \cr
v0_{oil} &= {\eta0_{oil} \over \rho0_{oil} } = \text{the kinematic viscosity of our oil in } m^2 / s
\end{align}
$$

### Some Examples

Ok, let's apply this to some examples using records in our oil library.

First we will import some things from our oil library that we will need.

```python
import numpy as np
from scipy.optimize import curve_fit

from pylab import *

%matplotlib inline

from oil_library import get_oil, get_oil_props, _get_db_session
from oil_library.utilities.estimations import pour_point_from_kvis
from oil_library.models import (Base, ImportedRecord, Oil,
                                Density, KVis, DVis,
                                Toxicity, Category)
from oil_library.oil_props import OilProps

session = _get_db_session()
```

Now we can query for an oil that we are interested in.

#### Example 1:  Estimating Kinematic Viscosity

For this example, we will examine an oil that has no kinematic viscosities, but does in fact have a measured dynamic viscosity.

```python
# query all imported records that have DVis but no KVis
recs = (session.query(ImportedRecord)
        .join(DVis)
        .outerjoin(KVis)
        .filter(KVis.imported_record_id == None)
        .all())
print len(recs)

# query all Oils associated with our imported records.
oils = [r.oil for r in recs if r.oil is not None]
print len(oils)

# now let's choose one that matches our example precisely
oil_obj = [o for o in oils
           if (len(o.imported.densities) == 0)][0]
print oil_obj
print oil_obj.imported.dvis
print oil_obj.imported.kvis
```

    128
    126
    <Oil("KUWAIT EXPORT")>
    [<DVis(0.0581 kg/ms at 310.9278K)>]
    []

The imported record for this oil contains a dynamic viscosity, but no kinematic viscosity.  So we will need to convert it to a kinematic viscosity using the method above.

So in order to do that, we need to get the density for this oil.

```python
print oil_obj.imported.densities
print oil_obj.imported.api
```

    []
    31.4

Alright, you can see that the imported record for this oil also has no densities, but it does have an API.  So of course we will need to convert the API to a density at $ 15^\circ C $.

Fortunately, the oil object already has this estimated.

(For details on this, please see the Oil density discussion )

```python
print oil_obj.densities
```

    [<Density(868.631062001 kg/m^3 at 288.15K)>]

Now, is this the density value we want?  Not really, because we would really like to know what the oil density is at $ 310.93^\circ K $

For a full discussion of calculating the oil density at a temperature, please see the Oil Density notebook document.  But for our purposes, we will use the convenience function density_at_temp() located in the PyGnome oil_library module.

It takes a reference density, a reference temperature, and an optional expansion coefficient.

There is a default expansion coefficient of 0.0008 used by this utility function.  This is a reasonable average estimate, but in this case we want to verify that the oil's viscosity is close to what we would expect.  So we will also get the expansion coefficient that is used by the record.

```python
from oil_library.utilities.estimations import density_at_temp
from oil_library.imported_record.estimations import ImportedRecordWithEstimation

# we need a good expansion coefficient from our imported record
imp_rec = ImportedRecordWithEstimation(oil_obj.imported)
expansion_coeff = imp_rec._vol_expansion_coeff(imp_rec.get_densities(), 310.9278)
print 'Expansion Coefficient: ', expansion_coeff

density_at_311k = density_at_temp(oil_obj.densities[0].kg_m_3,
                                  oil_obj.densities[0].ref_temp_k,
                                  310.9278,
                                  expansion_coeff)
print 'density at 311K: ', density_at_311k

```

    Expansion Coefficient:  0.0009
    density at 311K:  851.181817714

Alright, now we have the dynamic viscosity and the density.  Now we can calculate the kinematic viscosity at $ 310.93^\circ K $

```python
dvis = oil_obj.imported.dvis[0]
dvis.kg_ms
kinematic_viscosity_at_311k = dvis.kg_ms / density_at_311k
print kinematic_viscosity_at_311k
```

    6.82580369914e-05

Ok, let's see if that is what was estimated for our oil.

```python
print oil_obj.kvis
```

    [<KVis(6.82580369914e-05 m^2/s at 310.9278K)>]

Sure enough, the oil object has a kinematic value that matches our work.

#### Example 2:  Dynamic Viscosities must be at unique temperatures.

```python
oil_obj = (session.query(Oil)
           .filter(Oil.name == 'DESTIN DOME CIS, MMS')
           .one())
print oil_obj
print oil_obj.imported.dvis
print oil_obj.imported.kvis
```

    <Oil("DESTIN DOME CIS, MMS")>
    [<DVis(0.00274 kg/ms at 313.15K)>]
    [<KVis(3.16e-06 m^2/s at 313.15K)>]

This record has both a dynamic and kinematic density at $ 313^\circ K $.

If an oil record has both a kinematic and dynamic viscosity at a particular temperature, we will prefer the kinematic.

```python
print oil_obj.kvis
```

    [<KVis(3.16e-06 m^2/s at 313.15K)>]

However any dynamic viscosities that exist at unique temperatures will be converted to kinematic and used.

```python
oil_obj = (session.query(Oil)
           .filter(Oil.name == 'ROSSIIELF, RUSSIAN JOINT STOCK CO')
           .one())
print oil_obj
print oil_obj.imported.dvis
print oil_obj.imported.kvis
```

    <Oil("ROSSIIELF, RUSSIAN JOINT STOCK CO")>
    [<DVis(0.12 kg/ms at 273.15K)>]
    [<KVis(1.957e-05 m^2/s at 313.15K)>, <KVis(1.395e-05 m^2/s at 323.15K)>]

This oil has a couple of kinematic viscosities, but also has a dynamic viscosity at a temperature that does not match any of the kinematic viscosities.

Here we will convert the dynamic viscosity and add it to the list of kinematic viscosities.

```python
print oil_obj.kvis
```

    [<KVis(0.000129914529915 m^2/s at 273.15K)>, <KVis(1.957e-05 m^2/s at 313.15K)>, <KVis(1.395e-05 m^2/s at 323.15K)>]

## Finding Oil Viscosity at Temperature

So now that we have initial measured viscosities and their respective reference temperatures, we will need to estimate the oil's viscosity at a certain temperature or temperatures in order to provide reasonable viscosity values for a PyGnome model.

Estimating Viscosity at a certain temperature is done using the following formula:

$$
\begin{align}
v_{ref} &= \text{A measured reference viscosity} \cr
T_{ref} &= \text{The temperature at which the reference viscosity was measured} \cr
T_{pp} &= \text{The pour point (the point at which the substance stops behaving like a fluid)} \cr
T &= \text{The temperature at which we would like to compute the viscosity} \cr
k_{v2} &= \text{The Viscosity Temperature Parameter} \cr
\cr
v_{max} &= v_{ref} exp \left( k_{v2} \cdot \left[ \frac 1{T_{pp}} - \frac 1{T_{ref}} \right] \right) \cr
v_0 &= \left[ v_{ref} exp \left( k_{v2} \cdot \left[ \frac 1T - \frac 1{T_{ref}} \right] \right) \right]^{\bot v_{max}}_{ \top 0} \cr
\end{align}
$$

Alright, most of the parameters above are self-explanatory, and coming up with reasonable values for them is fairly straightforward.

But what is a reasonable value for $ k_{v2} $?

Well, Payne et al (1982) recommend that $ k_{v2} = 9000^\circ K $.  But Adios 2, based on the results of several oil measurements recorded in the Environment Canada database, instead uses $ k_{v2} = 5000^\circ K $.

```python
def kvis_at_temp(ref_kvis, ref_temp_k, temp_k):
    k_v2 = 5000.0

    return ref_kvis * np.exp(k_v2 / temp_k - k_v2 / ref_temp_k)

```

The formula above has been packaged as a function in the oil library.  Here is its import location

```python
from oil_library.utilities.estimations import kvis_at_temp
```

In addition, it has been packaged as a method of oil objects.

### Some Examples

Here we will get an oil with a number of measured kinematic viscosities and make a graph of the interpolated viscosities as a function of temperature.

Alright, first let's get our oil.

```python
oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD02448').one()
print oil_obj
```

    <Oil("LUCKENBACH FUEL OIL")>

Ok, now we need to determine a good temperature range that we can use to graph our viscosities.

```python
oil_obj.kvis
```

    [<KVis(0.245 m^2/s at 288.7056K)>,
     <KVis(0.0641 m^2/s at 294.2611K)>,
     <KVis(0.0225 m^2/s at 299.8167K)>,
     <KVis(0.00458 m^2/s at 310.9278K)>]

```python
pour_point_from_kvis(oil_obj.kvis[0].m_2_s,
                     oil_obj.kvis[0].ref_temp_k)
```

    267.02016700827045

It seems that a range from $ 250^\cdot K - 350^\cdot K $ will work.

```python
plt.figure(1, figsize=(12,8))
ax = plt.subplot(111)

#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD02448').one()
oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD04009').one()
#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD00009').one()
imp_rec = ImportedRecordWithEstimation(oil_obj.imported)

temps = np.linspace(250.0, 350.0, 200)
kvis_ref_temps = [v.ref_temp_k for v in oil_obj.kvis]
kvis_refs = [v.m_2_s * 1e6 for v in oil_obj.kvis]
for v, t in zip(kvis_refs, kvis_ref_temps):
    print (v, t)

pour_point = pour_point_from_kvis(oil_obj.kvis[0].m_2_s,
                                  oil_obj.kvis[0].ref_temp_k)

x = temps
y = imp_rec.kvis_at_temp(temps) * 1e6

print imp_rec.kvis_at_temp(273.15 + 38)

graph_max = np.max(y) * 10
graph_min = np.min(y) / 10

plot(x, y, linewidth=1)
axis([np.min(temps), np.max(temps), graph_min, graph_max])
ax.axvline(273.15, color='g', ls='--')
ax.axhline(0, color='g', ls='--')

ax.set_title('{0} Viscosity/Temperature'.format(oil_obj.name))
figtext(-0.02, 0.5, 'cSt')

# annotations
for v, t in zip(kvis_refs, kvis_ref_temps):
    ax.plot(t, v, marker='o', color="green")
    ax.annotate('{0} cSt'.format(v),
                xy=(t, v),  xycoords='data',
                xytext=(0, 30), textcoords='offset points',
                arrowprops=dict(arrowstyle="->")
                )

# generate x ticks
kvis_ref_ticks = [(v, r'$T{0}={1:.1f}^\circ K$                '.format(i, v))
                  for i, v in enumerate(kvis_ref_temps)]
x_ticks = [(v, r'${0:.1f}^\circ K$'.format(v)) for v, l in zip(*plt.xticks())]
x_ticks += kvis_ref_ticks
x_ticks += [(273.15, r'$0^\circ C={0:.1f}^\circ K$'.format(273.15))]
x_ticks += [(pour_point, r'$PourPoint = {0:.1f}^\circ K$'.format(pour_point))]
plt.xticks(*zip(*x_ticks))
plt.yscale('log', nonposy='clip')

for tick in ax.xaxis.get_major_ticks():
    tick.label.set_fontsize(10)
    # specify integer or one of preset strings, e.g.
    #tick.label.set_fontsize('x-small')
    tick.label.set_rotation('vertical')

show()
```

It seems visually intuitive from looking at the above graph that the rate at which the kinematic viscosity decreases with temperature, a rate that is calculated using the above formula, does not in fact match our data points.  It seems that we may need to implement something that will adjust the rate somehow.

First we parameterize $k_{v2}$ for the kvis-to-temperature formula

```python
def kvis_at_temp(temp_k, ref_kvis, ref_temp_k, k_v2):
    return ref_kvis * np.exp(k_v2 / temp_k - k_v2 / ref_temp_k)
```

We need a form of the function that we can use to perform a curve optimization to determine $k_{v2}$.
We can derive this form by compartmentalizing parts of the equation that are not affected by a change in temperature.
This derivation is as follows:

$$
\begin{align}
v_0 &= v_{ref} exp \left( \frac {k_{v2}} T - \frac {k_{v2}} {T_{ref}} \right) \cr
v_0 &= \left( v_{ref} \cdot e^{-k_{v2} / T_{ref}} \right) \cdot e^{k_{v2} / T} \cr
v_0 &= a \cdot exp \left( \frac {k_{v2}} T \right) \cr
\end{align}
$$

Here we are interested not so much in the coefficient $a$, but the exponential coefficient $k_{v2}$

```python
plt.figure(1, figsize=(12,8))
ax = plt.subplot(111)

# This oil has an exponential increase with temperature.  Why??
#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD00311').one()

# These oils show a flat viscosity response to temperature.  Why??
#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD02548').one()
#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD02552').one()

oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD02448').one()
#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD04009').one()
#oil_obj = session.query(Oil).filter(Oil.adios_oil_id == 'AD00009').one()

temps = np.linspace(250.0, 320.0, 200)
kvis_ref_temps = np.array([v.ref_temp_k for v in oil_obj.kvis])
kvis_refs = np.array([v.m_2_s for v in oil_obj.kvis])
kvis_weathering = np.array([v.weathering for v in oil_obj.kvis])
pour_point = pour_point_from_kvis(oil_obj.kvis[0].m_2_s,
                                  oil_obj.kvis[0].ref_temp_k)

for v, t, w in zip(kvis_refs, kvis_ref_temps, kvis_weathering):
    print (v, t, w)

# This works pretty well for IFO 180
# - initial a = 0.01
# - initial k_v2 = 6000
def exp_func(temp_k, a, k_v2):
    return a * np.exp(k_v2 / temp_k)

for k in np.logspace(3.3, 4.5, num=8):
    # log range from about 5000-32000
    a_coeff = kvis_refs[0] * np.exp(-k / kvis_ref_temps[0])
    try:
        print 'trying (k_v2, a) = ', (k, a_coeff)
        popt, pcov = curve_fit(exp_func, kvis_ref_temps, kvis_refs,
                               p0=(a_coeff, k), maxfev = 2000)

        std_dev = np.sqrt(np.diag(pcov))
        print '\tpopt: ', popt
        print '\tpcov: ', pcov
        print '\tk_v2 deviation: ', (std_dev/popt)[1]

        # - we want our covariance to be a reasonably small number,
        #   but it can get into the thousands even for a good fit.
        #   So we will only check for inf values.
        # - for sample sizes < 3, the covariance is unreliable.
        if len(kvis_refs) > 2 and np.any(pcov == np.inf):
            print '\tcovariance too high: {}', pcov
            continue

        break
    except (ValueError, RuntimeError), e:
        print '\tcaught exception: ', e
        continue

print 'using popt: ', popt
x = temps
y = exp_func(temps, *popt)

graph_max = np.max(y) * 10
graph_min = np.min(y) / 10

plot(x, y, linewidth=1)
axis([np.min(temps), np.max(temps) + 10.0, graph_min, graph_max])
ax.axvline(273.15, color='g', ls='--')
ax.axhline(0, color='g', ls='--')

ax.set_title('{0} Viscosity/Temperature'.format(oil_obj.name))
figtext(-0.02, 0.5, 'cSt')

# annotations
for v, t in zip(kvis_refs, kvis_ref_temps):
    ax.plot(t, v, marker='o', color="green")
    ax.annotate(r'{0:.2g} $m^2/s$'.format(v),
                xy=(t, v),  xycoords='data',
                xytext=(0, 30), textcoords='offset points',
                arrowprops=dict(arrowstyle="->")
                )

# generate x ticks
kvis_ref_ticks = [(v, r'$T{0}={1:.1f}^\circ K$                '.format(i, v))
                  for i, v in enumerate(kvis_ref_temps)]
x_ticks = [(v, r'${0:.1f}^\circ K$'.format(v)) for v, l in zip(*plt.xticks())]
x_ticks += kvis_ref_ticks
x_ticks += [(273.15, r'$0^\circ C={0:.1f}^\circ K$'.format(273.15))]
x_ticks += [(pour_point, r'$PourPoint = {0:.1f}^\circ K$'.format(pour_point))]
plt.xticks(*zip(*x_ticks))
plt.yscale('log', nonposy='clip')

for tick in ax.xaxis.get_major_ticks():
    tick.label.set_fontsize(10)
    # specify integer or one of preset strings, e.g.
    #tick.label.set_fontsize('x-small')
    tick.label.set_rotation('vertical')

show()
```

Ok, this curve optimization fits the data reasonably for a couple of different oils.  We now turn this process into a more easily usable function.

```python
def determine_k_v2(ref_temp_k, ref_kvis):
    k_v2 = 5000.0  # default if we can't find an optimization

    def exp_func(temp_k, a, k_v2):
        return a * np.exp(k_v2 / temp_k)

    for k in np.logspace(3.6, 4.5, num=8):
        # k = log range from about 5000-32000
        a_coeff = ref_kvis[0] * np.exp(-k / ref_temp_k[0])

        try:
            popt, pcov = curve_fit(exp_func, ref_temp_k, ref_kvis,
                                   p0=(a_coeff, k), maxfev = 2000)

            # - we want our covariance to be a reasonably small number,
            #   but it can get into the thousands even for a good fit.
            #   So we will only check for inf values.
            # - for sample sizes < 3, the covariance is unreliable.
            if len(ref_kvis) > 2 and np.any(pcov == np.inf):
                print 'covariance too high.'
                continue

            k_v2 = popt[1]
            #std_dev = np.sqrt(np.diag(pcov))
            break
        except (ValueError, RuntimeError), e:
            print 'caught exception: ', e
            continue

    return k_v2

```

Now let's see what the $k_{v2}$ trends are for the oils in our database.

```python
k_v2_list = []
for oil_obj in session.query(Oil).filter(Oil.product_type == 'Crude'):
    if len(oil_obj.imported.kvis) >= 2:
        k_v2 = determine_k_v2(*zip(*[(k.ref_temp_k, k.m_2_s)
                                     for k in oil_obj.imported.kvis]))
        k_v2_list.append(k_v2)
        if k_v2 < 0.0:
            print (oil_obj.name, oil_obj.adios_oil_id), k_v2

print np.min(k_v2_list), np.max(k_v2_list), np.mean(k_v2_list)

```

    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    (u'DUBAI, CITGO', u'AD00311') -1970.84061009
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    caught exception:  Optimal parameters not found: Number of calls to function has reached maxfev = 2000.
    (u'AGBAMI, STATOIL', u'AD02548') -3.28235530165e-06
    (u'AASGARD BLEND, STATOIL', u'AD02552') -3.28235530165e-06
    -1970.84061009 15462.2947889 3751.2948041

It is interesting to note that there are a few oils that have viscosity data resulting in a negative value for $k_{v2}$.  These are probably data errors, but we should protect ourselves from this possibility.  In this case, I suppose we could choose a default mean value.  And the default mean value we have been using is 5000.0.

```python
def determine_k_v2(ref_temp_k, ref_kvis):
    k_v2 = 5000.0  # default if we can't find an optimization

    def exp_func(temp_k, a, k_v2):
        return a * np.exp(k_v2 / temp_k)

    for k in np.logspace(3.6, 4.5, num=8):
        # k = log range from about 5000-32000
        a_coeff = ref_kvis[0] * np.exp(-k / ref_temp_k[0])

        try:
            popt, pcov = curve_fit(exp_func, ref_temp_k, ref_kvis,
                                   p0=(a_coeff, k), maxfev=2000)

            # - we want our covariance to be a reasonably small number,
            #   but it can get into the thousands even for a good fit.
            #   So we will only check for inf values.
            # - for sample sizes < 3, the covariance is unreliable.
            if len(ref_kvis) > 2 and np.any(pcov == np.inf):
                print 'covariance too high.'
                continue

            if popt[1] <= 1.0:
                continue

            k_v2 = popt[1]
            #std_dev = np.sqrt(np.diag(pcov))
            break
        except (ValueError, RuntimeError):
            continue

    return k_v2

```

```python
plt.figure(1, figsize=(12,8))
ax = plt.subplot(111)

k_v2_list = []
oils_total = 0
for oil_obj in session.query(Oil).filter(Oil.product_type == 'Crude'):
    oils_total += 1
    if len(oil_obj.imported.kvis) >= 2:
        k_v2 = determine_k_v2(*zip(*[(k.ref_temp_k, k.m_2_s)
                                     for k in oil_obj.imported.kvis]))
        k_v2_list.append(k_v2)
        if k_v2 < 0.0:
            print (oil_obj.name, oil_obj.adios_oil_id), k_v2
print '# Oils Total: {}, # Oils with multi-kvis: {}'.format(oils_total, len(k_v2_list))

k_low, k_high, k_mean = np.min(k_v2_list), np.max(k_v2_list), np.mean(k_v2_list)
k_std, k_var = np.std(k_v2_list), np.var(k_v2_list)

mu = np.log(k_mean / np.sqrt(1 + k_var / k_mean ** 2.0))
sigma = np.sqrt(np.log(1 + k_var / k_mean ** 2.0))
log_mode = np.exp(mu - sigma ** 2.0)

# the histogram of the data
n, bins, patches = ax.hist(k_v2_list, 50, normed=1, facecolor='green')

ax.set_xlabel('$k_{v2}$')
ax.set_ylabel('Occurrence')
ax.set_title(r'Histogram of $k_{v2}$ values over all crude oils with multiple KVis')

ax.axvline(k_mean, color='m', ls='--')
ax.axvline(log_mode, color='m', ls='--')

x_ticks = [(v, v) for v, l in zip(*plt.xticks())]
x_ticks += [(k_mean, r'$k_{{mean}}={0:.1f}$              '.format(k_mean))]
x_ticks += [(log_mode, r'$k_{{mode}}={0:.1f}$              '.format(log_mode))]
x_ticks += [(k_low, r'$k_{{low}}={0:.1f}$              '.format(k_low))]
x_ticks += [(k_high, r'$k_{{high}}={0:.1f}$              '.format(k_high))]
plt.xticks(*zip(*x_ticks))
for tick in ax.xaxis.get_major_ticks():
    tick.label.set_fontsize(12)
    # specify integer or one of preset strings, e.g.
    #tick.label.set_fontsize('x-small')
    tick.label.set_rotation('vertical')

ax.grid(True)

show()
```

Plotting a histogram of the values of $k_{v2}$ for the crude oils, it does seem to indicate a lognormal distribution.  And we see that the distribution lognormal mode (2075.7) is a bit lower than the default we have been using (5000.0).  Bill's most recent document on viscosity as a function of temperature suggests an alternative value of 2416.0 (Abu-Eishah, 1999), which would seem to be a better default value to use based on the measured values in our set.

The majority of the crude oils in our database have only one viscosity measurement (527 total), and for these we don't have enough data to estimate $k_{v2}$.  So we will need to rely on a good default value most of the time.

However there seems to be a very wide range between the minimum and maximum values for the oils that we are able to estimate.  And it stands to reason that there is probably a similar stratification of viscosity properties for the oils that we are not able to estimate.  Of course using a default average $k_{v2}$ for those oils would not be appropriate, but we can not really know since we don't have the data.

Let's also try this for refined products.

```python
plt.figure(1, figsize=(12,8))
ax = plt.subplot(111)

k_v2_list = []
oils_total = 0
for oil_obj in session.query(Oil).filter(Oil.product_type == 'Refined'):
    oils_total += 1
    if len(oil_obj.imported.kvis) >= 2:
        k_v2 = determine_k_v2(*zip(*[(k.ref_temp_k, k.m_2_s)
                                     for k in oil_obj.imported.kvis]))
        k_v2_list.append(k_v2)
        if k_v2 < 0.0:
            print (oil_obj.name, oil_obj.adios_oil_id), k_v2
print '# Oils Total: {}, # Oils with multi-kvis: {}'.format(oils_total, len(k_v2_list))

k_low, k_high, k_mean = np.min(k_v2_list), np.max(k_v2_list), np.mean(k_v2_list)
k_std, k_var = np.std(k_v2_list), np.var(k_v2_list)

mu = np.log(k_mean / np.sqrt(1 + k_var / k_mean ** 2.0))
sigma = np.sqrt(np.log(1 + k_var / k_mean ** 2.0))
log_mode = np.exp(mu - sigma ** 2.0)

# the histogram of the data
n, bins, patches = ax.hist(k_v2_list, 50, normed=1, facecolor='green')

ax.set_xlabel('$k_{v2}$')
ax.set_ylabel('Occurrence')
ax.set_title(r'Histogram of $k_{v2}$ values over all refined products with multiple KVis')

ax.axvline(k_mean, color='m', ls='--')
ax.axvline(log_mode, color='m', ls='--')

x_ticks = [(v, v) for v, l in zip(*plt.xticks())]
x_ticks += [(k_mean, r'$k_{{mean}}={0:.1f}$              '.format(k_mean))]
x_ticks += [(log_mode, r'$k_{{mode}}={0:.1f}$              '.format(log_mode))]
x_ticks += [(k_low, r'$k_{{low}}={0:.1f}$              '.format(k_low))]
x_ticks += [(k_high, r'$k_{{high}}={0:.1f}$              '.format(k_high))]
plt.xticks(*zip(*x_ticks))
for tick in ax.xaxis.get_major_ticks():
    tick.label.set_fontsize(12)
    # specify integer or one of preset strings, e.g.
    #tick.label.set_fontsize('x-small')
    tick.label.set_rotation('vertical')

ax.grid(True)

show()
```

Here we see that the histogram does sort of begin to approximate a lognormal.  But the number of samples is so small that I don't think we can really have confidence in the calculated mean and mode values.  With a larger sample size, we might start to see something that we can draw some conclusions from.

It is currently not a requirement that a refined product have multiple viscosities.  And a quick query shows that only 7 out of 70 refined products have them.  It would be nice if we had more viscosity data for these records.

Visually, and based on the above, I believe that if we needed to choose a default, it would lie somewhere around 2500.0.  So again, Abu-Eishah's (1999) alternative recommendation of 2416.0 would probably work.

```python

```


| |Pierre Proulx, ing, professeur|
|:---|:---|
|Département de génie chimique et de génie biotechnologique |** GCH200-Phénomènes d'échanges I **|

### Section 10.7, Conduction de la chaleur dans une ailette

```python
#
# Pierre Proulx
#
#
# Préparation de l'affichage et des outils de calcul symbolique
#
import sympy as sp
from IPython.display import *
sp.init_printing(use_latex=True)
%matplotlib inline
```

```python
# Paramètres, variables et fonctions

W,B,h,k,T,T_A,delta_z,z,T_0,L=sp.symbols('W,B,h,k,T,T_A,delta_z,z,T_0,L')
q=sp.symbols('q')
T=sp.Function('T')(z)
```

```python
#
# Bilan dans l'ailette
#
eq=(2*B*W*q(z)-2*B*W*q(z+delta_z))-2*W*delta_z*h*(T-T_A)
eq=(eq/(2*B*W*delta_z)).simplify()
display('Bilan différentiel',eq)
```

```python
# limite quand dz > 0
eq=sp.limit(eq,delta_z,0).doit()
display('Équation du bilan différentiel après limite',eq)
```

```python
fourier=-k*sp.diff(T,z)
eq=eq.subs(q(z),fourier)
display('Après substitution de la loi de Fourier pour avoir une équation en fonction de T',eq)
```

```python
T=sp.dsolve(eq,T)
display('Solution générale incluant les constantes inconnues',T)
```

```python
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
condition_1=sp.Eq(T.rhs.diff(z).subs(z,L    ),0)
condition_2=sp.Eq(T.rhs.subs(z,0)-T_0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2'))    # pour trouver C1 et C2
display(constantes)
```

```python
T=T.subs(constantes)
display(T)
```

### * 1  Exemples de calcul: quantité de chaleur dissipée

```python
#
# La méthode la plus simple est de calculer le flux qui passe à
# la base de l'ailette et le multiplier par la surface de la section
#
QD=2*B*W*(-k*sp.diff(T,z).subs(z,0))
display(QD)
```

```python
#
# Tracons le profil de température pour des valeurs de paramètres choisis
#
dico={'h':20,'k':20,'B':0.001,'T_0':100,'T_A':20,'L':0.01}
Tplot=T.rhs.subs(dico)
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize']=10,8
sp.plot(Tplot,(z,0,L.subs(dico)),ylabel='T');
```

>> Comment calculer l'efficacité des ailettes: voyons le profil de température d'une autre ailette.

```python
#
# Tracons le profil de température pour une ailette presque idéale
#
# Pourquoi cete ailette est-elle plus efficace que la précédente?
#
dico={'h':20,'k':300,'B':0.001,'T_0':100,'T_A':20,'L':0.01}
plt.rcParams['figure.figsize']=10,8
Tplot=T.rhs.subs(dico)
sp.plot(Tplot,(z,0,L.subs(dico)),xlabel='z',ylabel='T');
```

>> La quantité maximale de chaleur que peut dissiper une ailette est :

$ Q_{max} = h A (T_0 -T_A) $

>> La quantité de chaleur dissipée par une ailette de section constante est :

$ Q_{reel} = P \int _{0} ^{L} h (T_z - T_A) dz $

ou P est le périmètre de l'ailette.
>> L'efficacité de l'ailette réelle est donc

$ \eta = \frac {Q_{reel}} {Q_{max}} $

> * Exercice: calculez l'efficacité des deux ailettes ci-haut.


###### Material desenvolvido para o minicurso: Introdução à solução numérica de EDP's, ministrado no ERMAC/2018 de 5 a 6 de abril de 2018 na Universidade Federal de Lavras, Lavras/MG, Brasil. Autor: [Jonas Laerte Ansoni](http://jonasansoni.blogspot.com.br/).

#  Minicurso: Introdução à solução numérica de EDP's

### 2.6 Estabilidade e condição CFL

Na aula anterior desta série, estudamos a solução numérica das equações de convecção lineares, usando o método das diferenças finitas. Você testou o método utilizado usando diferentes opções de parâmetros? Se você fez isto, provavelmente se deparou com algum comportamento inesperado. Sua solução chegou a explodir (às vezes de maneira legal!)?

#### [Olhar] Livro Fortuna, página 204.

### O que aconteceu??

Para responder a essa pergunta, temos que pensar um pouco sobre o que estamos realmente implementando no código quando resolvemos a equação de convecção linear com o método forward-time / backward-space.

Em cada iteração do _loop_ de tempo, usamos os dados existentes sobre a solução no tempo $n$ para calcular a solução no próximo período de tempo, $n + 1$. Nos primeiros casos, o aumento no número de pontos de grade retornou resultados mais precisos. Houve menos erros de discretização e a onda em movimento parecia mais uma onda quadrada do que no nosso primeiro exemplo.

Cada iteração do _loop_ de tempo avança a solução por uma etapa de tempo de comprimento $\Delta t$, que tinha o valor 0,025 nos exemplos acima. Durante essa iteração, avaliamos a solução $c$ em cada um dos $x_i$ pontos da grade. Mas no último plot, algo claramente deu errado.

O que aconteceu é que durante o período de tempo $\Delta t$, a onda está viajando a uma distância maior que `dx`, e dizemos que a solução se torna *instável* nessa situação (esta afirmação pode ser provada formalmente, ver abaixo). O comprimento `dx` do espaçamento da grade é inversamente proporcional ao número total de pontos `nx`: nós utilizamos mais pontos de grade, então `dx` ficou menor. Uma vez que o `dx` ficou menor que o $u \Delta t $ (a distância percorrida pela solução numérica em um único intervalo de tempo) não é mais possível para o esquema numérico resolver a equação corretamente!

#### Interpretação gráfica da condição CFL.

Considere a ilustração acima. O triângulo verde representa o domínio de dependência do esquema numérico. De fato, para cada etapa de tempo, a variável $c_i^{n + 1}$ depende apenas dos valores $ c_i^{n}$ e $u_{i-1}^{n}$.

Quando a distância $u\Delta t$ for menor que $\Delta x$, a linha característica traçada a partir da coordenada da grade $i, n + 1 $ permanece _entre_ os pontos $i-1, n$ e $i, n$ na grade. Em seguida, dizemos que o domínio _matemático da dependência_ da solução do EDP original está contido no _domínio de dependência_ do esquema numérico.

Pelo contrário, se $\Delta x$ for menor que $u \Delta t$, então as informações sobre a solução necessária para $c_i^{n + 1}$ não estão disponíveis no _domínio de dependência_ do esquema numérico, porque a linha característica traçada a partir da coordenada da grade $i, n + 1$ permanece _entre_ o ponto $i-1, n$ na grade.

A condição a seguir garante que o domínio de dependência da equação diferencial esteja contido no domínio _numerico_ de dependência:

\begin{equation}\sigma = \frac{u \Delta t}{\Delta x} \leq 1
\end{equation}

Como pode ser provado formalmente, a estabilidade da solução numérica requer que o tamanho do passo "dt" seja calculado em relação ao tamanho de "dx" para satisfazer a condição acima.

O valor de $u \Delta t/ \Delta x$ é chamado de **número de Courant-Friedrichs-Lewy** (número de CFL), frequentemente denotado por $\sigma$. O valor $\sigma_{\text{max}}$ que garantirá a estabilidade depende da discretização usada; Para o esquema forward-time/backward-space, a condição para a estabilidade é $\sigma <1$.

Em uma nova versão do nosso código, usaremos o número CFL para calcular o passo de tempo apropriado `dt` dependendo do tamanho de` dx`.

#### Existe um teorema afirmando que o método será estável se $0 < \sigma \leq 1$ e instável se $\sigma > 1$. (Ames, 1992).  Difusão numérica.

 Note que esta condição estabelece que a velocidade numérica tem que ser maior ou igual à velocidade de advecção $u$.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```python
def linearconv(nx):
    """Solve the linear convection equation.

    Solves the equation d_t u + c d_x u = 0 where
    * the wavespeed c is set to 1
    * the domain is x \in [0, 2]
    * 20 timesteps are taken, with \Delta t computed using the CFL 0.5
    * the initial data is the hat function

    Produces a plot of the results

    Parameters
    ----------

    nx : integer
        number of internal grid points

    Returns
    -------

    None : none
    """
    dx = 2/(nx-1)
    nt = 20
    c = 1
    sigma = .5
    x = numpy.linspace(0,2,nx)

    dt = sigma*dx

    u = numpy.ones(nx)
    lbound = numpy.where(x >= 0.5)
    ubound = numpy.where(x <= 1)
    u[numpy.intersect1d(lbound, ubound)]=2

    un = numpy.ones(nx)

    for n in range(nt):
        un = u.copy()
        u[1:] = un[1:] -c*dt/dx*(un[1:] -un[0:-1])
        u[0] = 1.0

    pyplot.plot(x, u, color='#003366', ls='--', lw=3)
    pyplot.ylim(0,2.5);
```

Agora, não importa quantos pontos usamos para a grade espacial: a solução será sempre estável!

```python
linearconv(41) #41, 101, 121
```

```python
from IPython.core.display import HTML
css_file = '../styles/custom.css'
HTML(open(css_file, "r").read())
```


```python
from sympy.physics.units import *
from sympy import *

# Rounding:
import decimal
from decimal import Decimal as DX
def iso_round(obj, pv, rounding=decimal.ROUND_HALF_EVEN):
    import sympy
    """
    Rounding acc. to DIN EN ISO 80000-1:2013-08
    place value = Rundestellenwert
    """
    assert pv in set([
        # place value   #  round to:
        100,            #  3rd last digit before decimal
        10,             #  2nd last
        1,              #  last
        0.1,            #  1st digit after decimal
        0.01,           #  2nd
        0.001,          #  3rd
        0.0001,         #  4th
        0.00001,        #  5th
        0.000001,       #  6th
        0.0000001,      #  7th
        0.00000001,     #  8th
        0.000000001,    #  9th
        0.0000000001,   # 10th
        ])
    try:
        tmp = DX(str(float(obj)))
        obj = tmp.quantize(DX(str(pv)), rounding=rounding)
    except:
        for i in range(len(obj)):
            tmp = DX(str(float(obj[i])))
            obj[i] = tmp.quantize(DX(str(pv)), rounding=rounding)
    return obj

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""
# kwargs["symbol_names"] = {FB: "F^{\mathsf B}", }

# Units:
(k, M, G ) = ( 10**3, 10**6, 10**9 )
(mm, cm, deg) = ( m/1000, m/100, pi/180)
Newton = kg*m/s**2
Pa     = Newton/m**2
MPa    = M*Pa
GPa    = G*Pa
kN     = k*Newton

# ---

c, mass = var("c, mass", positive=True)

K = Matrix([
[2*c,  -c],
[-c,  c],
])

M = Matrix([
[3*mass/2, 0],
[0, mass],
])

EI = 210 *GPa * 5100 *cm**4
l = S(45)/10 *m
sub_list = [
    (mass, 1000*kg    ),
    (c, 24*EI/l**3 ),
    ]

# ξ = λ²
xi = var("xi")
w = var("omega")

A = K + xi*M

pprint("\nCharacteristic equation:")
eq = Eq(det(A))
pprint(eq)

sol_xi = solve(eq,xi)

w2, w3 = var("w2, w3")
w = Matrix([w2, w3])

zero = Matrix([0,0])
for i in range(len(sol_xi)):
    pprint("\n\nEigenvalue:")
    xii = sol_xi[i]
    pprint(xii)
    Ai = A.subs(xi,xii)
    eq = Eq(Ai*w,zero)
    sol = solve(eq, w2)
    pprint("\nEigenvector:")
    pprint(sol)

# Omega 1:
pprint("\nw1 / (1/s):")
w1 = sqrt(2*c/mass)
tmp = w1.subs(sub_list)
tmp /= (1/s)
tmp = iso_round(tmp, 0.1)
pprint(tmp)

w = w1.subs(sub_list)
w_in_Hz = w / (1/s)

pprint("\nPeriod T1 / s:")
T = 2*pi/w
T_in_s = T / s
T_in_s = N(T_in_s,20)
T_in_s = float(T_in_s)
tmp = T_in_s
tmp = iso_round(tmp,0.001)
pprint(tmp)

from pylab import *
from numpy import linspace
t_in_s = linspace(0,T_in_s,100)
wt =  w_in_Hz * t_in_s
cos_wt = array([cos(float(x)) for x in wt])
w2_in_mm = -10 * cos_wt
w3_in_mm = +10 * cos_wt

plt.axis()
plt.grid()
plt.plot(t_in_s, w2_in_mm, "b-", label=r"$w2\,\, /  \,\, \mathrm{mm}$")
plt.plot(t_in_s, w3_in_mm, "r--", label=r"$w3\,\, /  \,\, \mathrm{mm}$")
plt.xlabel(r"$t  \,\, /  \,\, \mathrm{s}$")
plt.legend()
plt.savefig('2dofs_motion.svg', transparent=True)
plt.show()

# Characteristic equation:
#    2                ⎛      3⋅mass⋅ξ⎞
# - c  + (c + mass⋅ξ)⋅⎜2⋅c + ────────⎟ = 0
#                     ⎝         2    ⎠
#
#
# Eigenvalue:
# -2⋅c
# ─────
#  mass
#
# Eigenvector:
# {w₂: -w₃}
#
#
# Eigenvalue:
#  -c
# ──────
# 3⋅mass
#
# Eigenvector:
# ⎧    2⋅w₃⎫
# ⎨w₂: ────⎬
# ⎩     3  ⎭
#
# w1 / (1/s):
# 75.1
#
# Period T1 / s:
# 0.084

```