## Interactive Variogram Modeling Demonstration

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

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

### The Interactive Workflow

Here's a simple workflow on detecting the major spatial continuity directions in a spatial dataset with variogram analysis and then building a consistent. positive definate variogram model in 2D.

* this model will provide consistent variogram values for any offset (azimuth and distance) in 2D

This information is essential to optimum well placement and prectiction away from wells.  First let's explain the concept of spatial continuity and the variogram.

#### Spatial Continuity

**Spatial Continuity** is the correlation between values over distance.

* No spatial continuity – no correlation between values over distance, random values at each location in space regardless of separation distance.

* Homogenous phenomenon have perfect spatial continuity, since all values as the same (or very similar) they are correlated.

We need a statistic to quantify spatial continuity! A convenient method is the Semivariogram.

#### The Semivariogram

Function of difference over distance.

* The expected (average) squared difference between values separated by a lag distance vector (distance and direction), $h$:

\begin{equation}
\gamma(\bf{h}) = \frac{1}{2 N(\bf{h})} \sum^{N(\bf{h})}_{\alpha=1} (z(\bf{u}_\alpha) - z(\bf{u}_\alpha + \bf{h}))^2
\end{equation}

where $z(\bf{u}_\alpha)$ and $z(\bf{u}_\alpha + \bf{h})$ are the spatial sample values at tail and head locations of the lag vector respectively.

* Calculated over a suite of lag distances to obtain a continuous function.

* the $\frac{1}{2}$ term converts a variogram into a semivariogram, but in practice the term variogram is used instead of semivariogram.
* We prefer the semivariogram because it relates directly to the covariance function, $C_x(\bf{h})$ and univariate variance, $\sigma^2_x$:

\begin{equation}
C_x(\bf{h}) = \sigma^2_x - \gamma(\bf{h})
\end{equation}

Note the correlogram is related to the covariance function as:

\begin{equation}
\rho_x(\bf{h}) = \frac{C_x(\bf{h})}{\sigma^2_x}
\end{equation}

The correlogram provides of function of the $\bf{h}-\bf{h}$ scatter plot correlation vs. lag offset $\bf{h}$.

\begin{equation}
-1.0 \le \rho_x(\bf{h}) \le 1.0
\end{equation}

#### Variogram Observations

The following are common observations for variograms that should assist with their practical use.

##### Observation \#1 - As distance increases, variability increase (in general).

This is common since in general, over greater distance offsets, there is often more difference between the head and tail samples.

In some cases, such as with spatial cyclicity of the hole effect variogram model the variogram may have negative slope over somelag distance intervals

Negative slopes at lag distances greater than half the data extent are often caused by too few pairs for a reliable variogram calculation

##### Observation \#2 - Calculated with over all possible pairs separated by lag vector, $\bf{𝐡}$.

We scan through the entire data set, searching for all possible pair combinations with all other data.  We then calculate the variogram as one half the expectation of squared difference between all pairs.

More pairs results in a more reliable measure.

##### Observation \#3 - Need to plot the sill to know the degree of correlation.

**Sill** is the variance, $\sigma^2_x$

Given stationarity of the variance, $\sigma^2_x$, and variogram $\gamma(\bf{h})$:

we can define the covariance function:

\begin{equation}
C_x(\bf{h}) = \sigma^2_x - \gamma(\bf{h})
\end{equation}

The covariance measure is a measure of similarity over distance (the mirror image of the variogram as shown by the equation above).

Given a standardized distribution $\sigma^2_x = 1.0$, the covariance, $C_x(\bf{h})$, is equal to the correlogram, $\rho_x(\bf{h})$:

\begin{equation}
\rho_x(\bf{h}) = \sigma^2_x - \gamma(\bf{h})
\end{equation}

##### Observation \#4 - The lag distance at which the variogram reaches the sill is know as the range.

At the range, knowing the data value at the tail location provides no information about a value at the head location of the lag distance vector.

##### Observation \#5 - The nugget effect, a discontinuity at the origin

Sometimes there is a discontinuity in the variogram at distances less than the minimum data spacing.  This is known as **nugget effect**.

The ratio of nugget / sill, is known as relative nugget effect (%). Modeled as a discontinuity with no correlation structure that at lags, $h \gt \epsilon$, an infinitesimal lag distance, and perfect correlation at $\bf{h} = 0$.
Caution when including nuggect effect in the variogram model as measurement error, mixing populations cause apparent nugget effect

This exercise demonstrates the semivariogram calculation with GeostatsPy. The steps include:

1. generate a 2D model with sequential Gaussian simulation
2. sample from the simulation
3. calculate and visualize experimental semivariograms

#### Variogram Modeling

Spatial continuity can be modeled with nested, positive definate variogram structures:

\begin{equation}
\Gamma_x(\bf{h}) = \sum_{i=1}^{nst} \gamma_i(\bf{h})
\end{equation}

where $\Gamma_x(\bf{h})$ is the nested variogram model resulting from the summation of $nst$ nested variograms  $\gamma_i(\bf{h})$.

The types of structure commonly applied include:

* spherical

* exponential

* gaussian

* nugget

Other less common models include:

* hole effect

* dampenned hole effect

* power law

these will not be covered here.

Each one of these variogram structures, $\gamma_i(\bf{h})$, is based on a geometric anisotropy model parameterized by the orientation and range in the major and minor directions.  In 2D this is simply an azimuth and ranges, $azi$, $a_{maj}$ and $a_{min}$. Note, the range in the minor direction (orthogonal to the major direction).

The geometric anisotropy model assumes that the range in all off-diagonal directions is based on an ellipse with the major and minor axes alligned with and set to the major and minor for the variogram.

\begin{equation}
\bf{h}_i = \sqrt{\left(\frac{r_{maj}}{a_{maj_i}}\right)^2 + \left(\frac{r_{maj}}{a_{maj_i}}\right)^2}
\end{equation}

Therefore, if we know the major direction, range in major and minor directions, we may completely describe each nested componnent of the complete spatial continuity of the variable of interest, $i = 1,\dots,nst$.

Some comments on modeling nested variograms:

* we can capture nugget, short and long range continuity structures

* we rely on the geometric anisotropy model, so all structures must inform the same level of contribution (porportion of the sill) in all directions.

* the geometric anisotropy model is based on azimuth of the major direction of continuity, range in the major direction and range in the minor direction (orthogonal to the major direction).  The range is interpolated between the major and minor azimuths with a ellipse model

* we can vary the type of variogram, direction or azimuth of the major direction, and major and minor ranges by structure

In this workflow we will explore methods to:

1. detect directionality from a spatial dataset
2. calculate the directional variograms in the major and minor directions
3. build a consistent 2D model fit to the major and minor directions

#### Objective

In the PGE 383: Stochastic Subsurface Modeling class I want to provide hands-on experience with building subsurface modeling workflows. Python provides an excellent vehicle to accomplish this. I have coded a package called GeostatsPy with GSLIB: Geostatistical Library (Deutsch and Journel, 1998) functionality that provides basic building blocks for building subsurface modeling workflows.

The objective is to remove the hurdles of subsurface modeling workflow construction by providing building blocks and sufficient examples. This is not a coding class per se, but we need the ability to 'script' workflows working with numerical methods.

#### Getting Started

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

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

You will need to copy the data file to your working directory.  They are available here:

* Tabular data - sample_data.csv at https://git.io/fh4gm.

There are exampled below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

#### Load the required libraries

The following code loads the required libraries.

```python
import geostatspy.GSLIB as GSLIB                       # GSLIB utilies, visualization and wrapper
import geostatspy.geostats as geostats                 # GSLIB methods convert to Python
```

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

```python
%matplotlib inline
import os                                               # to set current working directory
import sys                                              # supress output to screen for interactive variogram modeling
import io
import numpy as np                                      # arrays and matrix math
import pandas as pd                                     # DataFrames
import matplotlib.pyplot as plt                         # plotting
from matplotlib.pyplot import cm                        # color maps
from ipywidgets import interactive                      # widgets and interactivity
from ipywidgets import widgets
from ipywidgets import Layout
from ipywidgets import Label
from ipywidgets import VBox, HBox
```

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

#### Set the working directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).  Also, in this case make sure to place the required (see above) GSLIB executables in this directory or a location identified in the environmental variable *Path*.

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

#### Loading Tabular Data

Here's the command to load our comma delimited data file in to a Pandas' DataFrame object.

```python
df = pd.read_csv("sample_data_MV_biased.csv")                     # read a .csv file in as a DataFrame
#print(df.iloc[0:5,:])                                  # display first 4 samples in the table as a preview
df.head()                                               # we could also use this command for a table preview
```

      Unnamed: 0
      X
      Y
      Facies
      Porosity
      Perm
      AI

      0
      0
      100.0
      900.0
      0.0
      0.101319
      1.996868
      5590.417154

      1
      1
      100.0
      800.0
      1.0
      0.147676
      10.711789
      3470.845666

      2
      2
      100.0
      700.0
      1.0
      0.145912
      17.818143
      3586.988513

      3
      3
      100.0
      600.0
      1.0
      0.186167
      217.109365
      3732.114787

      4
      4
      100.0
      500.0
      1.0
      0.146088
      16.717367
      2534.551236

We will work with all facies pooled together. I wanted to simplify this workflow and focus more on spatial continuity direction detection. Finally, by not using facies we do have more samples to support our statistical inference. Most often facies are essential in the subsurface model. Don't worry we will check if this is reasonable in a bit.

You are welcome to repeat this workflow on a by-facies basis.  The following code could be used to build DataFrames ('df_sand' and 'df_shale') for each facies.

```p
df_sand = pd.DataFrame.copy(df[df['Facies'] == 1]).reset_index()  # copy only 'Facies' = sand records
df_shale = pd.DataFrame.copy(df[df['Facies'] == 0]).reset_index() # copy only 'Facies' = shale records
```

Let's look at summary statistics for all facies combined:

```python
df.describe().transpose()                          # summary table of sand only DataFrame statistics
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Unnamed: 0
      368.0
      293.260870
      169.058258
      0.000000
      150.500000
      296.000000
      439.500000
      586.000000

      X
      368.0
      499.565217
      289.770794
      0.000000
      240.000000
      500.000000
      762.500000
      990.000000

      Y
      368.0
      520.644022
      277.412187
      9.000000
      269.000000
      539.000000
      769.000000
      999.000000

      Facies
      368.0
      0.597826
      0.491004
      0.000000
      0.000000
      1.000000
      1.000000
      1.000000

      Porosity
      368.0
      0.127026
      0.030642
      0.041122
      0.103412
      0.125842
      0.148623
      0.210258

      Perm
      368.0
      85.617362
      228.362654
      0.094627
      2.297348
      10.377292
      50.581288
      1991.097723

      AI
      368.0
      4791.736646
      974.560569
      1981.177309
      4110.728374
      4713.325533
      5464.043562
      7561.250336

Let's transform the porosity and permeaiblity data to standard normal (mean = 0.0, standard deviation = 1.0, Gaussian shape). This is required for sequential Gaussian simulation (common target for our variogram models) and the Gaussian transform assists with outliers and provides more interpretable variograms.

Let's look at the inputs for the GeostatsPy nscore program.  Note the output include an ndarray with the transformed values (in the same order as the input data in Dataframe 'df' and column 'vcol'), and the transformation table in original values and also in normal score values.

```python
geostats.nscore                                         # see the input parameters required by the nscore function
```

    <function geostatspy.geostats.nscore(df, vcol, wcol=None, ismooth=False, dfsmooth=None, smcol=0, smwcol=0)>

The following command will transform the Porosity and Permeabilty to standard normal.

```python
#Transform to Gaussian by Facies
df['NPor'], tvPor, tnsPor = geostats.nscore(df, 'Porosity') # nscore transform for all facies porosity
df['NPerm'], tvPermSand, tnsPermSand = geostats.nscore(df, 'Perm')  # nscore transform for all facies permeability
```

Let's look at the updated DataFrame to make sure that we now have the normal score porosity and permeability.

```python
df.head()                                               # preview sand DataFrame with nscore transforms
```

      Unnamed: 0
      X
      Y
      Facies
      Porosity
      Perm
      AI
      NPor
      NPerm

      0
      0
      100.0
      900.0
      0.0
      0.101319
      1.996868
      5590.417154
      -0.749088
      -0.767247

      1
      1
      100.0
      800.0
      1.0
      0.147676
      10.711789
      3470.845666
      0.653263
      0.017030

      2
      2
      100.0
      700.0
      1.0
      0.145912
      17.818143
      3586.988513
      0.611663
      0.336607

      3
      3
      100.0
      600.0
      1.0
      0.186167
      217.109365
      3732.114787
      1.993601
      1.211919

      4
      4
      100.0
      500.0
      1.0
      0.146088
      16.717367
      2534.551236
      0.628172
      0.279461

That looks good! One way to check is to see if the relative magnitudes of the normal score transformed values match the original values.  e.g. that the normal score transform of 0.10 porosity normal score is less than the normal score transform of 0.14 porsity.  Also, the normal score transform of values close to the mean value should be close to 0.0

Let's also check the original and transformed sand and shale porosity distributions.

```python
plt.subplot(221)                                        # plot original sand and shale porosity histograms
plt.hist(df['Porosity'], facecolor='red',bins=np.linspace(0.0,0.25,1000),histtype="stepfilled",alpha=0.2,density=True,cumulative=True,edgecolor='black',label='Original')
plt.xlim([0.05,0.25]); plt.ylim([0,1.0])
plt.xlabel('Porosity (fraction)'); plt.ylabel('Frequency'); plt.title('Porosity')
plt.legend(loc='upper left')
plt.grid(True)

plt.subplot(222)
plt.hist(df['NPor'], facecolor='blue',bins=np.linspace(-3.0,3.0,1000),histtype="stepfilled",alpha=0.2,density=True,cumulative=True,edgecolor='black',label = 'Trans')
plt.xlim([-3.0,3.0]); plt.ylim([0,1.0])
plt.xlabel('Porosity (fraction)'); plt.ylabel('Frequency'); plt.title('Nscore Porosity')
plt.legend(loc='upper left')
plt.grid(True)

plt.subplot(223)                                        # plot nscore transformed sand and shale histograms
plt.hist(df['Perm'], facecolor='red',bins=np.linspace(0.0,1000.0,100000),histtype="stepfilled",alpha=0.2,density=True,cumulative=True,edgecolor='black',label='Original')
plt.xlim([0.0,1000.0]); plt.ylim([0,1.0])
plt.xlabel('Porosity (fraction)'); plt.ylabel('Frequency'); plt.title('Permeability')
plt.legend(loc='upper left')
plt.grid(True)

plt.subplot(224)                                        # plot nscore transformed sand and shale histograms
plt.hist(df['NPerm'], facecolor='blue',bins=np.linspace(-3.0,3.0,100000),histtype="stepfilled",alpha=0.2,density=True,cumulative=True,edgecolor='black',label = 'Trans')
plt.xlim([-3.0,3.0]); plt.ylim([0,1.0])
plt.xlabel('Permeability (mD)'); plt.ylabel('Frequency'); plt.title('Nscore Permeability')
plt.legend(loc='upper left')
plt.grid(True)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=2.2, wspace=0.2, hspace=0.3)
plt.show()
```

The normal score transform has correctly transformed the porosity and permeability to standard normal.

#### Method \#1: Ocular Inspection of Posted Data

Data visualization is very useful to detect patterns. Our brains are very good at pattern detection. I promote quantitative methods and recognize issues with cognitive bias, but it is important to recognize the value is expert intepretation based on data visualization.

* This data visualization will also be important to assist with parameter selection for the quantitative methods later.

Let's plot the location maps of normal score transforms of porosity and permeability for all facies. We will also include a cross plot of the nscore permeability vs. porosity colored by facies to aid with comparison in spatial features between the porosity and permeability data.

```python
cmap = plt.cm.plasma                    # color map
plt.subplot(131)
GSLIB.locmap_st(df,'X','Y','NPor',0,1000,0,1000,-3,3,'Nscore Porosity - All Facies','X (m)','Y (m)','Nscore Porosity',cmap)

plt.subplot(132)
GSLIB.locmap_st(df,'X','Y','NPerm',0,1000,0,1000,-3,3,'Nscore Permeability - All Facies','X (m)','Y (m)','Nscore Permeability',cmap)

plt.subplot(133)
facies = df['Facies'].values +0.01
plt.scatter(df['NPor'],df['NPerm'],c = facies,edgecolor = 'black',cmap = plt.cm.inferno)
#plt.plot([-3,3],[-3,3],color = 'black')
plt.xlabel(r'Nscore Porosity')
plt.ylabel(r'Nscore Permeability')
plt.title('Nscore Permeability vs. Porosity')
plt.xlim([-3,3])
plt.ylim([-3,3])
plt.grid(True)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=0.8, wspace=0.5, hspace=0.3)
plt.show()
```

What do you see?  Here's my observations:

* there is a high degree of spatial agreement between porosity and permeability, this is supported by the high correlation evident in the cross plot.
* there are no discontinuities that could suggest that facies represent a distinct change, rather the porosity and permeability seem continuous and the assigned facies are a truncation of their continous behavoir, we doing 'ok' with no facies
* suspect a 045 azimuth major direction of continuity (up - right)
* there may be cycles in the 135 azimuth
* there will not likely be a nugget effect, but there is an hint of some short scale discontinuity?

**Do you agree?** If you have a different observations, drop me a line at mpyrcz@austin.utexas.edu and I'll add to this lesson with credit.

#### Method \#2: Variogram Maps

Let's try out variogram maps.

* I realized that I had not coded variogram maps in Python, so I just did it and added it to GeostatsPy.  I have added if here temporarily to support my students completely their assignments. I'm teaching this week at a company and it is safer not to update the package until I have time to test it.

The inputs include:

* **input data** - DataFrame, 'df', and columns for x, y and property of interest, 'x', 'y' and 'vcol',
* **variogram map parameters** - number of cells in each direction to search, 'nxlag', nylag', the cell size / lag distance, 'dxlag' and 'dylag'''
* **search** - the minimum number of pairs reuqired to assign a result, 'mnpairs'
* **normalization** - 1 for standardize variance to 1.0 and 0 for not

The output is a 2D ndarray with the variogram map and the number of pairs.

```python
vmap, npmap = geostats.varmapv(df,'X','Y','NPor',tmin=-999,tmax=999,nxlag=11,nylag=11,dxlag=50,dylag=50,minnp=1,isill=1)

plt.subplot(121)
GSLIB.pixelplt_st(vmap,-575,575,-575,575,50.0,0,1.6,'Nscore Porosity Variogram Map','X(m)','Y(m)','Nscore Variogram',cmap)

plt.subplot(122)
GSLIB.pixelplt_st(npmap,-575,575,-575,575,50.0,0,900,'Nscore Porosity Variogram Map Number of Pairs','X(m)','Y(m)','Number of Pairs',cmap)

plt.subplots_adjust(left=0.0, bottom=0.0, right=3.0, top=1.2, wspace=0.2, hspace=0.2)
plt.show()

print('The shape of the output is ' + str(vmap.shape))
```

Notice that the output ndarray is 23, 23 cells? We asked for the number of cells to extend in each direction, 11 and 11 in x and y.  The map has an origin (zero distance) cell in the middle and extends 11 in both positive and negative directions.  So we have 2*$nx$ + 1, 2*$ny$ + 1 cells in the resulting variogram map and the $xmin = -1 * (nx * x_{cellsize} + \frac{1}{2} x_{cellsize})$ and the $xmax = nx * x_{cellsize} + \frac{1}{2} x_{cellsize}$

What do you think of this variogram map? These are my observations:

* major continuity direction is at azimuth 045
* there is a high degree of geometric anisotropy
* there is cyclicity in the 135 direction
* there may be some cyclicity in the 045 direction

From this variogram map we can immediately see directionality in our spatial data.

#### Method \#3: Experimental Variograms

Another method for exploring spatial data directionality is the calculation of multiple experimental variograms for a variety of directions.

We can use the location maps to help determine good variogram calculation parameters.

```p
tmin = -9999.; tmax = 9999.;
lag_dist = 100.0; lag_tol = 50.0; nlag = 7; bandh = 9999.9; azi = azi; atol = 22.5; isill = 1
```
* **tmin**, **tmax** are trimming limits - set to have no impact, no need to filter the data
* **lag_dist**, **lag_tol** are the lag distance, lag tolerance - set based on the common data spacing (100m) and tolerance as 100% of lag distance for additonal smoothing
* **nlag** is number of lags - set to extend just past 50 of the data extent
* **bandh** is the horizontal band width - set to have no effect
* **azi** is the azimuth -  it has not effect since we set atol, the azimuth tolerance, to 90.0
* **isill** is a boolean to standardize the distribution to a variance of 1 - it has no effect since the nscore transform sets the variance to 1.0

```python
tmin = -9999.; tmax = 9999.                             # no trimming
lag_dist = 100.0; lag_tol = 100.0; nlag = 8;            # maximum lag is 700m and tolerance > 1/2 lag distance for smoothing
bandh = 9999.9; atol = 22.5                             # no bandwidth, directional variograms
isill = 1                                               # standardize sill
azi_mat = [0,22.5,45,67.5,90,112.5,135,157.5]           # directions in azimuth to consider
```

Let's try running these variograms and visualizing them on separate plots.  I'll demonstrate a method to promgramtically loop over each direction for efficiency (and code brevity).

* we have the direction in the list called 'azi_mat'
* we use the command:
```p
for iazi in range(0,len(azi_mat)):
```
to loop over all the elements in the list with index 'iazi'

* we run the variogram calculation and store the reuslts in 2D arrays, iy is direction, ix is the lag
* we use subplots with the 'iazi' index to add each plot

```p
    plt.subplot(4,2,iazi+1)
```
we add one because the plot index must be $1,\ldots,n$, but arrays / list index as $0,\ldots,n-1$ in Python.

```python
# Arrays to store the results
lag = np.zeros((len(azi_mat),nlag+2)); gamma = np.zeros((len(azi_mat),nlag+2)); npp = np.zeros((len(azi_mat),nlag+2));

for iazi in range(0,len(azi_mat)):                      # Loop over all directions
    lag[iazi,:], gamma[iazi,:], npp[iazi,:] = geostats.gamv(df,"X","Y","NPor",tmin,tmax,lag_dist,lag_tol,nlag,azi_mat[iazi],atol,bandh,isill)
    plt.subplot(4,2,iazi+1)
    plt.plot(lag[iazi,:],gamma[iazi,:],'x',color = 'black',label = 'Azimuth ' +str(azi_mat[iazi]))
    plt.plot([0,2000],[1.0,1.0],color = 'black')
    plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
    plt.ylabel(r'$\gamma \bf(h)$')
    plt.title('Directional NSCORE Porosity Variogram')
    plt.xlim([0,700])
    plt.ylim([0,1.8])
    plt.legend(loc='upper left')
    plt.grid(True)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=4.2, wspace=0.2, hspace=0.3)
plt.show()
```

The directional variograms provide a very clear image of directionality. The ranges vary from 300m, 500m to zonal anisotropy, to 500m, 350m, 280m, 250m and finally 280m.  We are observing the actually spatial continuity ellipse by exploring a variety of directions!

* We can observe that Azimuth 045 is the major direction and Azimuth 135 is the minor direction.

This is a very powerful tool for exploring directionality in spatial datasets.

#### Variogram Modeling

From above we can identify 045 as the major direction and 135 as the minor direction. We must build a single 2D nested variogram model based on the experiential variograms in each 045 and 135 directions.  First, let's look at these two experimental variograms again.

Below edit the following code to select the major and minor directions from the plots above.

```python
# Select the plot above for the major and minor
imajor = 2
iminor = 6
```

Note the first plot is 0 and then step through $0, 1, \ldots, n$ where $n$ is the number of directional variograms calculated above.  For the provided example we selected the plots, 3rd (045 Azimuth) for the major direction and the 7th (135 Azimuth) for the minor direction.

```python
# Select the plot above for the major and minor
imajor = 2
iminor = 6

print('Major direction is ' + str(azi_mat[imajor]) + ' azimuth.')
print('Minor direction is ' + str(azi_mat[iminor]) + ' azimuth.')

if not abs(azi_mat[imajor] - azi_mat[iminor]) == 90.0:
    print('Major and minor directions must be orthogonal to each other.')
    sys.exit()

plt.subplot(1,2,1)
plt.plot(lag[imajor,:],gamma[imajor,:],'x',color = 'black',label = 'Azimuth ' + str(azi_mat[imajor]))
plt.plot([0,2000],[1.0,1.0],color = 'black')
plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
plt.ylabel(r'$\gamma \bf(h)$')
plt.title('Directional NSCORE Porosity Variogram - Major ' + str(azi_mat[imajor]) + ' Azimuth')
plt.xlim([0,700])
plt.ylim([0,1.8])
plt.legend(loc='upper left')

plt.subplot(1,2,2)
plt.plot(lag[iminor,:],gamma[iminor,:],'x',color = 'black',label = 'Azimuth ' +str(azi_mat[iminor]))
plt.plot([0,2000],[1.0,1.0],color = 'black')
plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
plt.ylabel(r'$\gamma \bf(h)$')
plt.title('Directional NSCORE Porosity Variogram  - Minor ' + str(azi_mat[iminor]) + ' Azimuth')
plt.xlim([0,700])
plt.ylim([0,1.8])
plt.legend(loc='upper left')

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.2, top=1.6, wspace=0.2, hspace=0.3)
plt.show()
```

We will assume these models will be applied in sequential Gaussian simulation; therefore, we must model to the sill (or the global distribution will not be reproduced over the realizations).  We will also not use cyclicity for now, as we are just getting started.

* Let's build a reasonable model to the sill.

##### make_variogram function

We use the make_variogram function to make a variogram model

* a dictionary for compact storage of the variogram model parameters to pass into plotting (below), kriging and simulation methods

The variogram model parameter include:

* **nug** - nugget effect contribution to sill
* **nst** - number of nested structures (1 or 2)
* **it** - type for this nested structure (1 - spherical, 2 - exponential, 3 - Gaussian)
* **cc** - contribution of each nested structure (contributions + nugget must sum to the sill)
* **azi** - the azimuth for this nested structure of the major direction, the minor is orthogonal
* **hmaj** - the range for this nested structure in the major direction
* **hmin** - the range for this nested structure in the minor direction

We increment it, cc, azi, hmaj, and hmin for the 1st and 2nd structures

* for only 1 structure plus optional nugget, omit the 2nd structure parameters and they will default to $cc2 = 0$, no contribution to the model

Here's my model:

```p
nug = 0.0; nst = 2
it1 = 1; cc1 = 0.6; azi1 = 45; hmaj1 = 350; hmin1 = 350                # first structure
it2 = 1; cc2 = 0.4; azi2 = 45; hmaj2 = 9999.9; hmin2 = 400             # second structure
```

Some comments on our model:

* we model to the sill of 1.0, since we applied the normal score transform ($nug + cc1 + cc2 = 1.0$)

* we used 2 spherical structures to capture zontal anisotropy in the 045 azimuth

* since the experimental variogram exceeds the sill with trend or cyclicity we could have attempted trend modeling and then worked with the residual, but we will not do this for workflow brevity and simplicity

We input these model parameters to make a variogram model dictionary with the make_variogram function as follows:

```p
vario = GSLIB.make_variogram(nug,nst,it1,cc1,azi1,hmaj1,hmin1,it2,cc2,azi2,hmaj2,hmin2)
```

##### vmodel function

To plot the variogram we use the vmodel function to project the model in the major and minor directions

The inputs for vmodel are:

* **nlag** - the number of points along the variogram to calculate for the projection

* **xlag** - the size of a lag for the projection

* **azm** - the direction of the projection in azimuth (this is all we need since we are working in 2D)

* **vario** - the variogram model dictionary from the make_variogram function (above)

Note: this function is just for visualization by projecting the variogram model in a direction, so the convention is to use a very small **xlag** and large **nlag** for a high resolution display of the variogram model

The outputs from the vmodel program include:

* **index** - the lag number for the projection

* **lag distance** - the distance offset along the projection (the **h** in the variogram plot)

* **variogram** - the variogram value at the lag distance for the projection (the $\gamma$(**h**) in the variogram plot)

* **covariance function** - the covariance function at the lag distance for the projection (for the C(**h**) plot)

* **correlogram** - the correlogram at the lag distance for the projection (for the $\rho$(**h**) plot)

We have 2 structures and no nugget effect.  We needed the 2nd structure to capture the zonal anisotropy in the 045 direction.  Let's calculate the variogram model in these directions and plot them with the experimental variograms.

```python
nug = 0.0; nst = 2                                             # 2 nest structure variogram model parameters
it1 = 1; cc1 = 0.6; azi1 = azi_mat[imajor]; hmaj1 = 350; hmin1 = 350
it2 = 1; cc2 = 0.4; azi2 = azi_mat[imajor]; hmaj2 = 9999.9; hmin2 = 400

vario = GSLIB.make_variogram(nug,nst,it1,cc1,azi1,hmaj1,hmin1,it2,cc2,azi2,hmaj2,hmin2) # make model object
nlag = 70; xlag = 10;                                          # project the model in the 045 azimuth
index_maj,h_maj,gam_maj,cov_maj,ro_maj = geostats.vmodel(nlag,xlag,azi_mat[imajor],vario)                                                     # project the model in the 135 azimuth
index_min,h_min,gam_min,cov_min,ro_min = geostats.vmodel(nlag,xlag,azi_mat[iminor],vario)

plt.subplot(1,2,1)
plt.plot(lag[imajor,:],gamma[imajor,:],'x',color = 'black',label = 'Azimuth ' +str(azi_mat[imajor]))
plt.plot([0,2000],[1.0,1.0],color = 'black')
plt.plot(h_maj,gam_maj,color = 'black')
plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
plt.ylabel(r'$\gamma \bf(h)$')
plt.title('Directional NSCORE Porosity Variogram - Major ' + str(azi_mat[imajor]) + ' Azimuth')
plt.xlim([0,700])
plt.ylim([0,1.8])
plt.legend(loc='upper left')

plt.subplot(1,2,2)
plt.plot(lag[iminor,:],gamma[iminor,:],'x',color = 'black',label = 'Azimuth ' +str(azi_mat[6]))
plt.plot([0,2000],[1.0,1.0],color = 'black')
plt.plot(h_min,gam_min,color = 'black')
plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
plt.ylabel(r'$\gamma \bf(h)$')
plt.title('Directional NSCORE Porosity Variogram  - Minor ' + str(azi_mat[iminor]) + ' Azimuth')
plt.xlim([0,700])
plt.ylim([0,1.8])
plt.legend(loc='upper left')

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.2, top=1.6, wspace=0.2, hspace=0.3)
plt.show()
```

#### Interactive Variogram Modeling

The following code uses the ipywidgets and matplotlib packages to build a method for interactive variogram modeling.

```python
# interactive calculation of the sample set (control of source parametric distribution and number of samples)
l = widgets.Text(value='                              Variogram Modeling, Michael Pyrcz, Associate Professor, The University of Texas at Austin',layout=Layout(width='950px', height='30px'))
nug = widgets.FloatSlider(min = 0, max = 1.0, value = 0.0, step = 0.1, description = 'nug',orientation='vertical',layout=Layout(width='90px', height='200px'))
nug.style.handle_color = 'gray'
it1 = widgets.Dropdown(options=['Spherical', 'Exponential', 'Gaussian'],value='Spherical',
    description='Type1:',disabled=False,layout=Layout(width='200px', height='30px'))
c1 = widgets.FloatSlider(min=0.0, max = 1.0, value = 0.0, description = 'c1',orientation='vertical',layout=Layout(width='90px', height='200px'))
c1.style.handle_color = 'blue'
hmaj1 = widgets.FloatSlider(min=0.01, max = 10000.0, value = 0.01, step = 100.0, description = 'hmaj1',orientation='vertical',layout=Layout(width='90px', height='200px'))
hmaj1.style.handle_color = 'red'
hmin1 = widgets.FloatSlider(min = 0.01, max = 10000.0, value = 0.01, step = 100.0, description = 'hmin1',orientation='vertical',layout=Layout(width='90px', height='200px'))
hmin1.style.handle_color = 'green'

it2 = widgets.Dropdown(options=['Spherical', 'Exponential', 'Gaussian'],value='Spherical',
    description='Type2:',disabled=False,layout=Layout(width='200px', height='30px'))
c2 = widgets.FloatSlider(min=0.0, max = 1.0, value = 0.0, description = 'c2',orientation='vertical',layout=Layout(width='90px', height='200px'))
c2.style.handle_color = 'blue'
hmaj2 = widgets.FloatSlider(min=0.01, max = 10000.0, value = 0.01, step = 100.0, description = 'hmaj2',orientation='vertical',layout=Layout(width='90px', height='200px'))
hmaj2.style.handle_color = 'red'
hmin2 = widgets.FloatSlider(min = 0.01, max = 10000.0, value = 0.01, step = 100.0, description = 'hmin2',orientation='vertical',layout=Layout(width='90px', height='200px'))
hmin2.style.handle_color = 'green'

ui1 = widgets.HBox([nug,it1,c1,hmaj1,hmin1,it2,c2,hmaj2,hmin2],)                   # basic widget formatting
#ui2 = widgets.HBox([it2,c2,hmaj2,hmin2],)                   # basic widget formatting
ui = widgets.VBox([l,ui1],)

def convert_type(it):
    if it == 'Spherical':
        return 1
    elif it == 'Exponential':
        return 2
    else:
        return 3

def f_make(nug,it1,c1, hmaj1, hmin1, it2, c2, hmaj2, hmin2):                       # function to take parameters, make sample and plot
    text_trap = io.StringIO()
    sys.stdout = text_trap

    it1 = convert_type(it1); it2 = convert_type(it2)
    if c2 > 0.0:
        nst = 2
    else:
        nst = 1

    vario = GSLIB.make_variogram(nug,nst,it1,c1,azi_mat[imajor],hmaj1,hmin1,it2,c2,azi2,hmaj2,hmin2) # make model object
    nlag = 70; xlag = 10;
    index_maj,h_maj,gam_maj,cov_maj,ro_maj = geostats.vmodel(nlag,xlag,azi_mat[imajor],vario)   # project the model in the major azimuth                                                  # project the model in the 135 azimuth
    index_min,h_min,gam_min,cov_min,ro_min = geostats.vmodel(nlag,xlag,azi_mat[iminor],vario) # project the model in the minor azimuth

    plt.subplot(2,2,1)
    plt.plot(lag[imajor,:],gamma[imajor,:],'x',color = 'black',label = 'Azimuth ' +str(azi_mat[2]))
    plt.plot([0,2000],[1.0,1.0],color = 'black')
    plt.plot(h_maj,gam_maj,color = 'black')
    plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
    plt.ylabel(r'$\gamma \bf(h)$')
    plt.title('Horizontal Major Directional NSCORE Porosity Variogram - Major ' + str(azi_mat[imajor]) + 'Azimuth')
    plt.xlim([0,700])
    plt.ylim([0,1.8])
    plt.legend(loc='upper left')

    plt.subplot(2,2,2)
    plt.plot(lag[iminor,:],gamma[iminor,:],'x',color = 'black',label = 'Azimuth ' +str(azi_mat[6]))
    plt.plot([0,2000],[1.0,1.0],color = 'black')
    plt.plot(h_min,gam_min,color = 'black')
    plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
    plt.ylabel(r'$\gamma \bf(h)$')
    plt.title('Horizontal Minor Directional NSCORE Porosity Variogram - Minor ' + str(azi_mat[iminor]) + 'Azimuth')
    plt.xlim([0,700])
    plt.ylim([0,1.8])
    plt.legend(loc='upper left')

    plt.subplot(2,2,3)
    plt.plot([0,2000],[1.0,1.0],color = 'black')
    plt.plot(h_maj,gam_maj,color = 'black',label = 'Azimuth ' + str(azi_mat[imajor]))
    plt.plot(h_min,gam_min,color = 'black',label = 'Azimuth ' + str(azi_mat[iminor]))
    deltas = [22.5, 45, 67.5];
    ndelta = len(deltas); hd = np.zeros(ndelta); gamd = np.zeros(ndelta);
    color=iter(cm.plasma(np.linspace(0,1,ndelta)))
    for delta in deltas:
        index,hd,gamd,cov,ro = geostats.vmodel(nlag,xlag,azi_mat[imajor]+delta,vario);
        c=next(color)
        plt.plot(hd,gamd,color = c,label = 'Azimuth ' + str(azi_mat[imajor]+delta))
    plt.xlabel(r'Lag Distance $\bf(h)$, (m)')
    plt.ylabel(r'$\gamma \bf(h)$')
    plt.title('Interpolated Azimuth NSCORE Porosity Variogram Models')
    plt.xlim([0,700])
    plt.ylim([0,1.8])
    plt.legend(loc='upper left')

    plt.subplots_adjust(left=0.0, bottom=0.0, right=2.2, top=1.5, wspace=0.3, hspace=0.3)
    plt.show()

# connect the function to make the samples and plot to the widgets
interactive_plot = widgets.interactive_output(f_make, {'nug':nug, 'it1':it1,'c1':c1, 'hmaj1':hmaj1, 'hmin1':hmin1, 'it2':it2, 'c2':c2, 'hmaj2':hmaj2, 'hmin2':hmin2})
interactive_plot.clear_output(wait = True)               # reduce flickering by delaying plot updating
```

### Interactive Nested Variogram Modeling Demostration

* select the nested structures and their types, contributions and major and minor ranges

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

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

### The Problem

Fit a positive definite variogram model based on the addition of multiple structures each describing spatial components of the feature variance

* **nug**: nugget effect

* **c1 / c2**: contributions of the sill

* **hmaj1 / hmaj2**: range in the major direction

* **hmin1 / hmin2**: range in the minor direction

```python
display(ui, interactive_plot)                            # display the interactive plot
```

    VBox(children=(Text(value='                              Variogram Modeling, Michael Pyrcz, Associate Professo…

    Output()

#### Comments

This was a basic demonstration of vairogram modeling for spatial continuity analysis. Much more could be done, I have other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

#### The Author:

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

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

For more about Michael check out these links:

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

#### Want to Work Together?

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

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

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

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

I'm always happy to discuss,

*Michael*

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

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

```python

```

```python

```


# MDL DB roll damping classification
This notebook is comparing a cubic, a quadratic and a linear rolldecay model on the whole database.

```python
%matplotlib inline
%load_ext autoreload
%autoreload 2
```

```python
#from jupyterthemes import jtplot
#jtplot.style(theme='onedork', context='notebook', ticks=True, grid=False)
```

```python
import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)
import numpy as np
import os
import matplotlib.pyplot as plt
#from pylab import rcParams
#rcParams['figure.figsize'] = 15, 5

import rolldecay.database as database
from mdldb.tables import Run
from rolldecayestimators.transformers import CutTransformer, LowpassFilterDerivatorTransformer, ScaleFactorTransformer, OffsetTransformer
from rolldecayestimators.direct_estimator_cubic import EstimatorCubic, EstimatorQuadraticB, EstimatorLinear

import data
import copy

from sklearn.pipeline import Pipeline
import seaborn as sns

from sympy.physics.vector.printing import vpprint, vlatex
from IPython.display import display, Math, Latex
from rolldecay.paper_writing import save_fig
```

## Cubic model
Has cubic damping and stiffness:

```python
Math(vlatex(EstimatorCubic.roll_decay_equation))
```

## Quadratic model
Has quadratic damping and stiffness:

```python
Math(vlatex(EstimatorQuadraticB.roll_decay_equation))
```

## Linear model
Has linear damping and stiffness:

```python
Math(vlatex(EstimatorLinear.roll_decay_equation))
```

```python
limit_score = 0.8
df_cubic = database.load(rolldecay_table_name='rolldecay_cubic_b', limit_score=limit_score,
                         exclude_table_name='rolldecay_exclude')
df_quadratic = database.load(rolldecay_table_name='rolldecay_quadratic_b', limit_score=limit_score,
                         exclude_table_name='rolldecay_exclude')
df_linear = database.load(rolldecay_table_name='rolldecay_linear_b', limit_score=limit_score,
                         exclude_table_name='rolldecay_exclude')
```

```python
df_cubic.describe()
```

```python
df_quadratic.describe()
```

```python
df_linear.describe()
```

## Comparison of the r2 score with the three models

```python
N=60

df_cubic['estimator'] = 'cubic'
df_quadratic['estimator'] = 'quadratic'
df_linear['estimator'] = 'linear'

df_comparison = df_cubic.copy()
df_comparison = df_comparison.append(df_quadratic, ignore_index=True, sort=False)
df_comparison = df_comparison.append(df_linear, ignore_index=True, sort=False)

bins = np.linspace(df_comparison['score'].min(), df_comparison['score'].max(),N)

groups=df_comparison.groupby(by='estimator', sort=False)
fig,axes=plt.subplots(nrows=len(groups))
for ax,(estimator, group) in zip(axes,groups):
    label = '%s' % estimator
    group['score'].hist(bins=bins, ax=ax, label = label)
    ax.set_title(label)
ax.set_xlabel('score')

ymaxs = []
for ax in axes:
    ylims = ax.get_ylim()
    ymaxs.append(ylims[1])
for ax in axes:
    ax.set_ylim(0,np.max(ymaxs))

fig.tight_layout()

```

```python
fig,ax=plt.subplots()
df_comparison2 = pd.merge(left=df_cubic, right=df_quadratic, how='inner', left_index=True,
                          right_index=True, suffixes=('','_quadratic'))
df_comparison2 = pd.merge(left=df_comparison2, right=df_linear, how='inner', left_index=True,
                          right_index=True, suffixes=('','_linear'))

df_comparison2['score_cubic-score_quadratic']=df_comparison2['score']-df_comparison2['score_quadratic']
df_comparison2['score_cubic-score_linear']=df_comparison2['score']-df_comparison2['score_linear']
df_comparison2['score_quadratic-score_linear']=df_comparison2['score_quadratic']-df_comparison2['score_linear']

df_comparison2.plot(y='score_cubic-score_quadratic', style='o', ax=ax, alpha=0.50)
df_comparison2.plot(y='score_cubic-score_linear', style='*', ax=ax, alpha=0.50)

ax.set_title('Score comparison')
ax.grid(True)
ax.legend()
```

... it looks like cubic model will not so much more accurate than the quadratic

## Closer look on the cubic coefficients

```python
fig,axes=plt.subplots(ncols=3)
for i in range(0,3):
    key = 'B_%iA' % (i+1)
    ax=axes[i]
    df_cubic[key].hist(bins=40, ax=ax)
    ax.set_title(key)

fig,axes=plt.subplots(ncols=3)
for i in range(0,3):
    key = 'C_%iA' % (1+(i*2))
    ax=axes[i]
    df_cubic[key].hist(bins=40, ax=ax)
    ax.set_title(key)

```

## Compare signals
select the run where cubic and quadratic model differ the most

```python
run_id = df_comparison2['score_cubic-score_quadratic'].abs().idxmax()
```

```python
df_comparison2.loc[run_id]['score_cubic-score_quadratic']
```

```python
db = database.get_db()
```

```python
db_run = db.session.query(Run).get(int(run_id))
df = database.load_run(db_run, save_as_example=False)
```

```python
df.plot(y='phi')
```

```python
lowpass_filter = LowpassFilterDerivatorTransformer(cutoff=2, minimum_score=0.99)
#scaler = ScaleFactorTransformer(scale_factor=db_run.model.scale_factor)  # dummy value None for now
cutter = CutTransformer(phi_max=np.deg2rad(9), phi_min=np.deg2rad(0.25))
offset_transformer = OffsetTransformer()

steps = [
    ('filter',lowpass_filter),
#    ('scaler',scaler),  # Is froude scaling a good idea??
    ('cutter', cutter),
#    ('offset_transformer',offset_transformer)
]

preprocessor = Pipeline(steps) # define the pipeline object.
preprocessor.fit(df)
X = preprocessor.transform(df)
```

```python
X.plot(y='phi')
```

```python
meta_data = df_cubic.loc[run_id]
cubic_estimator = EstimatorCubic.load(**meta_data, X=X)
cubic_estimator.plot_fit()
```

```python
meta_data = df_quadratic.loc[run_id]
quadratic_estimator = EstimatorQuadraticB.load(**meta_data, X=X)
quadratic_estimator.plot_fit()

```

```python
meta_data = df_linear.loc[run_id]
linear_estimator = EstimatorLinear.load(**meta_data, X=X)
linear_estimator.plot_fit()
```

```python
lowpass_filter = LowpassFilterDerivatorTransformer(cutoff=1, minimum_score=0.999)
#scaler = ScaleFactorTransformer(scale_factor=db_run.model.scale_factor)  # dummy value None for now
cutter = CutTransformer(phi_max=np.deg2rad(15), phi_min=np.deg2rad(0.25))
offset_transformer = OffsetTransformer()

steps = [
    ('filter',lowpass_filter),
#    ('scaler',scaler),  # Is froude scaling a good idea??
    ('cutter', cutter),
#    ('offset_transformer',offset_transformer)
]

preprocessor = Pipeline(steps) # define the pipeline object.
preprocessor.fit(df)
X = preprocessor.transform(df)
```

```python
meta_data = df_linear.loc[run_id]
linear_estimator = EstimatorLinear(fit_method='derivation')
linear_estimator.fit(X=X)
linear_estimator.plot_fit()
```

```python
meta_data = df_linear.loc[run_id]
linear_estimator = EstimatorCubic(fit_method='derivation')
linear_estimator.fit(X=X)
linear_estimator.plot_fit()
```

```python
meta_data = df_linear.loc[run_id]
linear_estimator = EstimatorCubic(fit_method='integration')
linear_estimator.fit(X=X)
linear_estimator.plot_fit()
```

```python
cubic_estimator.omega0
```

```python
cubic_estimator.calculate_amplitudes_and_damping()
cubic_estimator.X_amplitudes.plot(x='phi',y='omega0')
```

```python
#cubic_estimator.plot_peaks()
```

select the run where quadartic and linear model differ the most

```python
import matplotlib.pyplot as plt

run_id = df_comparison2['score_quadratic-score_linear'].abs().idxmax()
db_run = db.session.query(Run).get(int(run_id))
df = database.load_run(db_run, save_as_example=True)

#preprocessor['scaler'].scale_factor=db_run.model.scale_factor
X = preprocessor.transform(df)
fig,ax=plt.subplots()
with plt.style.context('paper_no_tex'):

    #fig.set_size_inches(5,3)
    meta_data = df_cubic.loc[run_id]
    cubic_estimator = EstimatorCubic.load(**meta_data, X=X)
    cubic_estimator.plot_fit(ax=ax)
    ax.get_legend().remove()
    ax.set_xlabel('')

    meta_data = df_quadratic.loc[run_id]
    quadratic_estimator = EstimatorQuadraticB.load(**meta_data, X=X)
    quadratic_estimator.plot_fit(ax=ax, model_test=False)
    ax.get_legend().remove()
    ax.set_xlabel('')

    meta_data = df_linear.loc[run_id]
    linear_estimator = EstimatorLinear.load(**meta_data, X=X)
    linear_estimator.plot_fit(ax=ax, model_test=False)
    ax.legend(['Model test', 'Linear', 'Quadratic', 'Cubic'])

    ax.set_xlim(X.index[0],25)
    ax.grid(True)

    plt.tight_layout()
    save_fig(fig=fig, name='roll_decay_model_compare')

```

```python
size = (3.0,3.0)
with plt.style.context('paper'):
    fig,ax=plt.subplots()
    fig.set_size_inches(size)
    linear_estimator.plot_damping(ax=ax)
    quadratic_estimator.plot_damping(ax=ax, include_model_test=False)
    cubic_estimator.plot_damping(ax=ax, include_model_test=False)
    #ax.legend(['Model test', 'Linear', 'Quadratic', 'Cubic'])
    ax.get_legend().remove()
    ax.grid(True)
    plt.tight_layout()
    save_fig(fig=fig, name='roll_decay_damping')

```

```python
with plt.style.context('paper'):
    fig,ax=plt.subplots()
    fig.set_size_inches(size)
    linear_estimator.plot_amplitude(ax=ax, include_model_test=True)
    quadratic_estimator.plot_amplitude(ax=ax, include_model_test=False)
    cubic_estimator.plot_amplitude(ax=ax, include_model_test=False)
    ax.set_ylabel(r'$\phi_a$ [deg]')
    ax.set_xlabel(r'Time [s]')

    ax.legend(['Model test', 'Linear', 'Quadratic', 'Cubic'])
    ax.set_xlim(X.index[0],25)
    ax.grid(True)
    plt.tight_layout()
    save_fig(fig=fig, name='roll_decay_amplitude')
```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```


```python
import numpy as np
import sympy as sp # for calculating derivative
import matplotlib.pyplot as plt

#import seaborn as sns # it will set to SNS style
#sns.reset_orig() # set back to the Matplotlib style
#sns.set() # set to sns style
# set the font
#plt.rcParams["font.family"] = 'arial'
%matplotlib notebook

from lmfit import minimize, Model, Minimizer, Parameters, Parameter, report_fit
```

```python
print 'Sm2Zr2O7', np.array([85, 130, 165, 210])/8.065 # PRB Raman Sm2Zr2O7
print 'Sm2Ti2O7', np.array([87, 132, 158, 270])/8.065 # PRB Raman Sm2Ti2O7
print 'Sm2Ti2O7',np.array([172,212.7])/8.065 # JPCM for Sm2Ti2O7
print np.array([23,35])*8.065 # Our Nd2Zr2O7
```

    Sm2Zr2O7 [ 10.53936764  16.11903286  20.45877247  26.03843769]
    Sm2Ti2O7 [ 10.78735276  16.36701798  19.59082455  33.47799132]
    Sm2Ti2O7 [ 21.3267204   26.37321761]
    [ 185.495  282.275]

# # Calculate the CEF heat capacity

```python
##### difine function for calculate and plot crystal field heat capacity
kb = 1.38*10**-23;
nmol = 6*10**23;
R = 8.31;
t = sp.symbols('t')

def cefcp(t, elevels, degen):
    # this fun retures a sympy object containing symbol 't'
    e = elevels*11.606*kb; g = degen
    energy = nmol*sum([ei*gi*sp.exp(-ei/kb/t) for ei,gi in zip(e, g)])/sum([gi*sp.exp(-ei/kb/t) for ei,gi in zip(e, g)])
    cp = sp.diff(energy,t)# sp.Sum can not sum over a python list, but sum can and sp.diff works on the summation
    return cp

def lamcefcp(elevels, degen):
    # return a fun that take the temperature as varable which can be array
    cefcpv = sp.lambdify(t, cefcp(t, elevels, degen), modules=['numpy', 'sympy'])
    return cefcpv

def plotcefcp(tRange, npts, elevels, degen):
    temps = np.linspace(tRange[0],tRange[1],num=npts,endpoint=True)
    cefcpv = lamcefcp(elevels, degen)
    cefcpdat = cefcpv(temps)
    #fig, ax = plt.subplots()
    plt.plot(temps, cefcpdat)
    plt.minorticks_on()
    plt.xlabel(r"T (K)",size=14)
    plt.ylabel(r'$C_p$ (J/mol/K)',size=14)
    fig.show()
    return

```

```python
e1 = np.array([0, 23, 34, 36, 106]); #e are in meV, now in J
g1 = np.array([2, 2, 2, 2, 2]); #Degenercy
lamcefcp(e1, g1)(300)
```

    3.7917511970696358

```python
# Ｆｉｔ　ｔｈｅ　ｒｅｐｏｒｔｅｄ　Ｓｍ２Ｚｒ２Ｏ７　ＣＥＦ　Ｃｐ　ｄａｔａ
# load data
fnames = [r'D:\9_Program files\crystal_field\szo_cp.txt',
          r'D:\5_Neutron Scattering\7_Merlin_Mar_2016_SmZrO\szo_cp_diditalized.txt',
          r'D:\5_Neutron Scattering\7_Merlin_Mar_2016_SmZrO\sto_cp_diditalized.txt']
dat = np.genfromtxt(fnames[0]) # Spectre calculated
data = np.genfromtxt(fnames[1]) # data
data0 = np.genfromtxt(fnames[2]) # data
```

```python
# Defined the fitting function
kb = 1.38*10**-23;
nmol = 6*10**23;
R = 8.31;
t = sp.symbols('t')
# The the fitted parameters can not be list but many varible
def szocefcp(x,e1,e2):# e1 and e2 in meV
    # this fun retures a sympy object containing symbol 't'
    e = np.array([0,e1,e2,130,157,168,183])*11.606*kb # includeing the first multiplet energy for Sm2Zr2O6
    g = [2.,2.,2.,2,2,2,2]
    energy = nmol*sum([ei*gi*sp.exp(-ei/kb/t) for ei,gi in zip(e, g)])/sum([gi*sp.exp(-ei/kb/t) for ei,gi in zip(e, g)])
    #cp = sp.diff(energy,t)# sp.Sum can not sum over a python list, but sum can and sp.diff works on the summation
    cpv = sp.lambdify(t, sp.diff(energy,t), modules=['numpy', 'sympy'])
    return cpv(x)

def fitplot(dat,title,fitReg):
    mod=Model(szocefcp)
    params = mod.make_params()
    params['e1'].set(value=16, vary=True, min=0, max=40)
    params['e2'].set(value=26, vary=True, min=0, max=40)

    mask = (fitReg[0]< dat[:,0]) & (dat[:,0] <fitReg[1]) # change the fitting range
    result = mod.fit(dat[mask,1], x=dat[mask,0], params = params)
    print result.fit_report(modelpars=None, show_correl=False)
    #print 'fitted:','\n', result.params.valuesdict()

    fig, ax = plt.subplots(figsize=(5, 4))
    ax.plot(dat[:,0], dat[:,1],marker='o',mec='black',mew=1,mfc='none',linestyle='None', label='Data')
    ax.plot(dat[mask,0], result.best_fit, 'r-', label='Fitting')
    #ax.plot(dat[:,0],cefcp0(dat[:,0],15,35), label='Simu')

    params.loads(result.params.dumps())
    ax.text(200, 0.5,'Fitted'+'\n'+str(np.round(params['e1'].value,decimals=1))+'\n'
            +str(np.round(params['e2'].value,decimals=1))+' meV',
        verticalalignment='bottom', horizontalalignment='left',
        #transform=ax.transAxes,
        color='black', fontsize=15)
    plt.xlabel('T (K)',size=14)
    plt.ylabel('$C_p$ (J/K/mol)',size=14)

    plt.minorticks_on()
    plt.xticks( color='k', size=14)
    plt.yticks( color='k', size=14)

    ax.legend(loc='best',numpoints=1,frameon=False,framealpha=1,fontsize=14)
    #plt.title(title)
    #fig.savefig(r'D:\5_Neutron Scattering\7_Merlin_Mar_2016_SmZrO\szo_cp_diditalized.pdf', bbox_inches="tight",verbose=True)
    plt.show()
    return

# plot data and simulation
fig, ax = plt.subplots(figsize=(5, 4))
#sns.set(color_codes=True)
#ax.scatter(dat[:,0], dat[:,1],facecolors='none', label='Data')
ax.plot(dat[:,0], dat[:,1], marker='o',mec='black',mew=0.5,mfc='none',linestyle='None',label='Data') # mfc=None not work
ax.plot(dat[:,0],szocefcp(dat[:,0],6,20),c='r', label='Simu')

ax.legend(loc='best',numpoints=1,frameon=False,framealpha=1,fontsize=14)
plt.show()
```

    <IPython.core.display.Javascript object>

```python
fitplot(data,r'Sm$_2$Zr$_2$O$_7$ Cp',[10,300])
```

    [[Model]]
        Model(szocefcp)
    [[Fit Statistics]]
        # function evals   = 24
        # data points      = 43
        # variables        = 2
        chi-square         = 4.643
        reduced chi-square = 0.113
        Akaike info crit   = -91.709
        Bayesian info crit = -88.187
    [[Variables]]
        e1:   11.2619719 +/- 0.247439 (2.20%) (init= 16)
        e2:   28.8603212 +/- 0.875964 (3.04%) (init= 26)

    <IPython.core.display.Javascript object>

```python
# Fit and plot SmTiO Cp
fitplot(data0,r'Sm$_2$Ti$_2$O$_7$ Cp',[10,250])# used the higher multiplet levels of SmZrO
```

    [[Model]]
        Model(szocefcp)
    [[Fit Statistics]]
        # function evals   = 18
        # data points      = 33
        # variables        = 2
        chi-square         = 72.011
        reduced chi-square = 2.323
        Akaike info crit   = 29.750
        Bayesian info crit = 32.743
    [[Variables]]
        e1:   12.5610688 +/- 1.475858 (11.75%) (init= 16)
        e2:   21.7728990 +/- 3.321382 (15.25%) (init= 26)

    <IPython.core.display.Javascript object>

```python
# cal the crystal field heat capacity
# Nd2Zr2O7
e1 = np.array([0, 23, 34, 36, 106]); #e are in meV, now in J
g1 = np.array([2, 2, 2, 2, 2]); #Degenercy
# Sm2Ti2O7: take the JPCM pars
e1 = np.array([0, 21, 26, 136, 147,163,176]); #e are in meV, now in J
g1 = np.array([2, 2, 2, 2, 2, 2,2]); #Degenercy
plt.figure(figsize=[5,4])
plt.plot(data0[:,0], data0[:,1],marker='o',mec='b',mew=1,mfc='none',linestyle='None', label='Data')
plotcefcp([1,400], 400, e1, g1)
#ax.set_ylim([0,10])
```

    <IPython.core.display.Javascript object>

```python
# cal the crystal field heat capacity of Sm2Zr2O7
plt.figure(figsize=[5,4])
e1 = np.array([0, 13, 34, 128, 157, 167, 183]); #e are in meV, now in J
g1 = np.array([2, 2, 2, 2, 2, 2, 2]); #Degenercy

plotcefcp([1,300], 400, e1, g1)
e1 = np.array([0, 13, 34]); #e are in meV, now in J
g1 = np.array([2, 2, 2]); #Degenercy
plotcefcp([1,300], 400, e1, g1)
#ax.set_ylim([0,10])
```

    <IPython.core.display.Javascript object>

# Calculated susceptibility of single crystal Pyrochlore from the SPECTRE data

```python
# Ｒｏｔａｔｉｏｎ　ｍａｔｒｉｘ
#from http://stackoverflow.com/questions/6802577/python-rotation-of-3d-vector

# first method
import numpy as np
from scipy.linalg import expm3, norm
def M(axis, theta):
    return expm3(np.cross(np.eye(3), axis/norm(axis)*theta))
#axis, theta = [1,1,1], 2*np.pi/3
#print M(axis, theta)

# Scond method
import numpy as np
def rotation_matrix(axis, theta):
    """
    Return the rotation matrix associated with counterclockwise rotation about
    the given axis by theta radians.
    """
    axis = np.asarray(axis)
    theta = np.asarray(theta)
    axis = axis/np.sqrt(np.dot(axis, axis))
    a = np.cos(theta/2.0)
    b, c, d = -axis*np.sin(theta/2.0)
    aa, bb, cc, dd = a*a, b*b, c*c, d*d
    bc, ad, ac, ab, bd, cd = b*c, a*d, a*c, a*b, b*d, c*d
    m =  np.array([[aa+bb-cc-dd, 2*(bc+ad), 2*(bd-ac)],
                   [2*(bc-ad), aa+cc-bb-dd, 2*(cd+ab)],
                   [2*(bd+ac), 2*(cd-ab), aa+dd-bb-cc]])
    m[m<=0.0001] = 0 # zero out the samll values
    return m

print rotation_matrix([1,1,1],2*np.pi/3)
```

    [[ 0.  0.  1.]
     [ 1.  0.  0.]
     [ 0.  1.  0.]]

```python
import numpy as np
from numpy import linalg as LA
import string
from scipy import interpolate

def crysXT(fpath, Hdirections):
    '''
    'fpath' is a string giveing the path of the T-Chiz-Chixy-ChiPowder file produceed by SPECTRE;
    'Hdirections' is a n by 3 array showing the field directions and it can be normalized to be 1 oe later.
    '''

    ## load the chi-T data of Spectre: T,  chiz, (chix+chiy)/2, chi_powder
    rule = string.maketrans('D', 'E') # make a translation table (rules) for translating a string (D to E in the numbers)
    data = np.genfromtxt(fpath, usecols=(0,1,2),names='T, chiz, chixy', \
                         converters = {1: lambda val: float(val.translate(rule)),
                                       2: lambda val: float(val.translate(rule))})

    chixyz = np.vstack((data['chixy'],data['chixy'],data['chiz'])).T
    #chixyz = np.vstack((data[:,2],data[:,2],data[:,1])).transpose()

    ## prepare the matrix to store the crystal chi
    crystal = np.zeros((data.shape[0],Hdirections.shape[0]+1))
    crystal[:,0] = data['T']

    Hdirections = (Hdirections.T/np.linalg.norm(Hdirections, axis=1)).T # normalize the field to be unit

    # the matrixes transforming the mag field to the 4 local frames on a tetrahedra
    #(from Owen's Paper'A general theory of anisotropic... ')
    r1 = np.array([[-2,1,1]/np.sqrt(6.),[0,-1,1]/np.sqrt(2.),[1,1,1]/np.sqrt(3.)])
    r2 = np.array([[-2,-1,-1]/np.sqrt(6.),[0,1,-1]/np.sqrt(2.),[1,-1,-1]/np.sqrt(3.)])
    r3 = np.array([[2,1,-1]/np.sqrt(6.),[0,-1,-1]/np.sqrt(2.),[-1,1,-1]/np.sqrt(3.)])
    r4 = np.array([[2,-1,1]/np.sqrt(6.),[0,1,1]/np.sqrt(2.),[-1,-1,1]/np.sqrt(3.)])

    for idx, H in enumerate(Hdirections):
        #print H, idx
        #chi_atom = [chix,chiy,chiz] dot [hx COSa, hy cosb, hz cosc] /mod(H) where cosa=hx/mod(H) and mod(H)=1
        # [hx COSa, hy cosb, hz cosc] for the for the four atoms
        h1, h2, h3, h4 = np.dot(r1,H)**2, np.dot(r2,H)**2,\
                         np.dot(r3,H)**2, np.dot(r4,H)**2
        #print h1, h2, h3, h4

        # sum up the magnetizations of the four atoms and the average
        crystal[:,idx+1] = (np.dot(chixyz,h1)+np.dot(chixyz,h2)+ \
                            np.dot(chixyz,h3)+np.dot(chixyz,h4))/4.
    return crystal

import matplotlib.pyplot as plt
% matplotlib inline

def plotXT(data):
    fig, ax = plt.subplots(figsize = (5,4))
    ax.plot(data[:,0],data[:,1]**-1,label = '100')
    ax.plot(data[:,0],data[:,2]**-1,label = '110')
    ax.plot(data[:,0],data[:,3]**-1,label = '111')
    ax.minorticks_on()

    plt.xlabel(r"T (K)",size=14)
    plt.ylabel(r'$\frac{1}{\chi}$ (emu/mol)',size=14)
    plt.legend(loc = 'best')
    fig.show()
    return fig,ax

def crysMH(fpath, Hdirections, Hrange, npts):
    '''
    'fpath' is a string giveing the path of the H-Mx-My-Mz file produceed by SPECTRE;
    'Hdirections' is a n by 3 array showing the field directions and it can be normalized to be 1 oe later.
    ' Hrange' is [Hmin,Hmax], Hmax should be smaller than the max field in the fpath file.
    'npts' is number of points
    '''
    ## load the data of Spectre
    rule = string.maketrans('D', 'E') # make a translation table (rules) for translating a string (D to E in the numbers)
    data = np.genfromtxt(fpath, usecols=(0,1,2,3),names='Hz, mzx, mzy, mzz', \
                         converters = {1: lambda val: float(val.translate(rule)),
                                       2: lambda val: float(val.translate(rule)),
                                       3: lambda val: float(val.translate(rule))})
    # the function for calculating the magnetization: g-tensor, a funciton of field and temperature
    # here it is not incorrect, we should take a Hx mxx, Hy myy, and Hz, mzz
    px = interpolate.interp1d(data['Hz'],data['mzx'])
    py = interpolate.interp1d(data['Hz'],data['mzy'])
    pz = interpolate.interp1d(data['Hz'],data['mzz'])

    ## prepare the matrix to store the crystal chi
    hs = np.linspace(Hrange[0],Hrange[1],npts)
    crystal = np.zeros((hs.shape[0],Hdirections.shape[0]+1))
    crystal[:,0] = hs

    Hdirections = (Hdirections.T/np.linalg.norm(Hdirections, axis=1)).T # normalize the field to be 1 unit

    # the matrixes transforming the mag field to the 4 local frames on a tetrahedra
    #(from Owen's Paper'A general theory of anisotropic... ')
    r1 = np.array([[-2,1,1]/np.sqrt(6.),[0,-1,1]/np.sqrt(2.),[1,1,1]/np.sqrt(3.)])
    r2 = np.array([[-2,-1,-1]/np.sqrt(6.),[0,1,-1]/np.sqrt(2.),[1,-1,-1]/np.sqrt(3.)])
    r3 = np.array([[2,1,-1]/np.sqrt(6.),[0,-1,-1]/np.sqrt(2.),[-1,1,-1]/np.sqrt(3.)])
    r4 = np.array([[2,-1,1]/np.sqrt(6.),[0,1,1]/np.sqrt(2.),[-1,-1,1]/np.sqrt(3.)])

    for idx0, h in enumerate(hs):
        for idx1, H in enumerate(Hdirections):
            # the unit vector of the field in different local frames
            u1, u2, u3, u4 = np.abs(np.dot(r1,H)), np.abs(np.dot(r2,H)),\
                             np.abs(np.dot(r3,H)), np.abs(np.dot(r4,H))

            uss = np.array([u1, u2, u3, u4]) # a 4 by 3 matrix
            #h1, h2, h3, h4 = np.array([h*u1, h*u2, h*u3, h*u4])
            hss = uss*h # the magnetic field in the local frames
            mss = np.array([px(hss[:,0]), py(hss[:,1]), pz(hss[:,2])]).T # the moment vectors of the atoms in local frames

            m = np.sum(np.multiply(mss, uss))/4 # project the moment to field direction and sum up, average 4 atoms
            crystal[idx0,idx1+1] = m
    return crystal

#　ｔｒｙ　ｕｓｅ　ｇ　ｆａｃｔｏｒ　ａｎｄ　ｔｈｅｒｍａｌ　ｖａｖｅｒａｇｅ：　ｆａｉｌｅｄ
def crysMHg(fpath, Hdirections, Hrange, npts):
    '''
    'fpath' is a string giveing the path of the H-Mx-My-Mz file produceed by SPECTRE;
    'Hdirections' is a n by 3 array showing the field directions and it can be normalized to be 1 oe later.
    ' Hrange' is [Hmin,Hmax], Hmax should be smaller than the max field in the fpath file.
    'npts' is number of points
    '''
    ## load the data of Spectre
    rule = string.maketrans('D', 'E') # make a translation table (rules) for translating a string (D to E in the numbers)
    data = np.genfromtxt(fpath, usecols=(0,1,2,3),names='Hz, mzx, mzy, mzz', \
                         converters = {1: lambda val: float(val.translate(rule)),
                                       2: lambda val: float(val.translate(rule)),
                                       3: lambda val: float(val.translate(rule))})
    # the function for calculating the magnetization: g-tensor, a funciton of field and temperature
    # here it is not incorrect, we should take a Hx mxx, Hy myy, and Hz, mzz
    gfactor = np.array([[0.001,0,0],[0.0001,0,0],[0,0,5.2]])

    ## prepare the matrix to store the crystal chi
    hs = np.linspace(Hrange[0],Hrange[1],npts)
    crystal = np.zeros((hs.shape[0],Hdirections.shape[0]+1))
    crystal[:,0] = hs

    Hdirections = (Hdirections.T/np.linalg.norm(Hdirections, axis=1)).T # normalize the field to be 1 unit

    # the matrixes transforming the mag field to the 4 local frames on a tetrahedra
    #(from Owen's Paper'A general theory of anisotropic... ')
    r1 = np.array([[-2,1,1]/np.sqrt(6.),[0,-1,1]/np.sqrt(2.),[1,1,1]/np.sqrt(3.)])
    r2 = np.array([[-2,-1,-1]/np.sqrt(6.),[0,1,-1]/np.sqrt(2.),[1,-1,-1]/np.sqrt(3.)])
    r3 = np.array([[2,1,-1]/np.sqrt(6.),[0,-1,-1]/np.sqrt(2.),[-1,1,-1]/np.sqrt(3.)])
    r4 = np.array([[2,-1,1]/np.sqrt(6.),[0,1,1]/np.sqrt(2.),[-1,-1,1]/np.sqrt(3.)])

    for idx0, h in enumerate(hs):
        for idx1, H in enumerate(Hdirections):
            for atom in np.arrang((0,3)):
                u0 = np.abs(np.dot(r1,H))
                H0 = u0*h

                fun = lambda the, phi: gfactor*np.array([np.sin(the)*np.cos(phi),\
                                                        np.sin(the)*np.sin(phi),\
                                                       np.cos(the)])*2
                np.dot(H0,np.dot(gfactor, ))
            # the unit vector of the field in different local frames
            u1, u2, u3, u4 = np.abs(np.dot(r1,H)), np.abs(np.dot(r2,H)),\
                             np.abs(np.dot(r3,H)), np.abs(np.dot(r4,H))

            uss = np.array([u1, u2, u3, u4]) # a 4 by 3 matrix
            #h1, h2, h3, h4 = np.array([h*u1, h*u2, h*u3, h*u4])
            hss = uss*h # the magnetic field in the local frames
             # the moment vectors of the atoms in local frames
            hss

            m = np.sum(np.multiply(mss, uss))/4 # project the moment to field direction and sum up, average 4 atoms
            crystal[idx0,idx1+1] = m
    return crystal

def plotMH(data):
    fig, ax = plt.subplots(figsize = (5,4))
    ax.plot(data[:,0],data[:,1],label = '100')
    ax.plot(data[:,0],data[:,2],label = '110')
    ax.plot(data[:,0],data[:,3],label = '111')
    ax.minorticks_on()

    plt.xlabel(r"H (T)",size=14)
    plt.ylabel(r'M ($\mu_B/at$)',size=14)
    plt.legend(loc = 'best')
    fig.show()
    return fig,ax
```

```python

```

```python
# two methods to divide along row
a = np.arange(1,61, dtype=float).reshape((3,4,5)) # Create a 3d array
a.shape                                           # (3,4,5)

b = np.array([1.0, 2.0, 3.0, 4.0])                # Create a 1-d array
b.shape                                           # (4,)

#a / b                                             # Gives a ValueError

print a / b[:, np.newaxis]
#print a
print (a.swapaxes(1,2)/ b).swapaxes(1,2)
```

    [[[  1.           2.           3.           4.           5.        ]
      [  3.           3.5          4.           4.5          5.        ]
      [  3.66666667   4.           4.33333333   4.66666667   5.        ]
      [  4.           4.25         4.5          4.75         5.        ]]

     [[ 21.          22.          23.          24.          25.        ]
      [ 13.          13.5         14.          14.5         15.        ]
      [ 10.33333333  10.66666667  11.          11.33333333  11.66666667]
      [  9.           9.25         9.5          9.75        10.        ]]

     [[ 41.          42.          43.          44.          45.        ]
      [ 23.          23.5         24.          24.5         25.        ]
      [ 17.          17.33333333  17.66666667  18.          18.33333333]
      [ 14.          14.25        14.5         14.75        15.        ]]]
    [[[  1.           2.           3.           4.           5.        ]
      [  3.           3.5          4.           4.5          5.        ]
      [  3.66666667   4.           4.33333333   4.66666667   5.        ]
      [  4.           4.25         4.5          4.75         5.        ]]

     [[ 21.          22.          23.          24.          25.        ]
      [ 13.          13.5         14.          14.5         15.        ]
      [ 10.33333333  10.66666667  11.          11.33333333  11.66666667]
      [  9.           9.25         9.5          9.75        10.        ]]

     [[ 41.          42.          43.          44.          45.        ]
      [ 23.          23.5         24.          24.5         25.        ]
      [ 17.          17.33333333  17.66666667  18.          18.33333333]
      [ 14.          14.25        14.5         14.75        15.        ]]]

```python
#Ｆｏｒ　Ｎｄ２Ｚｒ２Ｏ７
fpath = 'D:/9_Program files/crystal_field/Spectre/NdZr2O7/chi_180915_2.dat'
Hs = np.array([[1,0,0],[1,1,0],[1,1,1]])
crystalx = crysXT(fpath, Hs)
#print crystalx
plotXT(crystalx);

```

```python
fpath = 'D:/9_Program files/crystal_field/Spectre/NdZr2O7/nd_mh.dat'
Hs = np.array([[1,0,0],[1,1,0],[1,1,1]])
crystal = crysMH(fpath, Hs, [0.1,4.5], 30)
#print crystalx
plotMH(crystal);
```

```python

```

```python

```


```python
import numpy as np
from scipy.integrate import odeint

import numpy as np
from sympy import symbols,sqrt,sech,Rational,lambdify,Matrix,exp,cosh,cse,simplify,cos,sin
from sympy.vector import CoordSysCartesian

#from theano.scalar.basic_sympy import SymPyCCode
#from theano import function
#from theano.scalar import floats

from IRI import *
from Symbolic import *
from scipy.integrate import simps
from ENUFrame import ENU

import astropy.coordinates as ac
import astropy.units as au
import astropy.time as at

class Fermat(object):
    def __init__(self,nFunc=None,neFunc=None,frequency = 120e6,type='s'):
        self.type = type
        self.frequency = frequency#Hz
        if nFunc is not None and neFunc is not None:
            self.nFunc = nFunc
            self.neFunc = neFunc
            self.eulerLambda, self.jacLambda = self.generateEulerEqnsSym(self.nFunc)
            return
        if nFunc is not None:
            self.nFunc = nFunc
            self.neFunc = self.n2ne(nFunc)
            self.eulerLambda, self.jacLambda = self.generateEulerEqnsSym(self.nFunc)
            return
        if neFunc is not None:
            self.neFunc = neFunc
            self.nFunc = self.ne2n(neFunc)
            self.eulerLambda, self.jacLambda = self.generateEulerEqnsSym(self.nFunc)
            return

    def loadFunc(self,file):
        '''Load symbolic functions'''
        data = np.load(file)
        if 'neFunc' in data.keys():
            self.neFunc = data['neFunc']
            self.nFunc = self.ne2n(self.neFunc)
            self.eulerLambda, self.jacLambda = self.generateEulerEqnsSym(self.nFunc)
            return
        if 'nFunc' in data.keys():
            self.nFunc = data['nFunc']
            self.neFunc = self.n2ne(self.nFunc)
            self.eulerLambda, self.jacLambda = self.generateEulerEqnsSym(self.nFunc)
            return

    def ne2n(self,neFunc):
        '''Analytically turn electron density to refractive index. Assume ne in m^-3'''
        self.neFunc = neFunc
        #wp = 5.63e4*np.sqrt(ne/1e6)/2pi#Hz^2 m^3 lightman p 226
        fp2 = 8.980**2 * neFunc
        self.nFunc = sqrt(Rational(1) - fp2/self.frequency**2)
        return self.nFunc

    def n2ne(self,nFunc):
        """Get electron density in m^-3 from refractive index"""
        self.nFunc = nFunc
        self.neFunc = (Rational(1) - nFunc**2)*self.frequency**2/8.980**2
        return self.neFunc

    def euler(self,px,py,pz,x,y,z,s,t=0):
        N = np.size(px)
        euler = np.zeros([7,N])
        i = 0
        while i < 7:
            euler[i,:] = self.eulerLambda[i](px,py,pz,x,y,z,s,t)
            i += 1
        return euler

    def eulerODE(self,y,t,*args):
        '''return pxdot,pydot,pzdot,xdot,ydot,zdot,sdot'''
        #print(y)
        e = self.euler(y[0],y[1],y[2],y[3],y[4],y[5],y[6],args[0]).flatten()
        #print(e)
        return e

    def jac(self,px,py,pz,x,y,z,s,t=0):
        N = np.size(px)
        jac = np.zeros([7,7,N])
        i = 0
        while i < 7:
            j = 0
            while j < 7:
                jac[i,j,:] = self.jacLambda[i][j](px,py,pz,x,y,z,s,t)
                j += 1
            i += 1
        return jac

    def jacODE(self,y,t,*args):
        '''return d ydot / d y'''
        j = self.jac(y[0],y[1],y[2],y[3],y[4],y[5],y[6],args[0]).reshape([7,7])

        #print('J:',j)
        return j

    def generateEulerEqnsSym(self,nFunc=None):
        '''Generate function with call signature f(t,y,*args)
        and accompanying jacobian jac(t,y,*args), jac[i,j] = d f[i] / d y[j]'''
        if nFunc is None:
            nFunc = self.nFunc
        x,y,z,px,py,pz,s,t = symbols('x y z px py pz s t')
        if self.type == 'z':
            sdot = nFunc / pz
            pxdot = nFunc.diff('x')*nFunc/pz
            pydot = nFunc.diff('y')*nFunc/pz
            pzdot = nFunc.diff('z')*nFunc/pz

            xdot = px / pz
            ydot = py / pz
            zdot = Rational(1)

        if self.type == 's':
            sdot = Rational(1)
            pxdot = nFunc.diff('x')
            pydot = nFunc.diff('y')
            pzdot = nFunc.diff('z')

            xdot = px / nFunc
            ydot = py / nFunc
            zdot = pz / nFunc

        eulerEqns = (pxdot,pydot,pzdot,xdot,ydot,zdot,sdot)

        euler = [lambdify((px,py,pz,x,y,z,s,t),eqn,"numpy") for eqn in eulerEqns]
        self.eulerLambda = euler
        jac = []

        for eqn in eulerEqns:
            jac.append([lambdify((px,py,pz,x,y,z,s,t),eqn.diff(var),"numpy") for var in (px,py,pz,x,y,z,s)])
        self.jacLambda = jac

        return self.eulerLambda, self.jacLambda
    def integrateRay(self,X0,direction,tmax,time = 0,N=100):
        '''Integrate rays from x0 in initial direction where coordinates are (r,theta,phi)'''
        direction /= np.linalg.norm(direction)
        x0,y0,z0 = X0
        xdot0,ydot0,zdot0 = direction
        sdot = np.sqrt(xdot0**2 + ydot0**2 + zdot0**2)
        px0 = xdot0/sdot
        py0 = ydot0/sdot
        pz0 = zdot0/sdot
        init = [px0,py0,pz0,x0,y0,z0,0]
        if self.type == 'z':
            tarray = np.linspace(z0,tmax,N)
        if self.type == 's':
            tarray = np.linspace(0,tmax,N)
        #print("Integrating at {0} from {1} in direction {2} until {3}".format(time,X0,direction,tmax))
        #print(init)
        #print("Integrating from {0} in direction {1} until {2}".format(x0,directions,tmax))
        Y,info =  odeint(self.eulerODE, init, tarray, args=(time,),Dfun = self.jacODE, col_deriv = 0, full_output=1)
        #print(info['hu'].shape,np.sum(info['hu']),info['hu'])
        #print(Y)
        x = Y[:,3]
        y = Y[:,4]
        z = Y[:,5]
        s = Y[:,6]
        return x,y,z,s

def calcForwardKernel(kernelRays,file):
    '''ray is a dictionary {datumIdx:{'x':ndarray,'y':ndarray,'z':ndarray,'s':ndarray}}
    kernel is a symbolic function of (x,y,z,t) symbols
    time is a double'''
    results = {}

    data = np.load(file)
    kernel = data['kernel'].item(0)
    Jkernel = data['Jkernel']
    #print(kernel)
    GLamb = lambdify(symbols('x y z t'),kernel,'numpy')
    #for each thing do kernel forward problem
    for rayPair in kernelRays:
        datumIdx = rayPair[0].id
        time = rayPair[0].time
        ray = rayPair[1]
        gres = GLamb(ray['x'],ray['y'],ray['z'],time)
        if np.size(gres) == 1:
            g = gres*(ray['s'][-1] - ray['s'][0])
        else:
            g = simps(gres,ray['s'])
        results[datumIdx] = {'g':g,'J':np.zeros(len(Jkernel))}
    paramIdx = 0
    while paramIdx < len(Jkernel):
        JLamb = lambdify(symbols('x y z t'),Jkernel[paramIdx],'numpy')
        for rayPair in kernelRays:
            datumIdx = rayPair[0].id
            time = rayPair[0].time
            ray = rayPair[1]
            jres = JLamb(ray['x'],ray['y'],ray['z'],time)
            if np.size(jres) == 1:
                results[datumIdx]['J'][paramIdx] = jres*(ray['s'][-1] - ray['s'][0])
            else:
                results[datumIdx]['J'][paramIdx] = simps(jres,ray['s'])
        paramIdx += 1
    return results

def generateKernel(gFile,forwardKernelParamDict):
    data = np.load(gFile)
    #print data
    Gk = data['Gk'].item(0)
    Jk = data['Jk']
    G = Gk.subs(forwardKernelParamDict)
    J = []
    for j in Jk:
        J.append(j.subs(forwardKernelParamDict))
    return {'G':G,'J':J}

def testSweep():
    sol = SolitonModel(1)
    neFunc = sol.generateSolitonsModel()

    f =  Fermat(neFunc = neFunc,type = 's')

    theta = np.linspace(-np.pi/8.,np.pi/8.,25)
    #phi = np.linspace(0,2*np.pi,6)
    rays = []
    origin = ac.ITRS(sol.enu.location).cartesian.xyz.to(au.km).value
    for t in theta:
        for p in theta:
            direction = ac.SkyCoord(np.sin(t),
                                    np.sin(p),
                                    1.,frame=sol.enu).transform_to('itrs').cartesian.xyz.value
            x,y,z,s = f.integrateRay(origin,direction,1000,time=0.)
            rays.append({'x':x,'y':y,'z':z})
    plotWavefront(f.nFunc.subs({'t':0}),rays,*getSolitonCube(sol),save = False)
    #plotFuncCube(f.nFunc.subs({'t':0}), *getSolitonCube(sol),rays=rays)

if __name__=='__main__':
    np.random.seed(1234)
    #testSquare()
    testSweep()
    #testThreadedFermat()
    #testSmoothify()
    #testcseLam()

```

    Generated solitons symbolic function with 8 params

    C:\Users\josh\Anaconda3\envs\mayavi_env\lib\site-packages\pyface\ui\wx\window.py:180: wxPyDeprecationWarning: Accessing deprecated property.
      alt_down     = event.m_altDown == 1,
    C:\Users\josh\Anaconda3\envs\mayavi_env\lib\site-packages\pyface\ui\wx\window.py:181: wxPyDeprecationWarning: Accessing deprecated property.
      control_down = event.m_controlDown == 1,
    C:\Users\josh\Anaconda3\envs\mayavi_env\lib\site-packages\pyface\ui\wx\window.py:182: wxPyDeprecationWarning: Accessing deprecated property.
      shift_down   = event.m_shiftDown == 1,

```python

```


# Landau-Zener transitions
This notebook illustrates using the `toy_systems` package to simulate the classic Landau-Zener system that illustrates the behaviour of quantum states at avoided energy crossings. The LZ system is a 2-level system with a coupling of constant strength $\Omega$ between the states, and an energy difference that varies linearly over time from $\Delta E\rightarrow-\infty$ as $t\rightarrow-\infty$ thorugh $\Delta E = 0$ at $t = 0$ to $\Delta E\rightarrow\infty$ as $t\rightarrow\infty$. If we take the initial state of the system at $t \rightarrow -\infty$ to be $|\psi\rangle = |g\rangle$, the Landau-Zener formula gives the probability of an

## Imports
Start by importing the necessary packages

```python
%load_ext autoreload
%autoreload 2

import joblib
import matplotlib.pyplot as plt
plt.style.use("ggplot")
import numpy as np
import qutip
from sympy import Symbol

from toy_systems.couplings import ToyCoupling, ToyEnergy
from toy_systems.decays import ToyDecay
from toy_systems.hamiltonian import Hamiltonian
from toy_systems.quantum_system import QuantumSystem
from toy_systems.states import Basis, BasisState, ToyQuantumNumbers
from toy_systems.visualization import Visualizer
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

## Define states
We start by defining the two states of the system: the ground state $|g\rangle$ and the excited $|e\rangle$, which we will later set to have a decay path to $|g\rangle$:

```python
g = BasisState(qn=ToyQuantumNumbers(label="g"))
e = BasisState(qn=ToyQuantumNumbers(label="e"))

# Print the states as a check
print(f"|g> = {g}")
print(f"|e> = {e}")

# Check that the states are orthogonal by taking inner product
print(f"<e|g> = {e @ g}")
```

    |g> = |g>
    |e> = |e>
    <e|g> = 0

## Define basis
We use the two orthogonal states to define a basis as shown below. The indexing of the matrix and vector representations of operators is determined by the order of the states when generating the state

```python
basis = Basis((g,e))
basis.print()
```

    |0> = |g>
    |1> = |e>

## Define energies
I'm going to define the system in the rotating frame so that the Hamiltonian doesn't have any quickly rotating terms of the form $e^{i\omega t}$. The detuning between the driving field and the natural frequency of the system is taken to be $\Delta(t)$ (time-varying), so I can set the energies of the two states to be $E_g = -\Delta/2$ and $E_e = \Delta/2$.

Note: the time dependence will be multiplied by the value of $\Delta$ when time-evolving the Hamiltonian.

```python
Δ = Symbol('Delta') # Using sympy to generate a symbolic Hamiltonian first (to make a "Δ" type "\Delta", then press tab)
E_g = ToyEnergy([g], -Δ/2, time_dep = "t")
E_e = ToyEnergy([e], +Δ/2, time_dep = "t") # Note the linear time-dependence
```

```python
E_g.time_dep
```

    't'

### Define coupling
I'm taking the strength of the driving field to vary as a Gaussian over time. This time dependence of the drive field may result in the probability of state transfer durin RAP being slightly different from

```python
Ω = Symbol('Omega')
coupling = ToyCoupling(g, e, Ω)
```

## Define Hamiltonian
Now that we have the states, energies and couplings, we can define the Hamiltonian

```python
H = Hamiltonian((E_g, E_e, coupling), basis)
display(H)
```

    H =
    [
    [-0.5, 1.0],
    [ 1.0, 0.5]]

    in basis Basis: name = None

## Define decay matrix from |e> to |g>
Can add a spontaneous decay from the excited state to the ground state as shown below:

```python
decay = ToyDecay(e, g, Symbol("Gamma"))
```

## Define a QuantumSystem
The QuantumSystem object combines the basis, Hamiltonian and decays to make setting parameters for time evolution using QuTiP more convenient.

```python
# Define the system
system = QuantumSystem(
    basis=basis,
    couplings=[E_g, E_e, coupling],
    decays=[decay],
)
visualizer = Visualizer(system, vertical={"label":10}, horizontal={"label":0})

# Get representations of the Hamiltonian and the decays that will be accepted by qutip
Hqobj, c_qobj = system.get_qobjs()
```

```python
Hqobj.ops[2].coeff
```

    'Omega'

## Time-evolution using `QuTiP`
The `toy_systems` package is designed to take care of generating the Hamiltonian and collapse operators for the quantum system, and spit out a `qutip.QobjEVo`-object. The actual time-evolution is then taken care of by `qutip` (or something else if the user so desires).

```python
# Get a pointer to the time-evolution arguments
args = Hqobj.args
print("Keys for setting arguments:")
print(f"args = {args}")
```

    Keys for setting arguments:
    args = {'Delta': 1, 'Omega': 1, 'Gamma': 1}

```python
# Generate a Qobj representing the initial state
psi0 = (1*g).qobj(basis)

# Make operators for getting the probability of being in each state
P_g_op = qutip.Qobj((1*g).density_matrix(basis), type = "oper")
P_e_op = qutip.Qobj((1*e).density_matrix(basis), type = "oper")

# Set the parameters for the system
args["Delta"] = 1e-2 # This is actually the rate at which delta varies, i.e. dD/dt
args["sigma"] = 2/args["Delta"] # Width of the Gaussian describing time-dependence of drive field
args["Omega"] = 1 # The maximum value of the drive field Rabi rate
args["Gamma"] = 0

# Times at which result is requested
times = np.linspace(-40/args["Delta"], 40/args["Delta"], 40001)

# The solver used by qutip sometimes doesn't seem to notice that adiabatic passage is supposed to be happening, so reduce max step size to
# mitigate this
options = qutip.solver.Options(method = 'adams', nsteps=1000000, max_step=1e2/args["Delta"],)

# Run the time-evolution
result = qutip.mesolve(Hqobj, psi0, times, c_ops = c_qobj, e_ops = [P_g_op, P_e_op], options = options)
```

Plot the result:

```python
fig, (ax,ax2) = plt.subplots(2,1, figsize = (16,9))
ax.plot(times, result.expect[0], label = "P_g")
ax.plot(times, result.expect[1], label = "P_e")
ax.legend()
ax.set_xlabel("Time / (1/Ω)")
ax.set_ylabel("Population in each state")

# Plot the coupling strength and detuning also
ax2c = ax2.twinx()
# ax2.grid(False)
# ax2c.grid(False)
ln1 = ax2.plot(times, E_e.eval(times, args), label = "Δ")
ln2 = coupling.plot_time_dep(times, args, ax = ax2c, label = 'Ω', c = 'tab:blue')
ax2c.set_ylabel('')
ax2.legend(ln1+ln2, [l.get_label() for l in (ln1 + ln2)])

```

## Testing if Landau-Zener formula applies
The famous Landau-Zener formula for the probability of adiabatic transitions assumes a constant coupling strength and a linear detuning ramp. Under these conditions the probability of staying in the initial state can be solved for exactly and is given by $P_{ini} = 1-\exp\left(-2\pi\Omega^2/\left(\frac{dD}{dt}\right)\right)$.

To investigate, I will do a parameter scan with the detuning rate. This will also illustrate using QuTiP and parallelization.

```python
# Define the function we want to run in parallel to perform the parameter scan
def parallel_func(Δ: float):

    # Generate a Qobj representing the initial state
    psi0 = (1*g).qobj(basis)

    # Make operators for getting the probability of being in each state
    P_g_op = qutip.Qobj((1*g).density_matrix(basis), type = "oper")
    P_e_op = qutip.Qobj((1*e).density_matrix(basis), type = "oper")

    args = {}

    # Set the parameters for the system
    args["Delta"] = Δ # This is actually the rate at which delta varies, i.e. dD/dt
    args["sigma"] = 2/Δ # Width of the Gaussian describing time-dependence of drive field
    args["Omega"] = 1 # The maximum value of the drive field Rabi rate
    args["Gamma"] = 0

    # Times at which result is requested
    times = np.linspace(-500/args["Delta"], 500/args["Delta"], 10001)

    # The solver used by qutip sometimes doesn't seem to notice that adiabatic passage is supposed to be happening, so reduce max step size to
    # mitigate this
    options = qutip.solver.Options(method = 'adams', nsteps=1000000, max_step=1e2/Δ, rhs_reuse=True)

    # Run the time-evolution
    result = qutip.mesolve(Hqobj, psi0, times, args = args, c_ops = c_qobj, e_ops = [P_g_op, P_e_op], options = options)

    # Plot the result
    fig, ax = plt.subplots(figsize = (16,9))
    ax.plot(times, result.expect[0], label = "P_g")
    ax.plot(times, result.expect[1], label = "P_e")
    ax.legend()
    ax.set_xlabel("Time / (1/Ω)")
    ax.set_ylabel("Population in each state")
    ax.set_title(f"Δ = {Δ}")

    return np.mean(result.expect[0][-1000:])

# Define the values of dD/dt we want to try
Δs = 10**np.linspace(-0.5,3,30)

# Define a loop that will be excecuted in parallel using joblib
probs = (joblib.Parallel(n_jobs = 10, backend='loky', verbose = True)(joblib.delayed(parallel_func)(Δ) for Δ in Δs))
```

    [Parallel(n_jobs=10)]: Using backend LokyBackend with 10 concurrent workers.
    [Parallel(n_jobs=10)]: Done  30 out of  30 | elapsed:   29.5s finished

```python
# Plot the result
fig, (ax, ax2) = plt.subplots(2,1, figsize = (16,9))
ax.plot(np.log10(Δs), probs, 'x')
ax.set_xlabel("dΔ/dt")
ax.set_ylabel("P_g")
ax2.plot(1/Δs, np.log(np.array(probs)), 'x')
ax2.set_xlabel("1/(dΔ/dt)")
ax2.set_ylabel("ln(P_g)")

# Fit a line to the log plot
from scipy.stats import linregress
x_fit = 1/Δs[3:]
y_fit = np.log(np.array(probs[3:]))
fit_result = linregress(x_fit, y_fit)
ax2.plot(x_fit, fit_result.slope*x_fit+fit_result.intercept)

print(f"slope: ({fit_result.slope/(2*np.pi):.4f})*2*pi, intercept: {fit_result.intercept:.4f}")
```

The linear fit seems quite good for the most part, and the slope and intercept are close to the expected values of $2\pi$ and 0. Strangely at the lower detuning ramp rates, the time integration is not working as well, or perhaps we are having isues with numerical precision.