## Interactive Variogram Calculation 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 an interactive workflow for calculating directional experimental variograms in 2D.

* setting the variogram calculation parameters for identifying spatial data pairs

This approach is essential for quantifying spatial continuity with sparsely sampled, irregular spatial data.

I have more comprehensive workflows for variogram calculation:

* [Experimental Variogram Calculation in Python with GeostatsPy](https://github.com/GeostatsGuy/PythonNumericalDemos/blob/master/GeostatsPy_variogram_calculation.ipynb)

* [Determination of Major and Minor Spatial Continuity Directions in Python with GeostatsPy](https://github.com/GeostatsGuy/PythonNumericalDemos/blob/master/GeostatsPy_spatial_continuity_directions.ipynb)

#### 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 Calculation Parameters

The variogram calculation parameters include:

* **azimuth** is the azimuth of the lag vector

* **azimuth tolerance** is the maximum allowable departure from the azimuth (isotropic variograms are calculated with an azimuth tolerance of to 90.0)

* **unit lag distance** the size of the bins in lag distance, usually set to the minimum data spacing

* **lag distance tolerance** - the allowable tolerance in lage distance, commonly set to 50% of unit lag distanceonal smoothing

* **number of lags** - set based on the spatial extent of the dataset, we can typically calculate reliable variograms up to 1/2 the extent of the dataset

* **bandwidth** is the maximum offset allowable from the lag vector

#### 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
      259
      190
      19
      0
      0.041122
      0.120862
      6367.442357

      1
      501
      270
      49
      0
      0.043853
      0.094627
      6906.176965

      2
      273
      270
      219
      0
      0.062169
      0.408779
      6244.965113

      3
      159
      380
      259
      0
      0.064295
      0.322764
      7113.679894

      4
      96
      870
      449
      0
      0.065537
      0.672005
      5672.957304

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
      367.0
      292.926431
      169.167110
      0.000000
      150.000000
      295.000000
      440.000000
      586.000000

      X
      367.0
      500.108992
      289.978309
      0.000000
      240.000000
      500.000000
      765.000000
      990.000000

      Y
      367.0
      520.403270
      277.752407
      9.000000
      269.000000
      539.000000
      769.000000
      999.000000

      Facies
      367.0
      0.596730
      0.491224
      0.000000
      0.000000
      1.000000
      1.000000
      1.000000

      Porosity
      367.0
      0.126874
      0.030546
      0.041122
      0.103398
      0.125616
      0.148323
      0.210258

      Perm
      367.0
      83.313352
      224.350475
      0.094627
      2.288119
      10.294907
      50.219914
      1991.097723

      AI
      367.0
      4790.458884
      975.582304
      1981.177309
      4108.843701
      4712.673154
      5466.957467
      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
      259
      190
      19
      0
      0.041122
      0.120862
      6367.442357
      -2.671293
      -2.644781

      1
      501
      270
      49
      0
      0.043853
      0.094627
      6906.176965
      -2.644781
      -2.775560

      2
      273
      270
      219
      0
      0.062169
      0.408779
      6244.965113
      -2.467028
      -1.789282

      3
      159
      380
      259
      0
      0.064295
      0.322764
      7113.679894
      -2.344090
      -1.945032

      4
      96
      870
      449
      0
      0.065537
      0.672005
      5672.957304
      -2.248832
      -1.316486

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.

#### 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                                    # set the color map
plt.subplot(131)                                        # location map of normal score transform of porosity
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)                                        # location map of normal score transform of permeability
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                      # normal score porosity / permeability scatter plot color coded by facies
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.

#### Experimental Variograms

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

```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 previous nscore transform sets the variance to 1.0

#### Dashboard for Interactive Variogram Calculation

Below we make a dashboard with the ipywidgets and matplotlib Python packages for calculating experimental variograms.

```python
# interactive calculation of the experimental variogram
l = widgets.Text(value='                              Variogram Calculation Interactive Demonstration, Michael Pyrcz, Associate Professor, The University of Texas at Austin',layout=Layout(width='950px', height='30px'))
lag = widgets.FloatSlider(min = 20, max = 500, value = 100, step = 10, description = 'lag',orientation='vertical',layout=Layout(width='90px', height='200px'))
lag.style.handle_color = 'gray'

lag_tol = widgets.FloatSlider(min = 20, max = 500, value = 50, step = 10, description = 'lag tolerance',orientation='vertical',layout=Layout(width='90px', height='200px'))
lag_tol.style.handle_color = 'gray'

nlag = widgets.IntSlider(min = 1, max = 100, value = 10, step = 1, description = 'number of lags',orientation='vertical',layout=Layout(width='90px', height='200px'))
nlag.style.handle_color = 'gray'

azi = widgets.FloatSlider(min = 0, max = 360, value = 0, step = 5, description = 'azimuth',orientation='vertical',layout=Layout(width='90px', height='200px'))
azi.style.handle_color = 'gray'

azi_tol = widgets.FloatSlider(min = 10, max = 90, value = 20, step = 5, description = 'azimuth tolerance',orientation='vertical',layout=Layout(width='120px', height='200px'))
azi_tol.style.handle_color = 'gray'

bandwidth = widgets.FloatSlider(min = 100, max = 2000, value = 2000, step = 100, description = 'bandwidth',orientation='vertical',layout=Layout(width='90px', height='200px'))
azi_tol.style.handle_color = 'gray'

ui1 = widgets.HBox([lag,lag_tol,nlag,azi,azi_tol,bandwidth],) # basic widget formatting
ui = widgets.VBox([l,ui1],)

def f_make(lag,lag_tol,nlag,azi,azi_tol,bandwidth):     # function to take parameters, calculate variogram and plot
#    text_trap = io.StringIO()
#    sys.stdout = text_trap

    lags, gammas, npps = geostats.gamv(df,"X","Y","NPor",tmin,tmax,lag,lag_tol,nlag,azi,azi_tol,bandwidth,isill=1.0)

    plt.subplot(111)                                    # plot experimental variogram
    plt.scatter(lags,gammas,color = 'black',s = npps*0.1,label = 'Azimuth ' +str(azi))
    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 - Azi ' + str(azi))
    plt.xlim([0,1000]); plt.ylim([0,1.8])
    plt.grid(True)

    plt.subplots_adjust(left=0.0, bottom=0.0, right=1.5, top=1.0, 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, {'lag':lag,'lag_tol':lag_tol,'nlag':nlag,'azi':azi,'azi_tol':azi_tol,'bandwidth':bandwidth})
interactive_plot.clear_output(wait = True)               # reduce flickering by delaying plot updating
```

### Interactive Variogram Calculation Demostration

* calculate omnidirectional and direction experimental variograms

#### 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

Calculate interpretable experimental variograms for sparse, irregularly-space spatial data.

* **azimuth** is the azimuth of the lag vector

* **azimuth tolerance** is the maximum allowable departure from the azimuth

* **unit lag distance** the size of the bins in lag distance

* **lag distance tolerance** - the allowable tolerance in lage distance

* **number of lags** - number of lags in the experimental variogram

* **bandwidth** - maximum departure from the lag vector

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

    VBox(children=(Text(value='                              Variogram Calculation Interactive Demonstration, Mich…

    Output()

#### Comments

This was a basic demonstration of vairogram calculation 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

```


# 02 - Reverse Time Migration

This notebook is the second in a series of tutorial highlighting various aspects of seismic inversion based on Devito operators. In this second example we aim to highlight the core ideas behind seismic inversion, where we create an image of the subsurface from field recorded data. This tutorial follows on the modelling tutorial and will reuse the modelling operator and velocity model.

## Imaging requirement

Seismic imaging relies on two known parameters:

- **Field data** - or also called **recorded data**. This is a shot record corresponding to the true velocity model. In practice this data is acquired as described in the first tutorial. In order to simplify this tutorial we will generate synthetic field data by modelling it with the **true velocity model**.

- **Background velocity model**. This is a velocity model that has been obtained by processing and inverting the field data. We will look at this methods in the following tutorial as it relies on the method we are describing here. This velocity model is usually a **smooth version** of the true velocity model.

## Imaging computational setup

In this tutorial, we will introduce the back-propagation operator. This operator simulates the adjoint wave-equation, that is a wave-equation solved in a reversed time order. This time reversal led to the naming of the method we present here, called Reverse Time Migration. The notion of adjoint in exploration geophysics is fundamental as most of the wave-equation based imaging and inversion methods rely on adjoint based optimization methods.

## Notes on the operators

As we already describe the creation of a forward modelling operator, we will use a thin wrapper function instead. This wrapper is provided by a utility class called `AcousticWaveSolver`, which provides all the necessary operators for seismic modeling, imaging and inversion. The `AcousticWaveSolver` provides a more concise API for common wave propagation operators and caches the the Devito `Operator` objects to avoid unnecessary recompilation. However, any newly introduced operators will be fully described and only used from the wrapper in the next tutorials.

As before we initialize printing and import some utilities. We also raise the Devito log level to avoid excessive logging for repeated operator invocations.

```python
import numpy as np
%matplotlib inline

from devito import configuration
configuration['log_level'] = 'WARNING'
```

## Computational considerations

Seismic inversion algorithms are generally very computationally demanding and require a large amount of memory to store the forward wavefield. In order to keep this tutorial as light-weight as possible we are using a very simple
velocity model that requires low temporal and special resolution. For a more realistic model, a second set of preset parameters for a reduced version of the 2D Marmousi data set [1] is provided below in comments. This can be run to create some more realistic subsurface images. However, this second present is more computationally demanding and requires a slightly more powerful workstation.

```python
# Configure model presets
from examples.seismic import demo_model

# Enable model presets here:
preset = 'twolayer-isotropic'  # A simple but cheap model (recommended)
# preset = 'marmousi2d'  # A larger more realistic model

# Standard preset with a simple two-layer model
if preset == 'twolayer-isotropic':
    def create_model(grid=None):
        return demo_model('twolayer-isotropic', origin=(0., 0.), shape=(101, 101),
                          spacing=(10., 10.), nbpml=20, grid=grid)
    filter_sigma = (1, 1)
    nshots = 21
    nreceivers = 101
    t0 = 0.
    tn = 1000.  # Simulation last 1 second (1000 ms)
    f0 = 0.010  # Source peak frequency is 10Hz (0.010 kHz)

# A more computationally demanding preset based on the 2D Marmousi model
if preset == 'marmousi2d-isotropic':
    def create_model(grid=None):
        return demo_model('marmousi2d-isotropic', data_path='../../../../opesci-data/',
                          grid=grid)
    filter_sigma = (6, 6)
    nshots = 301  # Need good covergae in shots, one every two grid points
    nreceivers = 601  # One recevier every grid point
    t0 = 0.
    tn = 3500.  # Simulation last 3.5 second (3500 ms)
    f0 = 0.025  # Source peak frequency is 25Hz (0.025 kHz)
```

# True and smooth velocity models

First, we create the model data for the "true" model from a given demonstration preset. This model represents the subsurface topology for the purposes of this example and we will later use it to generate our synthetic data readings. We also generate a second model and apply a smoothing filter to it, which represents our initial model for the imaging algorithm. The perturbation between these two models can be thought of as the image we are trying to recover.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_velocity, plot_perturbation
from scipy import ndimage

# Create true model from a preset
model = create_model()

# Create initial model and smooth the boundaries
model0 = create_model(grid=model.grid)
model0.vp = ndimage.gaussian_filter(model0.vp, sigma=filter_sigma, order=0)

# Plot the true and initial model and the perturbation between them
plot_velocity(model)
plot_velocity(model0)
plot_perturbation(model0, model)
```

## Acquisition geometry

Next we define the positioning and the wave signal of our source and the location of our receivers. To generate the wavelet for our source we require the discretized values of time that we are going to use to model a single "shot",,
which again depends on the grid spacing used in our model. For consistency this initial setup will look exactly as in the previous modelling tutorial, although we will vary the position of our source later on during the actual imaging algorithm.

```python
#NBVAL_IGNORE_OUTPUT
# Define acquisition geometry: source
from examples.seismic import TimeAxis, RickerSource

# Define time discretization according to grid spacing
dt = model.critical_dt  # Time step from model grid spacing
time_range = TimeAxis(start=t0, stop=tn, step=dt)

src = RickerSource(name='src', grid=model.grid, f0=f0, time_range=time_range)

# First, position source centrally in all dimensions, then set depth
src.coordinates.data[0, :] = np.array(model.domain_size) * .5
src.coordinates.data[0, -1] = 20.  # Depth is 20m

# We can plot the time signature to see the wavelet
src.show()
```

```python
# Define acquisition geometry: receivers
from examples.seismic import Receiver

# Initialize receivers for synthetic and imaging data
rec = Receiver(name='rec', grid=model.grid, npoint=nreceivers, time_range=time_range)
rec.coordinates.data[:, 0] = np.linspace(0, model.domain_size[0], num=nreceivers)
rec.coordinates.data[:, 1] = 30.
```

# True and smooth data

We can now generate the shot record (receiver readings) corresponding to our true and initial models. The difference between these two records will be the basis of the imaging procedure.

For this purpose we will use the same forward modelling operator that was introduced in the previous tutorial, provided by the `WaveSolver` utility class. This object instantiates a set of pre-defined operators according to an initial definition of the acquisition geometry, consisting of source and receiver symbols. The solver objects caches the individual operators and provides a slightly more high-level API that allows us to invoke the modelling modelling operators from the initial tutorial in a single line. In the following cells we use this to generate shot data by only specifying the respective model symbol `m` to use, and the solver will create and return a new `Receiver` object the represents the readings at the previously defined receiver coordinates.

```python
# Compute synthetic data with forward operator
from examples.seismic.acoustic import AcousticWaveSolver

solver = AcousticWaveSolver(model, src, rec, space_order=4)
true_d , _, _ = solver.forward(src=src, m=model.m)
```

```python
# Compute initial data with forward operator
smooth_d, _, _ = solver.forward(src=src, m=model0.m)
```

```python
#NBVAL_IGNORE_OUTPUT
# Plot shot record for true and smooth velocity model and the difference
from examples.seismic import plot_shotrecord

plot_shotrecord(true_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data - true_d.data, model, t0, tn)
```

# Imaging with back-propagation

As we explained in the introduction of this tutorial, this method is based on back-propagation.

## Adjoint wave equation

If we go back to the modelling part, we can rewrite the simulation as a linear system solve:

\begin{equation}
\mathbf{A}(\mathbf{m}) \mathbf{u} = \mathbf{q}
\end{equation}

where $\mathbf{m}$ is the discretized square slowness, $\mathbf{q}$ is the discretized source and $\mathbf{A}(\mathbf{m})$ is the discretized wave-equation. The discretized wave-equation matricial representation is a lower triangular matrix that can be solve with forward substitution. The pointwise writing or the forward substitution leads to the time-stepping stencil.

On a small problem one could form the matrix explicitly and transpose it to obtain the adjoint discrete wave-equation:

\begin{equation}
\mathbf{A}(\mathbf{m})^T \mathbf{v} = \delta \mathbf{d}
\end{equation}

where $\mathbf{v}$ is the discrete **adjoint wavefield** and  $\delta \mathbf{d}$ is the data residual defined as the difference between the field/observed data and the synthetic data $\mathbf{d}_s = \mathbf{P}_r \mathbf{u}$. In our case we derive the discrete adjoint wave-equation from the discrete forward wave-equation to get its stencil.

## Imaging

Wave-equation based imaging relies on one simple concept:

- If the background velocity model is cinematically correct, the forward wavefield $\mathbf{u}$ and the adjoint wavefield $\mathbf{v}$ meet at the reflectors position at zero time offset.

The sum over time of the zero time-offset correlation of these two fields then creates an image of the subsurface. Mathematically this leads to the simple imaging condition:

\begin{equation}
  \text{Image} = \sum_{t=1}^{n_t} \mathbf{u}[t] \mathbf{v}[t]
\end{equation}

In the following tutorials we will describe a more advanced imaging condition that produces shaper and more accurate results.

## Operator

We will now define the imaging operator that computes the adjoint wavefield $\mathbf{v}$ and correlates it with the forward wavefield $\mathbf{u}$. This operator essentially consist of three components:
* Stencil update of the adjoint wavefield `v`
* Injection of the data residual at the adjoint source (forward receiver) location
* Correlation of `u` and `v` to compute the image contribution at each timestep

```python
# Define gradient operator for imaging
from devito import TimeFunction, Operator
from examples.seismic import PointSource

from sympy import solve, Eq

def ImagingOperator(model, image):
    # Define the wavefield with the size of the model and the time dimension
    v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4)

    u = TimeFunction(name='u', grid=model.grid, time_order=2, space_order=4,
                     save=time_range.num)

    # Define the wave equation, but with a negated damping term
    eqn = model.m * v.dt2 - v.laplace - model.damp * v.dt

    # Use SymPy to rearranged the equation into a stencil expression
    stencil = Eq(v.backward, solve(eqn, v.backward)[0])

    # Define residual injection at the location of the forward receivers
    dt = model.critical_dt
    residual = PointSource(name='residual', grid=model.grid,
                           time_range=time_range,
                           coordinates=rec.coordinates.data)
    res_term = residual.inject(field=v, expr=residual * dt**2 / model.m,
                               offset=model.nbpml)

    # Correlate u and v for the current time step and add it to the image
    image_update = Eq(image, image - u * v)

    return Operator([stencil] + res_term + [image_update],
                    subs=model.spacing_map)
```

## Implementation of the imaging loop

As we just explained, the forward wave-equation is solved forward in time while the adjoint wave-equation is solved in a reversed time order. Therefore, the correlation of these two fields over time requires to store one of the two fields. The computational procedure for imaging follows:

- Simulate the forward wave-equation with the background velocity model to get the synthetic data and save the full wavefield $\mathbf{u}$
- Compute the data residual
- Back-propagate the data residual and compute on the fly the image contribution at each time step.

This procedure is applied to multiple source positions (shots) and summed to obtain the full image of the subsurface. We can first visualize the varying locations of the sources that we will use.

```python
#NBVAL_IGNORE_OUTPUT

# Prepare the varying source locations
source_locations = np.empty((nshots, 2), dtype=np.float32)
source_locations[:, 0] = np.linspace(0., 1000, num=nshots)
source_locations[:, 1] = 30.

plot_velocity(model, source=source_locations)
```

```python
# Run imaging loop over shots
from devito import Function, clear_cache

# Create image symbol and instantiate the previously defined imaging operator
image = Function(name='image', grid=model.grid)
op_imaging = ImagingOperator(model, image)

# Create a wavefield for saving to avoid memory overload
u0 = TimeFunction(name='u', grid=model0.grid, time_order=2, space_order=4,
                  save=time_range.num)

for i in range(nshots):
    # Important: We force previous wavefields to be destroyed,
    # so that we may reuse the memory.
    clear_cache()

    print('Imaging source %d out of %d' % (i+1, nshots))

    # Update source location
    src.coordinates.data[0, :] = source_locations[i, :]

    # Generate synthetic data from true model
    true_d, _, _ = solver.forward(src=src, m=model.m)

    # Compute smooth data and full forward wavefield u0
    u0.data.fill(0.)
    smooth_d, _, _ = solver.forward(src=src, m=model0.m, save=True, u=u0)

    # Compute gradient from the data residual
    v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4)
    residual = smooth_d.data - true_d.data
    op_imaging(u=u0, v=v, m=model0.m, dt=model0.critical_dt,
               residual=residual)
```

    Imaging source 1 out of 21
    Imaging source 2 out of 21
    Imaging source 3 out of 21
    Imaging source 4 out of 21
    Imaging source 5 out of 21
    Imaging source 6 out of 21
    Imaging source 7 out of 21
    Imaging source 8 out of 21
    Imaging source 9 out of 21
    Imaging source 10 out of 21
    Imaging source 11 out of 21
    Imaging source 12 out of 21
    Imaging source 13 out of 21
    Imaging source 14 out of 21
    Imaging source 15 out of 21
    Imaging source 16 out of 21
    Imaging source 17 out of 21
    Imaging source 18 out of 21
    Imaging source 19 out of 21
    Imaging source 20 out of 21
    Imaging source 21 out of 21

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_image

# Plot the inverted image
plot_image(np.diff(image.data, axis=1))
```

```python
assert np.isclose(np.linalg.norm(image.data), 1e6, rtol=1e1)
```

And we have an image of the subsurface with a strong reflector at the original location.

## References

[1] _Versteeg, R.J. & Grau, G. (eds.) (1991): The Marmousi experience. Proc. EAGE workshop on Practical Aspects of Seismic Data Inversion (Copenhagen, 1990), Eur. Assoc. Explor. Geophysicists, Zeist._


# Examples

For the imports of the example files, we use [pandas](https://pandas.pydata.org/), which we assume to be imported alongside `freqtools`.

```python
%matplotlib inline

import freqtools as ft
import numpy as np
import pandas as pd
```

### Counter data and frequency noise

In this example we import counter measurements of a beatnote of two free running Toptica DL pro and extract the corresponding frequency noise. Due to the limited internal buffer of the counter, we took three separate measurements of durations between 1 and 100 seconds and thus varying sample rates.

First, we import the data into `CounterData` objects. Each of the CounterData objects in `counter_data_list` is then converted to a `OscillatorNoise` object by using Welch's method and join them all together into one `OscillatorNoise` object:

```python
files = ['Counter_1.csv', 'Counter_2.csv', 'Counter_3.csv']

noise_list = []
for f in files:
    data = pd.read_csv(f, header=7)
    counter_data = ft.CounterData(data["Frequency"].values, duration=data["Time"].values[-1])
    noise_list.append(counter_data.to_oscillator_noise(method="welch"))

# Merge the noise data into one dataset
noise = noise_list[0]
noise.join(noise_list[1:2]) # join the 2nd and 3rd counter data to the 1st one
noise.label = 'laser beatnote'
```

We are interestd in the Frequency noise of one laser, so we have to divide the PSD by 2 (assuming uncorrelated noise). Changing the `values` property will automatically cause all representations to be recalculated.

```python
noise.values = noise.values / 2
```

For linewidth calculation, we create a `BetaLine` object.

```python
beta_line = ft.BetaLine()
```

```python

fig, ax = noise.plot()
beta_line.plot(noise.freqs, ax=ax)
ax.legend()
```

The linewidth of the laser can be calculated from the intersection of the $\beta$ separation line and the frequency noise. It can be calculated like this:

```python
beta_line.linewidth(noise) / 1e6
```

    0.27155627776409696

This means, an linewidth of roughly 270 kHz at 1 ms (the default value for `linewidth`).

### Spectrum analyzer data

Here, we analyze the data of a phaselock between two lasers.

First, we have a look a beatnote measured with a spectrum analyzer

```python
spec_data = pd.read_csv('laser_spectrum_analyzer_data.txt', names=["Frequency", "Amplitude"], skiprows=1, delimiter='\t')

spectrum = ft.SpectrumAnalyzerData(spec_data["Frequency"].values, spec_data["Amplitude"].values, rbw=3e5)
spectrum.plot()
```

We can also convert this data to phase noise (in a `OscillatorNoise` object) and plot the resulting PSD of the phase:

```python
laser_noise_from_spec = spectrum.to_oscillator_noise()
laser_noise_from_spec.plot()
```

### Phase noise

Alternatively, we record data with a Rohde&Schwartz FSW phase noise tool.

```python
laser_noise_data = pd.read_csv(
    "PN_FALC_100MHz.DAT",
    names=["Frequency", "Amplitude"],
    skiprows=133,
    usecols=[0,1],
    delimiter=";")
```

```python
laser_noise = ft.OscillatorNoise(laser_noise_data["Frequency"].values,
                            laser_noise_data["Amplitude"].values,
                            representation="script_L",
                            label="Laser")
laser_noise.plot()
```

### Calculating impact of laser and frequency chain phase noise on atom interferometer phase noise

In this example we use the laser phase noise data from above to calculate the impact of the laser's phase noise for a Mach Zehnder atom interferometer with an interferometer time 2 T = 400 ms and a pulse time 𝜏 = 20 μs. For this we first have to consider the noisecoming from the DRO that is used to lock the lasers at 6.8 GHz. We load the data and plot them together.

```python
dro_noise_data = pd.read_csv(
    "DRO_68_ref10M_pn.DAT",
    names=["Frequency", "Amplitude"],
    skiprows=120,
    usecols=[0,1],
    delimiter=";")
```

```python
dro_noise = ft.OscillatorNoise(dro_noise_data["Frequency"].values,
                            dro_noise_data["Amplitude"].values,
                            representation="script_L",
                            label="DRO")
```

```python
fig, ax = laser_noise.plot()
dro_noise.plot(ax=ax)
ax.legend()
```

Next, we have to consider the transfer function of the atom interferometer. It is a highly oscillatory function with a low-pass behaviour which an be verified by calling its `plot_magnitude` method. Here we scale the transfer function with $(k_\text{eff} T^2)$ with $k_\text{eff} = \frac{4\pi}{780\,\mathrm{nm}}$ to get it in units of m/s²/rad.

```python
mz_tf = ft.MachZehnderTransferFunction(T=260e-3, tau=40e-6, convert_to_g=True)
```

To calculate the atom interferometer noise, we first have to scale the phase noise with the squared magnitude of the transfer function and integrate over all frequencies of interest:

\begin{equation}
\sigma_{\text{AI}}^2 = \int_{f_0}^\infty |H_\text{AI}(2\pi f)|^2 \cdot S_\phi(f) \mathrm{d}f
\end{equation}

We leave the lower integration bound $f_0$ as a free parameter in this equation and inegrate all phasenoies above this frequency, or conversly, up to a certain measurement time. $S_\phi$ here is the 1-sided PSD of phase. To conversion from $L(f)$ to $S_\phi$ is done automatically, as can be seen by looking at the `representation` argument of the scaled phase noise or the plot label.

```python
laser_noise_scaled = mz_tf.scale_noise(laser_noise)
dro_noise_scaled = mz_tf.scale_noise(dro_noise)

fig, ax = laser_noise_scaled.plot()
dro_noise_scaled.plot(ax=ax)
ax.legend()
```

The last step now is the integration over all relevant frequencies. In this plot integration takes place from the right sight and for each frequency the accumulated phase noise of all higher frequencies is displayed.

```python

shot_noise = ft.AtomShotNoise(n_atoms=1e5, contrast=1, T=0.260, keff=2*np.pi/780e-9)
laser_noise_rms = ft.calc_rms_noise(laser_noise_scaled)
dro_noise_rms = ft.calc_rms_noise(dro_noise_scaled)

fig, ax = laser_noise_rms.plot()
dro_noise_rms.plot(ax=ax)
ax.axhline(shot_noise.values(0), color='C3', label="Atomic shot noise")
ax.legend()
```

```python

```


# 6. Gyakorlat - 1 DoF gerjesztett, csillapított lengőkar
2021.03.15

## Feladat:

```python
from IPython.display import Image
Image(filename='gyak6_1.png',width=900)
```

A mellékelt ábrán egy lengőkar látható, mely egy $l$ hosszúságú és $m$ tömegű rúddal van modellezve. Ez csak az A csukló körüli elfordulásra képes. Két eset van megkülöböztetve: az (a) ábra egy harmonikus erőgerjesztést mutat, míg a (b) ábrán egy harmoikus útgerjesztés történik a $k_0$ rugómerevségű rugón keresztül. A rúd mindkét esetben egy rugón ($k$, valamint a (b) ábrán $k_1$) és egy csillapításon ($c$) keresztül van a környezethez rözgzítve. Ezen 1 szabadságfokú lengőkar mozgását a $\varphi$ általánosított koordináta segítségével írjuk le, mely a vízszintes síktól van mérve. A szerkezetet a vízszintes síkban vizsgáljuk, valamint az egyensúlyi helyzete a $\varphi = 0$ szögelforduláshoz tartozik. Ekkor az (a) összeállítás $k$ rugóállandójú rugója, valamint a (b) $k_1$ rugóállandójú rugója erőmentes (nyújtatlan).

### Adatok:

|                       |                       |                    |
|:-----------------------|:-----------------------|:-----------------------|
| $m$ = 3 kg | $l$ = 1 m         | $\omega$ = 30 rad/s         |
| $k$ = 400 N/m | $F_0$ = 10 N| $c$ = 28 Ns/m         |
| $k_0$ = 1000 N/m | $r_0$ = 0.01 m| $k_1$ = 150 N/m         |

### Részfeladatok:
1. Vezesse le a mozgásegyenletet mindkét modell esetén, és számítsa ki a csillapított, illetve a csillapítatlan rendszer sajátkörfrekvencáját, csillapítási tényezőjét és statikus deformációját!
2. Ábrázolja a rezonanciagörbét és a fáziskésés diagrammot mindkét esetben!
3. Határozza meg a $\varphi(t)$ mozgástörvényt, amennyiben a kezdeti értékek $\varphi(t = 0) = \varphi_0 = 0{,}015$ rad és $\dot\varphi(t = 0) = 0$ rad/s!

## Megoldás:

### 1. Feladat

```python
from IPython.display import Image
Image(filename='gyak6_2.png',width=900)
```

(Mivel a szabadtest ábra felrajzolása és a dinamika alaptételének alkalmazása nem programozási feladat, így ez itt nincs részletezve.)

A következő mozgásegyenletek írhatóak fel (a szabadtest ábrák jelöléseit használva):

```python
import sympy as sp
from IPython.display import Math # hogy tudjunk LaTeX szöveget kiírni
sp.init_printing()

t, m, θ_A, l, Fr, Fr1, Fd = sp.symbols('t, m, θ_A, l, F_r, F_r1, F_d')
F0, k, c, k0, k1, ω, r0, ζ, f0 = sp.symbols('F_0, k, c, k0, k1, ω, r0, ζ, f0')

φ = sp.Function('φ')(t)
F = sp.Function('F')(t)
Fr0 = sp.Function('F_r0')(t)

# Készítsünk behelyettesítési listát az adatok alapján, SI-ben:
adatok = [(m, 3), (l, 1), (ω, 30), (k, 400), (F0, 10),
          (c, 28), (k0, 1000), (r0, 0.01), (k1, 150)]

mozgegy_a_eq = sp.Eq(θ_A*sp.diff(φ,t,2), F * l/2 - Fr*l - Fd*l)  # az (a) ábra alapján
mozgegy_b_eq = sp.Eq(θ_A*sp.diff(φ,t,2), -Fr0 * l/2 - Fr1*l - Fd*l)  # a (b) ábra alapján
```

```python
display(mozgegy_a_eq, mozgegy_b_eq)
```

```python
# Linearizáljuk a rugókban/csillapításokban ébredő erők kifejezését

Fr_kif = k*l*φ  # közelítés; kis kitérések esetén
Fd_kif = c*l*sp.diff(φ,t)  # közelítés; kis kitérések esetén
Fr1_kif = k1*l*φ  # közelítés; kis kitérések esetén
```

```python
# Vizsgáljuk a gerjesztés alakját.

# harmonikus erőgerjesztés esetében:
F_kif = F0*sp.cos(ω*t)

# harmonikus útgerjesztés esetében:
Fr0_kif = k0 * (l/2*φ - r0*sp.cos(ω*t)) # linearizált alak -> kis kitérésekre érvényes
```

```python
# Steiner-tétel használatával határozzuk meg a rúd tehetetlenségi nyomatékát
# `z` tengely körüli elfordulás esetén az `A` pontra.

θ_A_kif = sp.Rational(1,12) * m * l**2 + m * (l/2)**2 # sp.Rational(számláló,nevező) -> szimbolikus tört
```

```python
# Készítsünk behelyettesítési listát a fenti kifejezésekre:
kif_lista = [(θ_A,θ_A_kif), (Fr0,Fr0_kif), (F,F_kif),
             (Fr1,Fr1_kif), (Fd,Fd_kif), (Fr,Fr_kif)]

mozgegy_a_eq = mozgegy_a_eq.subs(kif_lista)
mozgegy_b_eq = mozgegy_b_eq.subs(kif_lista)
display(mozgegy_a_eq,mozgegy_b_eq)
```

```python
# A mozgásegyenletek most ún. sympy Equality objektumok,
display(type(mozgegy_a_eq))
display(mozgegy_a_eq.is_Equality)

# azaz relációk két kifejezés között (az egyenlet bal és jobb oldala).
# Ennek a használata eddig kényelmes volt, de az egyenletrendezés itt nem támogatott:
# nem lehet pl. a bal és a jobb oldalt is megszorozni egy számmal/kifejezéssel egyszerűen.
# Térjünk át a másik reprezentációra, ahol csak a bal oldallal foglalkozunk, míg a jobb
# oldalt 0-vá tesszük.
```

    sympy.core.relational.Equality

    True

```python
mozgegy_a = mozgegy_a_eq.rhs - mozgegy_a_eq.lhs
mozgegy_b = mozgegy_b_eq.rhs - mozgegy_b_eq.lhs

# osszunk le a főegyütthatóval
mozgegy_a = mozgegy_a / mozgegy_a.coeff(sp.diff(φ,t,2))
mozgegy_b = mozgegy_b / mozgegy_b.coeff(sp.diff(φ,t,2))

mozgegy_a = mozgegy_a.expand()
mozgegy_b = mozgegy_b.expand()

display(mozgegy_a,mozgegy_b) # egy oldalra rendezve!
```

```python
ω_nat_a = sp.sqrt(mozgegy_a.coeff(φ))
ω_nat_b = sp.sqrt(mozgegy_b.coeff(φ))

# φ első deriváltjának együtthatójából fejezhetjük ki a rel. csillapítási tényezőt.
φd_coeff_a = sp.Eq(mozgegy_a.coeff(sp.diff(φ)),2*ζ*ω_nat_a)
φd_coeff_b = sp.Eq(mozgegy_b.coeff(sp.diff(φ)),2*ζ*ω_nat_b)

ζ_a = sp.solve(φd_coeff_a,ζ)[0]
ζ_b = sp.solve(φd_coeff_b,ζ)[0]

ω_damp_a = ω_nat_a*sp.sqrt(1-ζ_a**2)
ω_damp_b = ω_nat_b*sp.sqrt(1-ζ_b**2)
```

```python
# Továbbá tudjuk, hogy a koszinuszos tag együtthatójából
# számolható a statikus deformáció. Ám az itt negatív együtthatóval
# szerpel, mert alapból az egyenlet jobb oldalán a helye, de most át lett hozva.

cos_coeff_a = sp.Eq(-mozgegy_a.coeff(sp.cos(ω*t)),f0*ω_nat_a**2)
f0_a = sp.solve(cos_coeff_a,f0)[0]

cos_coeff_b = sp.Eq(-mozgegy_b.coeff(sp.cos(ω*t)),f0*ω_nat_b**2)
f0_b = sp.solve(cos_coeff_b,f0)[0]
```

```python
# Helyettesítsünk be:

symb_list = [ω_nat_a,ω_nat_b,ζ_a,ζ_b,ω_damp_a,ω_damp_b,f0_a,f0_b]

num_eredmenyek = [elem.subs(adatok) for elem in symb_list] # list comprehension
num_eredmenyek

# Látszik, hogy ugyanazok lettek az eredmények az (a) és (b) esetben.
```

```python
display(Math('\omega_n = {}\ \\text{{rad/s}}'.format(num_eredmenyek[0])))
display(Math('\zeta = {}\ [1]'.format(num_eredmenyek[2].evalf(2))))
display(Math('\omega_d = {}\ \\text{{rad/s}}'.format(num_eredmenyek[4].evalf(4))))
display(Math('f_0 = {}\ \\text{{rad}}'.format(num_eredmenyek[7].evalf(3))))
```

$\displaystyle \omega_n = 20\ \text{rad/s}$

$\displaystyle \zeta = 0.70\ [1]$

$\displaystyle \omega_d = 14.28\ \text{rad/s}$

$\displaystyle f_0 = 0.0125\ \text{rad}$

### 2. Feladat

```python
# A rezonanciagörbe és a fáziskésés diagram:

import matplotlib.pyplot as plt
from numpy import linspace

plt.subplots(figsize=(9, 9))  # beállítjuk a 'vászon' méretét

λ = sp.Symbol('λ')

# 2 koordináta rendszerre fogunk plottolni. Ezek elhelyezkedése egy rácson van. Ennek a
# rácsnak 2 sora lesz, és 1 oszlopa. Azt, hogy éppen melyikre akarunk rajzolni,
# a 3. paraméter adja meg: `subplot(sorok_száma, oszlopok_száma, kivalaszott)`.
ax1 = plt.subplot(2,1,1) # Tehát ő lesz az egyik krsz,
ax2 = plt.subplot(2,1,2) # és ő a másik.

ζ_list = [1E-5, 0.3, 0.5, 0.7]  # ezen listaelemeken iterál végig a `for` ciklus

for ζ in ζ_list: # végigiterálunk a rel. csill. tényezőkön

    N = 1 / sp.sqrt( (1 - λ**2)**2 + 4 * ζ**2 * λ**2 )
    ϑ = sp.atan2((1-λ**2),2*ζ*λ) + sp.pi/2

    λ_vals = linspace(0,4,501) # az x tengelyen felveszünk 101 osztási pontot 0 és 4 között
    # mindegyikre kiszámoljuk a megfelelő y értéket
    N_vals = [N.subs(λ,λ_item) for λ_item in λ_vals]  # ő lesz a felső krsz-en
    ϑ_vals = [ϑ.subs(λ,λ_item).evalf(4) for λ_item in λ_vals]  # ő az alsón

    ax1.plot(λ_vals,N_vals) # itt a felső krsz-re rajzolunk
    ax1.set_xlabel("λ [1]")
    ax1.set_ylabel("N [1]")
    ax1.set_ylim([0,3])
    ax2.plot(λ_vals,ϑ_vals) # itt az alsóra
    ax2.set_xlabel("λ [1]")
    ax2.set_ylabel("ϑ [rad]")
    ax2.set_ylim([-0.2,3.5])
# itt véget ért a `for`, mert már nics indentálva

ax1.grid() # rácsvonalak
ax2.grid()
ax1.legend(['ζ = {}'.format(elem) for elem in ζ_list])  # jelmagyarázat
ax2.legend(['ζ = {}'.format(elem) for elem in ζ_list])

plt.show()
```

### 3. Feladat

```python
kezdeti_ert = {φ.subs(t,0): 0.015, φ.diff(t).subs(t,0): 0}

φ_mozgtorv = sp.dsolve(mozgegy_a.subs(adatok),φ,ics=kezdeti_ert)
display(φ_mozgtorv.evalf(4))

# Egyszerű megoldást nem találtam, amivel a gerjesztő harmonikus tagokat
# amplitúdó+fáziskésés alakra lehet hozni, csak a kézi módszert, ami a gyakorlaton is volt.

Φ = sp.sqrt(φ_mozgtorv.rhs.coeff(sp.sin(30*t))**2
        + φ_mozgtorv.rhs.coeff(sp.cos(30*t))**2).evalf(4)

ϑ = sp.atan2(φ_mozgtorv.rhs.coeff(sp.sin(30*t)),
             φ_mozgtorv.rhs.coeff(sp.cos(30*t)))

display(Math('\Phi = {}\ \\text{{rad}}'.format(Φ)))
display(Math('ϑ = {}\ \\text{{rad}}'.format(ϑ.evalf(4))))

# Mellesleg ϑ leolvasható a fáziskésés diagramról, valamint Φ kiszámítható
# a rezonanciagörbéről leolvasott `N` nagyítás, és a statikus deformáció szorzatából.
```

Készítette:

        Csuzdi Domonkos (Alkalmazott Mechanika Szakosztály) és
        Juhos-Kiss Álmos (Alkalmazott Mechanika Szakosztály)
        Takács Dénes (BME MM) ábrái és Béri Bence (BME MM) kidolgozása alapján.

        Hibák, javaslatok:
        amsz.bme@gmail.com
        csuzdi02@gmail.com
        almosjuhoskiss@gmail.com

        2021.03.15


# Implementation of Devito viscoacoustic equations based on SLS, Kelvin-Voigt, and Maxwell rheological models

## This tutorial is contributed by SENAI CIMATEC (2020)

This tutorial is based on:

**Wave Fields in Real Media: Wave Propagation in Anisotropic, Anelastic, Porous and Electromagnetic Media** (2015)

Jose M. Carcione

https://www.elsevier.com/books/wave-fields-in-real-media/carcione/978-0-08-099999-9

**Linear inversion in layered viscoacoustic media using a time‐domain method** (1994)

Joakim O. Blanch and William W. Symes

SEG Technical Program Expanded Abstracts

https://doi.org/10.1190/1.1822695

**True-amplitude prestack depth migration** (2007)

Feng Deng and George A. McMechan

GEOPHYSICS Technical Papers

https://doi.org/10.1190/1.2714334

**Attenuation compensation for least-squares reverse time migration using the viscoacoustic-wave equation** (2014)

Gaurav Dutta and Gerard T. Schuster

GEOPHYSICS Technical Papers

https://doi.org/10.1190/geo2013-0414.1

**Multiscale viscoacoustic waveform inversion with the second generation wavelet transform and adaptive time–space domain finite-difference method** (2014)

Zhiming Ren, Yang Liu,and Qunshan Zhang

Geophysical Journal International, Volume 197, Issue 2, 1 May 2014, Pages 948–974

https://doi.org/10.1093/gji/ggu024

**Viscoacoustic prestack reverse time migration based on the optimal time-space domain high-order finite-difference method** (2014)

Yan Zhao, Yang Liu, and Zhi-Ming Ren

Appl. Geophys. 11, 50–62.

https://doi.org/10.1007/s11770-014-0414-8

**A stable and efficient approach of Q reverse time migration** (2018)

Yan Zhao, Ningbo Mao, and Zhiming Ren

GEOPHYSICS Technical Papers

https://doi.org/10.1190/geo2018-0022.1

## Introduction

The conversion of mechanical energy to heat, occurs during the propagation of seismic waves on the subsurface, due to the viscosity of the rocks. The presence of oil and gas in these rocks causes seismic attenuations. Thus, associated effects, such as dispersion and dissipation, can significantly affect the amplitudes, as well as the phase of the seismic pulse.  However, in the seismic exploration, the subsurface has still been considered as an ideal elastic/acoustic medium, that is, disregarding its mitigating effect. In practice, the propagation of seismic waves on the subsurface is in many ways different from propagation in an ideal solid.

For example, some subsurface rocks have anisotropic properties, are heterogeneous, porous and so on. The acoustic/elastic wave equation is not sensitive enough to describe propagation in these more complicated mediums. Generally, the viscosity of materials in the subsurface causes energy dissipation and consequently a decrease in amplitude, in addition to modifying the frequency content of the waves. This phenomenon of energy dissipation of the wave is called seismic absorption or attenuation.

The goal of this tutorial is to perform a seismic modeling taking into account the viscosity of the medium, so that it is possible to more accurately simulate the seismic data and consequently build images with better resolution in the processing of this data, in addition to extracting more detailed information on rocky materials through seismic inversion.

In this tutorial, we implement a set of second-order forward viscoacoustic equations based on the **Maxwell**, **Kelvin-Voigt (KV)**, and **standard linear solid (SLS)** rheological models.

Table of symbols

| Symbol         | Description
| :---         | :---
|$f$                                  |Frequency                                            |
|$f_o$                                |Reference frequency                                  |
|$\omega$                             |Angular frenquency                                   |
|$\omega_0$                           |Angular Reference Frequency                          |
|$v$                                  |Velocity model                                       |
|$v_0$                                |Reference velocity at $\omega_0$                     |
|$\kappa$                             |Bulk modulus                                         |
|$g$                                  |Absorption coefficient                               |
|$\tau$                               |Relaxation time                                      |
|$\tau_\sigma$                        |Stress relaxation parameter                          |
|$\tau_\epsilon$                      |Strain relaxation parameter                          |
|$Q$                                  |Quality factor                                       |
|$\eta$                               |Viscosity                                            |
|$\rho$                               |Density                                              |
|$\nabla$                             |Nabla operator                                       |
|$P({\bf x},t)$                       |Pressure field                                       |
|$r({\bf x},t)$                       |Memory variable                                      |
|${\bf v}({\bf x},t)$                 |Particle velocity                                    |
|$S({\bf x}_s,t)$                     |Source                                               |

# Seismic modelling with Devito

Before start with the viscoacoustic approaches we will describe a setup of seismic modelling with Devito in a simple 2D case. We will create a physical model of our domain and define a single source and an according set of receivers to model for the forward model. But first, we initialize some basic utilities.

```python
import numpy as np
import sympy as sp

import matplotlib.pyplot as plt

from devito import *
from examples.seismic.source import RickerSource, WaveletSource, TimeAxis
from examples.seismic import ModelViscoacoustic, plot_image, setup_geometry, plot_velocity
```

```python
nx = 300
nz = 300

# Define a physical size
shape = (nx, nz)
spacing = (20., 20.)
origin = (0., 0.)
nlayers = 3
nbl = 50
space_order = 8
dtype = np.float32

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

# Define a velocity profile. The velocity is in km/s
vp_top = 1.5
vp_bottom = 3.5

# Define a velocity profile in km/s
v = np.empty(shape, dtype=dtype)
v[:] = vp_top  # Top velocity (background)
vp_i = np.linspace(vp_top, vp_bottom, nlayers)
for i in range(1, nlayers):
    v[..., i*int(shape[-1] / nlayers):] = vp_i[i]  # Bottom velocity

qp[:] = 3.516*((v[:]*1000.)**2.2)*10**(-6) # Li's empirical formula

rho[:] = 0.31*(v[:]*1000.)**0.25 # Gardner's relation
```

```python
#NBVAL_IGNORE_OUTPUT
model = ModelViscoacoustic(space_order=space_order, vp=v, qp=qp, b=1/rho,
                           origin=origin, shape=shape, spacing=spacing,
                           nbl=nbl)
```

    Operator `initdamp` ran in 0.01 s
    Operator `pad_vp` ran in 0.01 s
    Operator `pad_b` ran in 0.01 s
    Operator `pad_qp` ran in 0.01 s

```python
#NBVAL_IGNORE_OUTPUT
aspect_ratio = model.shape[0]/model.shape[1]

plt_options_model = {'cmap': 'jet', 'extent': [model.origin[0], model.origin[0] + model.domain_size[0],
                                               model.origin[1] + model.domain_size[1], model.origin[1]]}
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 5))

slices = [slice(model.nbl, -model.nbl), slice(model.nbl, -model.nbl)]

img1 = ax[0].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, **plt_options_model)
fig.colorbar(img1, ax=ax[0])
ax[0].set_title(r"V (km/s)", fontsize=20)
ax[0].set_xlabel('X (m)', fontsize=20)
ax[0].set_ylabel('Depth (m)', fontsize=20)
ax[0].set_aspect('auto')

img2 = ax[1].imshow(np.transpose(qp), vmin=15, vmax=220, **plt_options_model)
fig.colorbar(img2, ax=ax[1])
ax[1].set_title("Q", fontsize=20)
ax[1].set_xlabel('X (m)', fontsize=20)
ax[1].set_ylabel('Depth (m)', fontsize=20)
ax[1].set_aspect('auto')

img3 = ax[2].imshow(np.transpose(rho), vmin=1.9, vmax=2.4, **plt_options_model)
fig.colorbar(img3, ax=ax[2])
ax[2].set_title(r"Density $\rho$ (g/cm^3)", fontsize=20)
ax[2].set_xlabel('X (m)', fontsize=20)
ax[2].set_ylabel('Depth (m)', fontsize=20)
ax[2].set_aspect('auto')

plt.tight_layout()
```

```python
f0 = 0.005 # peak/dominant frequency
b = model.b
rho = 1./b

# velocity model
vp = model.vp

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

s = model.grid.stepping_dim.spacing
damp = model.damp
```

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

```python
from examples.seismic import Receiver

def src_rec(p, model):
    src = RickerSource(name='src', grid=model.grid, f0=f0, time_range=time_range)
    src.coordinates.data[0, :] = np.array(model.domain_size) * .5
    src.coordinates.data[0, -1] = 8.

    # Create symbol for receivers
    rec = Receiver(name='rec', grid=model.grid, npoint=shape[0], time_range=time_range)

    # Prescribe even spacing for receivers along the x-axis
    rec.coordinates.data[:, 0] = np.linspace(0, model.domain_size[0], num=shape[0])
    rec.coordinates.data[:, 1] = 8.

    src_term = src.inject(field=p.forward, expr=(s*src))
    rec_term = rec.interpolate(expr=p)

    return src_term + rec_term, src, rec
```

Auxiliary functions for plotting data:

```python
def plot_receiver(rec):
    rec_plot = rec.resample(num=1001)
    scale_for_plot = np.diag(np.linspace(1.0, 2.5, 1001)**2.0)
    # Pressure (txx + tzz) data at sea surface
    extent = [rec_plot.coordinates.data[0, 0], rec_plot.coordinates.data[-1, 0], 1e-3*tn, t0]
    aspect = rec_plot.coordinates.data[-1, 0]/(1e-3*tn)/.5

    plt.figure(figsize=(10, 10))
    plt.imshow(np.dot(scale_for_plot, rec_plot.data), vmin=-.01, vmax=.01, cmap="seismic",
           interpolation='lanczos', extent=extent, aspect=aspect)
    plt.ylabel("Time (s)", fontsize=20)
    plt.xlabel("Receiver position (m)", fontsize=20)
```

```python
def plot_v_and_p(model, v, p):

    slices = [slice(model.nbl, -model.nbl), slice(model.nbl, -model.nbl)]
    scale = .5*1e-3

    plt_options_model = {'extent': [model.origin[0] , model.origin[0] + model.domain_size[0],
                                    model.origin[1] + model.domain_size[1], model.origin[1]]}

    fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 7))

    ax[0].imshow(np.transpose(v[0].data[0][slices]), vmin=-scale, vmax=scale, cmap="RdGy", **plt_options_model)
    ax[0].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, cmap="jet", alpha=.5, **plt_options_model)
    ax[0].set_aspect('auto')
    ax[0].set_xlabel('X (m)', fontsize=20)
    ax[0].set_ylabel('Depth (m)', fontsize=20)
    ax[0].set_title(r"$v_{x}$", fontsize=20)

    ax[1].imshow(np.transpose(v[1].data[0][slices]), vmin=-scale, vmax=scale, cmap="RdGy", **plt_options_model)
    ax[1].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, cmap="jet", alpha=.5, **plt_options_model)
    ax[1].set_aspect('auto')
    ax[1].set_xlabel('X (m)', fontsize=20)
    ax[1].set_title(r"$v_{z}$", fontsize=20)

    ax[2].imshow(np.transpose(p.data[0][slices]), vmin=-scale, vmax=scale, cmap="RdGy", **plt_options_model)
    ax[2].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, cmap="jet", alpha=.5, **plt_options_model)
    ax[2].set_aspect('auto')
    ax[2].set_xlabel('X (m)', fontsize=20)
    ax[2].set_title(r"$P$", fontsize=20)
```

## Equation based on standard linear solid (SLS) rheological model

The equations of motion for a viscoacoustic medium can be written as:

\begin{equation}
\left\{
\begin{array}{lcl}
\frac{\partial P}{\partial t} + \kappa (\tau + 1)(\nabla \cdot {\bf v}) + r = S({\bf x}_{s}, t) \\
\frac{\partial {\bf v}}{\partial t} + \frac{1}{\rho}\nabla{P} = 0 \\
\frac{\partial r}{\partial t} + \frac{1}{\tau_{\sigma}} [r + \tau \kappa (\nabla \cdot {\bf v})] = 0.
\end{array}
\right.
\label{first-order-dutta}
\end{equation}

Where $\tau = \tau_{\epsilon}/\tau_{\sigma} -1$ represents the magnitude of  $Q$. $\tau_{\epsilon}$ and $\tau_{\sigma}$ are, respectively, the stress and strain relaxation parameters, given by:

\begin{equation}
\tau_\sigma = \frac{\sqrt{Q^2+1}-1}{2 \pi f_0 Q}
\end{equation}
and
\begin{equation}
\tau_\epsilon= \frac{\sqrt{Q^2+1}+1}{2\pi f_0 Q}
\end{equation}

```python
# Stencil created from Carcione (2015) / Blanch and Symes (1995) / Dutta and Schuster (2014)
def SLS(model, p, r, v):

    # Bulk modulus
    bm = rho * (vp * vp)

    # Define PDE to v
    pde_v = v - s * b * grad(p)
    u_v = Eq(v.forward, damp * pde_v)

    # Define PDE to r
    pde_r = r - s * (1. / t_s) * r - s * (1. / t_s) * tt * bm * div(v.forward)
    u_r = Eq(r.forward, damp * pde_r)

    # Define PDE to p
    pde_p = p - s * bm * (tt + 1.) * div(v.forward) - s * r.forward
    u_p = Eq(p.forward, damp * pde_p)

    return [u_v, u_r, u_p]
```

```python
# Seismic Modelling from Carcione (2015) / Blanch and Symes (1995) / Dutta and Schuster (2014) viscoacoustic wave equation.
def modelling_SLS(model):

    # Create symbols for particle velocity, pressure field, memory variable, source and receivers

    v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=space_order)

    p = TimeFunction(name="p", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    r = TimeFunction(name="r", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    # define the source injection and create interpolation expression for receivers

    src_rec_expr, src, rec = src_rec(p, model)

    eqn = SLS(model, p, r, v)

    op = Operator(eqn + src_rec_expr, subs=model.spacing_map)

    op(time=time_range.num-1, dt=dt, src=src, rec=rec)

    return rec, v, p
```

```python
#NBVAL_IGNORE_OUTPUT
rec, v, p = modelling_SLS(model)
```

    Operator `Kernel` ran in 0.46 s

```python
#NBVAL_IGNORE_OUTPUT
plot_receiver(rec)
```

```python
assert np.isclose(np.linalg.norm(rec.data), 16, rtol=10)
```

```python
#NBVAL_IGNORE_OUTPUT
plot_v_and_p(model, v, p)
```

```python
assert np.isclose(norm(v[0]), 1.87797, atol=1e-3, rtol=0)
```

## Equation based on Kelvin-Voigt (KV) rheological model

The viscoacoustic wave equation in time domain is written as:

\begin{equation}
\frac{\partial^{2}P}{\partial{t^2}} - v^{2}\nabla^{2}{P} - \eta\nabla^{2}\left(\frac{\partial P}{\partial t}\right) = S({\bf x}_{s}, t),
\end{equation}

where $\eta = \frac{v^2}{\omega_{0}Q}$ represents the viscosity of medium.

Considering the variable density $\rho$, the equation can be rewritten as:

\begin{equation}
\frac{\partial^{2}P}{\partial{t^2}} - \kappa \nabla \cdot \frac{1}{\rho} \nabla{P} - \eta \rho \nabla \cdot \frac{1}{\rho} \nabla \left(\frac{\partial{P}}{\partial{t}}\right) = S({\bf x}_{s}, t).
\end{equation}

The equation can be written using a first order formulation, given by:

\begin{equation}
\left\{
\begin{array}{ll}
\frac{\partial P}{\partial t} + \kappa \nabla \cdot {\bf v} - \eta \rho \nabla \cdot \frac{1}{\rho} \nabla{P} = S({\bf x}_{s}, t) \\
\frac{\partial {\bf v}}{\partial t} + \frac{1}{\rho} \nabla{P} = 0
\end{array}
\right.
\end{equation}

```python
# Stencil created from Ren et al. (2014) viscoacoustic wave equation.
def KV(model, p, v):

    # Angular frequency
    w = 2. * np.pi * f0

    # Define PDE to v
    pde_v = v - s * b * grad(p)
    u_v = Eq(v.forward, damp * pde_v)

    # Define PDE to p
    pde_p = p - s * vp * vp * rho * div(v.forward) + s * \
            ((vp * vp * rho) / (w * model.qp)) * div(b * grad(p, shift=.5), shift=-.5)

    u_p = Eq(p.forward, damp * pde_p)

    return [u_v, u_p]
```

```python
# Seismic Modelling from Ren et al. (2014) viscoacoustic wave equation.
def modelling_KV(model):

    # Create symbols for particle velocity, pressure field, source and receivers

    v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=space_order)

    p = TimeFunction(name="p", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    # define the source injection and create interpolation expression for receivers

    src_rec_expr, src, rec = src_rec(p, model)

    eqn = KV(model, p, v)

    op = Operator(eqn + src_rec_expr, subs=model.spacing_map)

    op(time=time_range.num-1, dt=dt, src=src, rec=rec)

    return rec, v, p
```

```python
#NBVAL_IGNORE_OUTPUT
rec, v, p = modelling_KV(model)
```

    Operator `Kernel` ran in 0.68 s

```python
#NBVAL_IGNORE_OUTPUT
plot_receiver(rec)
```

```python
assert np.isclose(np.linalg.norm(rec.data), 15, rtol=10)
```

```python
#NBVAL_IGNORE_OUTPUT
plot_v_and_p(model, v, p)
```

```python
assert np.isclose(norm(v[0]), 1.0639238, atol=1e-3, rtol=0)
```

## Equation based on Maxwell rheological model

The viscoacoustic wave equation for the propagating pressure $P$ in the time-space domain:

\begin{equation}
\frac{1}{v^2}\frac{\partial^{2}P}{\partial{t^2}} - \nabla^{2}P + \frac{g}{v}\frac{\partial P}{\partial{t}}  = S({\bf x}_{s}, t),
\label{eq-deng}
\end{equation}

where $g$ is the absorption coefficient, given by:

\begin{equation}
g = \frac{2\pi f_{0}}{vQ},
\end{equation}

The equation can be written using a first order formulation, given by:

\begin{equation}
\left\{
\begin{array}{lcl}
\frac{\partial P}{\partial t} + \kappa (\nabla \cdot {\bf v}) + \frac{2\pi f_{0}}{Q}P= S({\bf x}_{s}, t) \\
\frac{\partial {\bf v}}{\partial t} + \frac{1}{\rho}\nabla{P} = 0 \\
\end{array}
\right.
\end{equation}

```python
# Stencil created from Deng and McMechan (2007) viscoacoustic wave equation.
def Maxwell(model, p, v):

    # Angular frequency
    w = 2. * np.pi * f0

    # Define PDE to v
    pde_v = v - s * b * grad(p)
    u_v = Eq(v.forward, damp * pde_v)

    # Define PDE to p
    pde_p = p - s * vp * vp * rho * div(v.forward) - s * (w / model.qp) * p
    u_p = Eq(p.forward, damp * pde_p)

    return [u_v, u_p]
```

```python
# Seismic Modelling from Deng and McMechan (2007) viscoacoustic wave equation.
def modelling_Maxwell(model):

    # Create symbols for particle velocity, pressure field, source and receivers

    v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=space_order)

    p = TimeFunction(name="p", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    # define the source injection and create interpolation expression for receivers

    src_rec_expr, src, rec = src_rec(p, model)

    eqn = Maxwell(model, p, v)

    op = Operator(eqn + src_rec_expr, subs=model.spacing_map)

    op(time=time_range.num-1, dt=dt, src=src, rec=rec)

    return rec, v, p
```

```python
#NBVAL_IGNORE_OUTPUT
rec, v, p = modelling_Maxwell(model)
```

    Operator `Kernel` ran in 0.36 s

```python
#NBVAL_IGNORE_OUTPUT
plot_receiver(rec)
```

```python
assert np.isclose(np.linalg.norm(rec.data), 16, rtol=10)
```

```python
#NBVAL_IGNORE_OUTPUT
plot_v_and_p(model, v, p)
```

```python
assert np.isclose(norm(v[0]), 1.1323929, atol=1e-3, rtol=0)
```

# More references

[1] https://academic.oup.com/gji/article/197/2/948/616510

[2] https://link.springer.com/article/10.1007/s11770-014-0414-8

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


<!-- dom:TITLE: FFM232, Klassisk fysik och vektorfält - Föreläsningsanteckningar -->
# FFM232, Klassisk fysik och vektorfält - Föreläsningsanteckningar
<!-- dom:AUTHOR: [Christian Forssén](http://fy.chalmers.se/subatom/nt/), Institutionen för fundamental fysik, Chalmers, Göteborg, Sverige -->
<!-- Author: --> **[Christian Forssén](http://fy.chalmers.se/subatom/nt/), Institutionen för fundamental fysik, Chalmers, Göteborg, Sverige**

Date: **Sep 30, 2015**

## Ytterligare räkneproblem

Här ges några exempel hur vi kan använda Gauss och Stokes satser på vektorfält med singulariteter.

Här hanterar vi singulariteten genom att lägga in
* en liten sfär med radien $\epsilon$ kring en punktkälla.

* en cylinder kring $z$-axeln för en linjekälla.

* en cirkel med radien $\epsilon$ runt $z$-axeln för en virveltråd.

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

### Exempel: Punktkälla

Beräkna integralen

$$
\begin{equation}
  \int_S \vec{F} \cdot \mbox{d}\vec{S}
\end{equation}
$$

där $S$ är ytan

$$
\begin{equation}
  \rho = a - z, \,\,\,0 \le z \le a
\end{equation}
$$

med en normal som pekar uppåt, och fältet ges av

$$
\begin{equation}
  \vec{F} = F_0 \left[\left(\frac{a^2}{r^2} + \frac{r^2 \cos^3 \theta}{a^2}
\right) \hat{e}_{r} - \frac{r^2 \cos^2 \theta \sin \theta}{a^2} \hat{z}
\right].
\end{equation}
$$

$F_0$ och $a$ är konstanter.

### Lösning

Vi börjar med att studera ytan $S$ som är en kon med  spetsen i $z = a$.  Konen har sin öppna bottenyta vid $z = 0$.

Om vi tittar på fältet, så ser vi genast att det har en punktkälla i origo.  För att tolka de övriga termerna så skriver vi om fältet som

$$
\begin{equation}
  \vec{F} = F_0 \frac{a^2}{r^2}\hat{e}_{r} + F_0 \frac{r^2\cos^2 \theta}{a^2}
\left(\cos \theta \hat{e}_{r} - \sin \theta \hat{e}_{\theta}\right).
\end{equation}
$$

Uttrycket inom parentesen är basvektorn $\hat{z}$, och $r\cos \theta = z$.  Vi kan därför dela upp fältet i två delar, en del som vi skriver i sfäriska koordinater, och som är singulär, och en andra del som vi skriver i kartesiska koordinater:

$$
\begin{equation}
  \vec{F} = F_0 \left(\frac{a^2}{r^2} \hat{e}_{r} + \frac{z^2}{a^2}
\hat{e}_{z}\right)
\end{equation}
$$

Divergensen av $\vec{F}$ blir

$$
\begin{equation}
  \nabla \cdot \vec{F} = \frac{2 F_0 z}{a^2}.
\end{equation}
$$

För att använda Gauss sats behöver vi dels isolera singulariteten i origo genom att lägga in en halvsfär, $S_\epsilon$, med radien $\epsilon$ runt origo för $z >0$ och med en normalvektor som pekar mot origo.
Dessutom måste vi sluta konen, vilket vi gör genom att lägga till en cirkelskiva, $S_1$,
med ytterradien $a$ och normalvektorn $-\hat{e}_{z}$ i $xy$-planet.

Gauss sats ger oss nu

$$
\begin{equation}
  \int_S {\bf F\cdot \mbox{d}S} + \int_{S_\epsilon} \vec{F} \cdot \mbox{d}\vec{S}
+ \int_{S_1} \vec{F} \cdot \mbox{d}\vec{S} = \int_V {\bf \nabla \cdot F} \mbox{d}V.
\end{equation}
$$

Vi börjar med att beräkna volymsintegralen

$$
\begin{equation}
  \int_V \nabla \cdot \vec{F} \mbox{d}V = \int_V \frac{2\pi F_0 z}{a^2}
\mbox{d}V.
\end{equation}
$$

Vi noterar här att vad vi gör är att integrera över cirkelskivor med radien $a -z$, så vår integral kan skrivas

$$
\begin{equation}
  \int_0^a \frac{2 F_0 z}{a^2} \pi \left(a-z\right)^2 \mbox{d}z =
\frac{2\pi F_0}{a^2}\left[\frac{z^4}{4} -\frac{2}{3} a z^3 + \frac{a^2 z^2}{2}
\right]_0^a = \frac{2\pi F_0}{a^2} \frac{a^4}{12} = \frac{\pi F_0 a^2}{6}.
\end{equation}
$$

Sedan betraktar vi integralen över $S_\epsilon$ (lägg märke till att
$r = \epsilon$ på $S_\epsilon$).

$$
\begin{equation}
  \int_{S_\epsilon} \vec{F} \cdot \mbox{d}\vec{S} = \int_{S_\epsilon}
F_0 \left(\frac{a^2}{\epsilon^2} \hat{e}_{r} +
\frac{\epsilon^2 \cos^2 \theta}{a^2} \hat{e}_{z}\right) \cdot \left(-
\hat{e}_{r}\right) \mbox{d}S = - F_0 \left(\int_S \frac{a^2}{\epsilon^2}
\mbox{d}S + \int_S \frac{\epsilon^2 \cos^3 \theta}{a^2}\mbox{d}S\right) =
\nonumber
\end{equation}
$$

$$
\begin{equation}
- F_0 \left( \frac{a^2}{\epsilon^2} 2\pi \epsilon^2 + \frac{\epsilon^2}{a^2}
\int_S \cos^3 \theta \mbox{d}S\right) \to - 2\pi a^2 F_0 \,\,\,\,\mathrm{då }
\epsilon \to 0.
\end{equation}
$$

Slutligen har vi integralen över $S_1$

$$
\begin{equation}
  \int_{S_1} \vec{F} \cdot \mbox{d}\vec{S} = \int_{S_1} F_0 \left(\frac{a^2}{r^2}
\hat{e}_{r} + \frac{z^2}{a^2} \hat{e}_{z}\right)\cdot\left(-\hat{e}_{z}
\right)\mbox{d}S.
\end{equation}
$$

Tänk nu på att $\hat{e}_{r} \cdot \hat{z} = 0$ i $xy$-planet, och att $z = 0$ därstädes.  Därav följer att integralen är 0. Det följer nu att vår eftersökta integral blir

$$
\begin{equation}
  \int_S \vec{F} \cdot \mbox{d}\vec{S} = \frac{\pi F_0 a^2}{6} + 2\pi F_0 a^2
= \frac{13\pi F_0 a^2}{6}.
\end{equation}
$$

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

### Exempel: Linjekälla

Beräkna integralen

$$
\begin{equation}
  \int_S \vec{F} \cdot \mbox{d}\vec{S},
\end{equation}
$$

där ytan $S$ ges av $r = 2a$, $0 \le \varphi < 2\pi$ och $\frac{\pi}{4} \le \theta \le \frac{3\pi}{4}$ och har normalen $\hat{e}_{r}$, och fältet ges av

$$
\begin{equation}
  \vec{F} = F_0 \left[\left(\frac{a}{\rho} + \frac{\rho}{a}\right)
\hat{e}_{\rho} + \frac{z}{a} \hat{e}_{z}\right].
\end{equation}
$$

$F_0$ och $a$ är här konstanter.

### Lösning

Ytan $S$ är den mittersta delen av en sfär. Den avgränsas vid $z = \pm \sqrt{2} a$.

Fältet $\vec{F}$ är singulärt för $\rho = 0$, det vill säga längs $z$-axeln.  Fältets divergens är

$$
\begin{equation}
  \nabla \cdot \vec{F} = F_0 \left[\frac{1}{\rho} \frac{\partial}{\partial \rho}
\left(a+ \frac{\rho^2}{a}\right) + \frac{\partial}{\partial z} \left(\frac{z}{a}
\right)\right] = F_0 \left(\frac{1}{\rho}\frac{2\rho}{a} + \frac{1}{a}\right) =
\frac{3 F_0}{a}.
\end{equation}
$$

Vi sluter volymen genom att lägga till en cylinder, $S_\epsilon$ kring $z$-axeln mellan $-\sqrt{2}a \le z \le \sqrt{2}a$ och med radien $\epsilon$.  Normalen för $S_\epsilon$ är $-\hat{e}_{\rho}$.  Dessutom lägger vi till cirkulära skivor, $S_+$ och $S_-$, vid $z = \pm \sqrt{2}$ med normaler $\pm \hat{e}_{z}$.

Vi kan nu skriva Gauss sats som

$$
\begin{equation}
\int_S \vec{F} \cdot \mbox{d}\vec{S} + \int_{S_\epsilon} \vec{F} \cdot \mbox{d}\vec{S} +
\int_{S_+} \vec{F} \cdot \mbox{d}\vec{S} + \int_{S_-} \vec{F} \cdot \mbox{d}\vec{S} =
\int_V \nabla \cdot \vec{F} \mbox{d}S.
\end{equation}
$$

Vi börjar med volymsintegralen

$$
\begin{equation}
  \int_V \nabla \cdot \vec{F} \mbox{d}S = \int_S \frac{3 F_0}{a} \mbox{d}V.
\end{equation}
$$

Vi kan se den här integralen som om vi integrerar över cirkelskivor med radien $\sqrt{4 a^2 -z^2}$ för $-\sqrt{2}a \le z \le \sqrt{2}a$

$$
\begin{equation}
  \int_{-\sqrt{2}a}^{\sqrt{2}a} \frac{3F_0}{a} \pi \left(4a^2 - z^2\right)
\mbox{d}z = \frac{3F_0}{a}
\left[4a^2z - \frac{z^3}{3}\right]_{-\sqrt{2}a}^{\sqrt{2}a}
\nonumber
\end{equation}
$$

$$
\begin{equation}
= \frac{3F_0}{a} \left(4\sqrt{2}a^3 - \frac{2}{3}\sqrt{2}a^3 +
4\sqrt{2}a^3 - \frac{2}{3}\sqrt{2}a^3\right) \nonumber
\end{equation}
$$

$$
\begin{equation}
= 20 \sqrt{2} F_0 a^2.
\end{equation}
$$

Vi tar nu hand om ytintegralerna.  Först $S_\epsilon$, där $\rho = \epsilon$ på $S_\epsilon$

$$
\begin{equation}
  \int_{S_\epsilon} \vec{F} \cdot \mbox{d}\vec{S} = \int_{S_\epsilon}  F_0 \left[
\left(\frac{a}{\epsilon} + \frac{\epsilon}{a}\right) \hat{e}_{\rho} +
\frac{z}{a}\hat{e}_{z}\right) \cdot \left(-\hat{e}_{\rho}\right) \mbox{d}S \nonumber
\end{equation}
$$

$$
\begin{equation}
 = \int_{S_\epsilon} F_0 \left(\frac{a}{\epsilon} + \frac{\epsilon}{a}\right)
\mbox{d}S = F_0 \left(\frac{a}{\epsilon} + \frac{\epsilon}{a}\right)
\int_{S_\epsilon} \mbox{d}S \nonumber
\end{equation}
$$

$$
\begin{equation}
= F_0 \left(\frac{a}{\epsilon} + \frac{\epsilon}{a}\right) 2\pi \epsilon 2\sqrt{2}
a \to 4\sqrt{2} \pi F_0 a^2 \,\,\,\,\mbox{då }\,\,\,\,\epsilon \to 0
\end{equation}
$$

På ytan $S_+$  har vi

$$
\begin{equation}
  \int_{S_+} \vec{F} \cdot \mbox{d}\vec{S} = \int_{S_+} F_0 \left[\left(\frac{a}{\rho}
+ \frac{\rho}{a} \right) \hat{e}_{\rho} + \frac{z}{a}\hat{e}_{z}\right]
\cdot \hat{e}_{z} \mbox{d}S = \int_{S_+} F_0 \frac{z}{a} \mbox{d}S.
\end{equation}
$$

På $S_+$ är $z = \sqrt{2}a$, vilket också är skivans ytterradie

$$
\begin{equation}
  \int_{S_+} \vec{F} \cdot \mbox{d}\vec{S} = F_0 \sqrt{2} \pi 2a^2 = 2\sqrt{2} a^2F_0.\end{equation}
$$

Vi inser att av symmetriskäl får integralen över $S_-$ samma värde.

Summerar vi nu ihop integralerna får vi

$$
\begin{equation}
  \int_S \vec{F} \cdot \mbox{d}\vec{S} = 20 \sqrt{2} F_0 a^2 - 4 \sqrt{2} F_0 a^2
- 2 \times 2 \sqrt{2} F_0 a^2 = 12\sqrt{2} F_0 a^2.
\end{equation}
$$

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

### Exempel: Virveltråd

Beräkna integralen

$$
\begin{equation}
  \oint_C \vec{F} \cdot \mbox{d}\vec{r},
\end{equation}
$$

där kurvan $C$ ges av

$$
\begin{equation}
  x^2 + \frac{y^2}{4} = a^2,\,\,\,\mbox{och}\,\,\,z = 0,
\end{equation}
$$

som genomlöps i positiv riktning, och fältet ges av

$$
\begin{equation}
  \vec{F} = F_0 \left[\frac{\rho \sin 2\varphi}{2a} \hat{e}_{\rho}
+ \left(\frac{a}{\rho} - \frac{\rho \sin^2 \varphi}{a}\right)\hat{e}_{\varphi}
\right].
\end{equation}
$$

$F_0$ och $a$ är konstanter.

### Lösning

Kurvan $C$ är en ellips med centrum i origo och halvaxlarna $a$ och $2a$. Enligt högerhandsregeln så väljer vi $\hat{e}_{z}$ som normalen till ellipsskivan.

Fältet $\vec{F}$ är singulärt på $z$-axeln.  Fältets rotation blir

$$
\begin{equation}
  \nabla \times \vec{F} = \frac{F_0}{\rho} \left|
  \begin{array}{ccc}
\hat{e}_{\rho} & \rho \hat{e}_{\varphi} & \hat{e}_{z} \\
\frac{\partial}{\partial \rho} & \frac{\partial}{\partial \varphi} &
\frac{\partial}{\partial z} \\
\frac{\rho \sin 2\varphi}{2a} & a-
\frac{\rho^2\sin 2 \varphi}{a} & 0\\
\end{array}
\right| = \frac{F_0}{\rho} \left(-\frac{2\rho \sin^2\varphi}{a} -
\frac{\rho\cos 2\varphi}{a}\right) \hat{e}_{z} =
- \frac{F_0}{a}\hat{e}_{z}.
\end{equation}
$$

För att använda Stokes sats lägger in en cirkel, $C_\epsilon$ med radien $\epsilon$ runt origo.  Vi orienterar $C_\epsilon$ så att vi följer den medurs, det vill säga dess tangentvektor blir $-\hat{e}_{\varphi}$. Stokes sats ger oss sedan

$$
\begin{equation}
  \oint_C \vec{F} \cdot \mbox{d}\vec{r} + \oint_{C_\epsilon} \vec{F} \cdot \mbox{d}\vec{r}
= \int_S {\bf \nabla \times F \cdot \mbox{d}S}.
\end{equation}
$$

Först beräknar vi ytintegralen

$$
\begin{equation}
  \int_S \nabla \times \vec{F} \cdot \mbox{d}\vec{S} = \int_S -\frac{F_0}{a} \hat{z} \cdot \hat{z} \mbox{d}S = - \frac{F_0}{a} \pi a\times 2a = -2\pi F_0 a,
\end{equation}
$$

där vi har utnyttjat att ellipsens area är $2\pi a^2$.  Sedan beräknar vi integralen längs kurvan $C_\epsilon$ där $\rho = \epsilon$

$$
\begin{equation}
  \oint_{C_\epsilon} \vec{F} \cdot \mbox{d}\vec{r} = \oint_{C_\epsilon} F_0
\left[\frac{\rho \sin 2\varphi}{2a} \hat{e}_{\rho} + \left(\frac{a}{\rho}-
\frac{\rho \sin^2\varphi}{a}\right) \hat{e}_{\varphi}\right] \cdot
\left(-\hat{e}_{\varphi}\right) \mbox{d}r \nonumber
\end{equation}
$$

$$
\begin{equation}
= -F_0\left(
\oint_{C_\epsilon}\frac{a}{\epsilon} \mbox{d}r - \oint_{C_\epsilon}
\frac{\epsilon \sin^2 \varphi}{a} \mbox{d}r\right) \nonumber
\end{equation}
$$

$$
\begin{equation}
= -F_0
\left(\frac{a}{\epsilon} 2\pi \epsilon - \frac{\epsilon}{a} \oint_{C_\epsilon}
\sin^2 \varphi \mbox{d}r\right) \to -2\pi F_0 a \,\,\,\,\mbox{då } \,\,\,\,
\epsilon \to 0.
\end{equation}
$$

Det följer att integralen blir

$$
\begin{equation}
  \int_C \vec{F} \cdot \mbox{d}\vec{r} = 0.
\end{equation}
$$

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


Massimo Nocentini

November 9, 2016: script creation, splitting

November 7, 2016: check $\Phi_{ij}$ polys, `power` closed form

November {4,5,6}, 2016: `inverse`, `power`, `sqrt` $g$ polys

November 3, 2016: Fibonacci, $\mathcal{C}$, $d_{n,k}$ matrix for $\Phi$ polys

November 2, 2016: fix $\Phi$ polynomials def, $\mathcal{P}$

Abstract

Theory of matrix functions, with tutorial functions on simple matrix.

```python
from sympy import *
from sympy.abc import n, i, N, x, lamda, phi, z, j, r, k, a

from commons import *
from matrix_functions import *
from sequences import *
import functions_catalog

init_printing()
```

```python
M = define(Symbol(r'\mathcal{R}'), Matrix([[3, 1], [1, 3]]))
M
```

```python
m = M.rhs.rows # degree of \Xi minimal polynomial
```

```python
Phi_poly = Phi_poly_ctor(deg=m-1)
Phi_poly
```

```python
with lift_to_Lambda(Phi_poly) as Phi_fn:
    pass

Phi_fn, Phi_fn(z, 3, j)
```

```python
eigendata = spectrum(M)
eigendata
```

```python
data, eigenvals, multiplicities = eigendata.rhs
```

```python
Phi_polynomials = component_polynomials(eigendata)
Phi_polynomials
```

```python
f, h = Function('f'), Function('h')
function_eq = define(let=f(z), be=h(z))
function_eq
```

```python
g = Hermite_interpolation_polynomial(function_eq, eigendata, Phi_polynomials)
g
```

```python
g.subs({h: Lambda(z, sqrt(z))})
```

```python
g.subs({h: Lambda(z, 1/z)})
```

```python
t = g.subs(eigenvals)
t
```

```python
with lift_to_matrix_function(g) as g:
    res = g(M)

res
```

```python
define(res.lhs, res.rhs.subs(eigenvals))
```

```python
cmatrices = component_matrices(M, Phi_polynomials)
cmatrices
```

```python
Zi1 = list(cm.rhs.as_immutable() for (i, j), cm in cmatrices.items() if j == 1)
Zi1
```

```python
s = zeros(m)
for Z in Zi1:
    s += Z
s, s.subs(eigenvals)
```

### `const`

```python
f_const = define(let=f(z), be=r)
f_const
```

```python
g_const = Hermite_interpolation_polynomial(f_const, eigendata, Phi_polynomials)
g_const
```

```python
g_const = g_const.subs(eigenvals)
g_const
```

```python
with lift_to_matrix_function(g_const) as G_const:
    m_const = G_const(M)
m_const
```

### `identity`

```python
f_identity = Eq(f(z), z)
f_identity
```

```python
g_identity = Hermite_interpolation_polynomial(f_identity, eigendata, Phi_polynomials, matrix_form=True)
g_identity
```

```python
g_identity = Hermite_interpolation_polynomial(f_identity, eigendata, Phi_polynomials, matrix_form=False)
g_identity
```

```python
with lift_to_matrix_function(g_identity) as G_identity:
    m_identity = G_identity(M)
m_identity
```

### `sqrt`

```python
f_sqrt = define(let=f(z), be=sqrt(z))
f_sqrt
```

```python
g_sqrt = Hermite_interpolation_polynomial(f_sqrt, eigendata, Phi_polynomials)
g_sqrt
```

```python
g_sqrt = g_sqrt.subs(eigenvals)
g_sqrt
```

```python
with lift_to_matrix_function(g_sqrt) as G_sqrt:
    m_sqrt = G_sqrt(M)
m_sqrt
```

```python
assert (m_sqrt.rhs**2).applyfunc(simplify) == M.rhs
```

### `power`

```python
f_power = define(let=f(z), be=z**r)
f_power
```

```python
g_power = Hermite_interpolation_polynomial(f_power, eigendata, Phi_polynomials, matrix_form=True)
g_power
```

```python
g_power = Hermite_interpolation_polynomial(f_power, eigendata, Phi_polynomials)
g_power
```

```python
g_power = g_power.subs(eigenvals)
g_power.simplify()
```

```python
with lift_to_matrix_function(g_power) as G_power:
    m_power = G_power(M)
m_power
```

```python
assert (M.rhs**r).applyfunc(simplify) == m_power.rhs
```

### `log`

```python
f_log = define(let=f(z), be=log(z, 2))
f_log
```

```python
g_log = Hermite_interpolation_polynomial(f_log, eigendata, Phi_polynomials)
g_log
```

```python
g_log = g_log.subs(eigenvals)
g_log
```

```python
with lift_to_matrix_function(g_log) as G_log:
    M_log = G_log(M)
M_log
```

### `inverse`

```python
f_inverse = define(let=f(z), be=1/z)
f_inverse
```

```python
g_inverse = Hermite_interpolation_polynomial(f_inverse, eigendata, Phi_polynomials)
g_inverse
```

```python
with lift_to_matrix_function(g_inverse) as G_inverse:
    m_inverse = G_inverse(M)

m_inverse, m_inverse.rhs.subs(eigenvals)
```

### `expt`

```python
f_expt = define(let=f(z), be=exp(z))
f_expt
```

```python
g_expt = Hermite_interpolation_polynomial(f_expt, eigendata, Phi_polynomials)
g_expt
```

```python
g_expt = g_expt.subs(eigenvals)
g_expt
```

```python
with lift_to_matrix_function(g_expt) as G_expt:
    m_expt = G_expt(M)
m_expt
```

### `geometric`

```python
f_geo = define(let=f(z), be=(1-z**(r+1))/(1-z))
f_geo
```

```python
g_geo = Hermite_interpolation_polynomial(f_geo, eigendata, Phi_polynomials)
g_geo
```

```python
with lift_to_matrix_function(g_geo) as G_geo:
    m_geo = G_geo(M)

m_geo
```

```python
m_geo= define(m_geo.lhs, m_geo.rhs.subs(eigenvals))
m_geo
```

```python
define(m_geo.lhs, m_geo.rhs.subs({r:4}))
```

```python
s = Function('s')
sum_def = define(let=s(z), be=Sum(z**i, (i, 0, 4)).doit())
sum_def
```

```python
with lift_to_matrix_function(sum_def) as sum_fn:
    R = sum_fn(M)
R
```

---

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.