Table of Contents

1  Pytorch Introduction

1.1  Linear Regression

1.2  Linear Regression Version 2

1.3  Logistic Regression

1.4  Recurrent Neural Network (RNN)

1.4.1  Vanilla RNN

1.4.2  LSTM

2  Reference

```python
# code for loading the format for the notebook
import os

# path : store the current path to convert back to it later
path = os.getcwd()
os.chdir(os.path.join('..', '..', 'notebook_format'))
from formats import load_style
load_style(css_style = 'custom2.css', plot_style = False)
```

```python
os.chdir(path)

# 1. magic for inline plot
# 2. magic to print version
# 3. magic so that the notebook will reload external python modules
# 4. magic to enable retina (high resolution) plots
# https://gist.github.com/minrk/3301035
%matplotlib inline
%load_ext watermark
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = 'retina'

import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable

%watermark -a 'Ethen' -d -t -v -p torch,numpy,matplotlib
```

    Ethen 2018-05-10 19:51:37

    CPython 3.6.4
    IPython 6.3.1

    torch 0.4.0
    numpy 1.14.3
    matplotlib 2.2.2

# Pytorch Introduction

```bash
# installation on a mac
# for more information on installation refer to
# the following link:
# http://pytorch.org/
conda install pytorch torchvision -c pytorch
```

At its core, PyTorch provides two main features:

- An n-dimensional Tensor, similar to numpy array but can run on GPUs. PyTorch provides many functions for operating on these Tensors, thus it can be used as a general purpose scientific computing tool.
- Automatic differentiation for building and training neural networks.

Let's dive in by looking at some examples:

## Linear Regression

```python
# make up some trainig data and specify the type to be float, i.e. np.float32
# We DO not recommend double, i.e. np.float64, especially on the GPU. GPUs have bad
# double precision performance since they are optimized for float32
X_train = np.asarray([3.3, 4.4, 5.5, 6.71, 6.93, 4.168, 9.779, 6.182, 7.59,
                      2.167, 7.042, 10.791, 5.313, 7.997, 5.654, 9.27, 3.1], dtype = np.float32)
X_train = X_train.reshape(-1, 1)
y_train = np.asarray([1.7, 2.76, 2.09, 3.19, 1.694, 1.573, 3.366, 2.596, 2.53,
                      1.221, 2.827, 3.465, 1.65, 2.904, 2.42, 2.94, 1.3], dtype = np.float32)
y_train = y_train.reshape(-1, 1)

# Convert numpy array to Pytorch Tensors to hold input and outputs
# and wrap them into Variables. Variables wraps a Tensor and records
# the history of operations applied to it, i.e. it can record the
# gradients. Here we're setting requires_grad = False as it indicates
# we do not need to compute gradients with respect to these Variables
# during the backward pass
X = Variable(torch.from_numpy(X_train), requires_grad = False)
y = Variable(torch.from_numpy(y_train), requires_grad = False)
```

Here we start defining the linear regression model, recall that in linear regression, we are optimizing for the squared loss.

\begin{align}
L = \frac{1}{2}(y-(Xw + b))^2
\end{align}

```python
# with linear regression, we apply a linear transformation
# to the incoming data, i.e. y = Xw + b, here we only have a 1
# dimensional data, thus the feature size will be 1
model = nn.Linear(in_features = 1, out_features = 1)

# although we can write our own loss function, the nn module
# also contains definitions of popular loss functions; here
# we use the MSELoss, a.k.a the L2 loss, and size_average parameter
# simply divides it with the number of examples
criterion = nn.MSELoss(size_average = True)

# Then we use the optim module to define an Optimizer that will update the weights of
# the model for us. Here we will use SGD; but it contains many other
# optimization algorithms. The first argument to the SGD constructor tells the
# optimizer the parameters that it should update
learning_rate = 0.01
optimizer = optim.SGD(model.parameters(), lr = learning_rate)
```

```python
# start the optimization process
n_epochs = 100
for _ in range(n_epochs):
    # torch accumulates the gradients, thus before running new things
    # use the optimizer object to zero all of the gradients for the
    # variables it will update (which are the learnable weights of the model),
    # think in terms of refreshing the gradients before doing the another round of update
    optimizer.zero_grad()

    # forward pass: compute predicted y by passing X to the model
    output = model(X)

    # compute the loss function
    loss = criterion(output, y)

    # backward pass: compute gradient of the loss with respect to model parameters
    loss.backward()

    # call the step function on an Optimizer makes an update to its parameters
    optimizer.step()
```

```python
# plot the data and the fitted line to confirm the result

# change default style figure and font size
plt.rcParams['figure.figsize'] = 8, 6
plt.rcParams['font.size'] = 14

# access the data of the torch Variable,
# which will be a torch FloatTensor and
# convert it back to a numpy ndarray
y_pred = model(X).data.numpy()
plt.plot(X_train, y_train, 'ro', label = 'Original data')
plt.plot(X_train, y_pred, label = 'Fitted line')
plt.legend()
plt.show()
```

```python
# to get the parameters, i.e. weight and bias from the model,
# we can use the state_dict() attribute from the model that
# we've defined
model.state_dict()
```

    OrderedDict([('weight', tensor([[ 0.3040]])), ('bias', tensor([ 0.4274]))])

```python
# or we could get it from the model's parameter
# which by itself is a generator
param_list = list(model.parameters())
print(param_list[0].data, param_list[1].data)
```

    tensor([[ 0.3040]]) tensor([ 0.4274])

## Linear Regression Version 2

A better way of defining our model is to inherit the `nn.Module` class, to use it all we need to do is define our model's forward pass and the `nn.Module` will automatically define the backward method for us, where the gradients will be computed using autograd.

```python
class LinearRegression(nn.Module):

    def __init__(self, in_features, out_features):
        super().__init__()  # boilerplate call
        self.in_features = in_features
        self.out_features = out_features
        self.linear = nn.Linear(in_features, out_features)

    def forward(self, x):
        out = self.linear(x)
        return out

# same optimization process
n_epochs = 100
learning_rate = 0.01
criterion = nn.MSELoss(size_average = True)
model = LinearRegression(in_features = 1, out_features = 1)

# when we defined our LinearRegression class, we've assigned
# a neural network's component/layer to a class variable in the
# __init__ function, and now notice that we can directly call
# .parameters() on the class we've defined due to some Python magic
# from the Pytorch devs
optimizer = optim.SGD(model.parameters(), lr = learning_rate)

for epoch in range(n_epochs):
    # forward + backward + optimize
    optimizer.zero_grad()
    output = model(X)
    loss = criterion(output, y)
    loss.backward()
    optimizer.step()

    # print the loss per 20 epoch this time
    if (epoch + 1) % 20 == 0:
        # starting from pytorch 0.4.0, we use .item to get a python number from a
        # torch scalar, before loss.item() looks something like loss.data[0]
        print('Epoch [{}/{}], Loss: {:.4f}'.format(epoch + 1, n_epochs, loss.item()))
```

    Epoch [20/100], Loss: 0.4099
    Epoch [40/100], Loss: 0.3862
    Epoch [60/100], Loss: 0.3647
    Epoch [80/100], Loss: 0.3451
    Epoch [100/100], Loss: 0.3274

After training our model, we can also [save the model's parameter and load it back into the model in the future](https://stackoverflow.com/questions/42703500/best-way-to-save-a-trained-model-in-pytorch)

```python
checkpoint_path = 'model.pkl'
torch.save(model.state_dict(), checkpoint_path)
model.load_state_dict(torch.load(checkpoint_path))

y_pred = model(X).data.numpy()
plt.plot(X_train, y_train, 'ro', label = 'Original data')
plt.plot(X_train, y_pred, label = 'Fitted line')
plt.legend()
plt.show()
```

## Logistic Regression

Let's now look at a classification example, here we'll define a logistic regression that takes in a bag of words representation of some text and predicts over two labels "English" and "Spanish".

```python
# define some toy dataset
train_data = [
    ('me gusta comer en la cafeteria'.split(), 'SPANISH'),
    ('Give it to me'.split(), 'ENGLISH'),
    ('No creo que sea una buena idea'.split(), 'SPANISH'),
    ('No it is not a good idea to get lost at sea'.split(), 'ENGLISH')
]

test_data = [
    ('Yo creo que si'.split(), 'SPANISH'),
    ('it is lost on me'.split(), 'ENGLISH')
]
```

The next code chunk create words to index mappings. To build our bag of words (BoW) representation, we need to assign each word in our vocabulary an unique index. Let's say our entire corpus only consists of two words "hello" and "world", with "hello" corresponding to index 0 and "world" to index 1. Then the BoW vector for the sentence "hello world hello world" will be [2, 2], i.e. the count for the word "hello" will be at position 0 of the array and so on.

```python
idx_to_label = ['SPANISH', 'ENGLISH']
label_to_idx = {"SPANISH": 0, "ENGLISH": 1}

word_to_idx = {}
for sent, _ in train_data + test_data:
    for word in sent:
        if word not in word_to_idx:
            word_to_idx[word] = len(word_to_idx)
print(word_to_idx)

VOCAB_SIZE = len(word_to_idx)
NUM_LABELS = len(label_to_idx)
```

    {'me': 0, 'gusta': 1, 'comer': 2, 'en': 3, 'la': 4, 'cafeteria': 5, 'Give': 6, 'it': 7, 'to': 8, 'No': 9, 'creo': 10, 'que': 11, 'sea': 12, 'una': 13, 'buena': 14, 'idea': 15, 'is': 16, 'not': 17, 'a': 18, 'good': 19, 'get': 20, 'lost': 21, 'at': 22, 'Yo': 23, 'si': 24, 'on': 25}

Next we define our model using the inherenting from `nn.Module` approach and also two helper functions to convert our data to torch Tensors so we can use to during training.

```python
class BoWClassifier(nn.Module):

    def __init__(self, vocab_size, num_labels):
        super().__init__()
        self.linear = nn.Linear(vocab_size, num_labels)

    def forward(self, bow_vector):
        """
        When we're performing a classification, after passing
        through the linear layer or also known as the affine layer
        we also need pass it through the softmax layer to convert a vector
        of real numbers into probability distribution, here we use
        log softmax for numerical stability reasons.
        """
        return F.log_softmax(self.linear(bow_vector), dim = 1)
```

```python
def make_bow_vector(sentence, word_to_idx):
    vector = torch.zeros(len(word_to_idx))
    for word in sentence:
        vector[word_to_idx[word]] += 1

    return vector.view(1, -1)

def make_target(label, label_to_idx):
    return torch.LongTensor([label_to_idx[label]])
```

We are now ready to train this!

```python
model = BoWClassifier(VOCAB_SIZE, NUM_LABELS)

# note that instead of using NLLLoss (negative log likelihood),
# we could have used CrossEntropyLoss and remove the log_softmax
# function call in our forward method. The CrossEntropyLoss docstring
# explciitly states that this criterion combines `LogSoftMax` and
# `NLLLoss` in one single class.
criterion = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr = 0.1)

n_epochs = 100
for epoch in range(n_epochs):
    for instance, label in train_data:
        bow_vector = Variable(make_bow_vector(instance, word_to_idx))
        target = Variable(make_target(label, label_to_idx))

        # standard step to perform the forward and backward step
        model.zero_grad()
        log_probs = model(bow_vector)
        loss = criterion(log_probs, target)
        loss.backward()
        optimizer.step()

# predict on the test data to check if the model actually learned anything
for instance, label in test_data:
    bow_vec = Variable(make_bow_vector(instance, word_to_idx))
    log_probs = model(bow_vec)

    y_pred = np.argmax(log_probs.data[0].numpy())
    label_pred = idx_to_label[y_pred]
    print('true label: ', label, ' predicted label: ', label_pred)
```

    true label:  SPANISH  predicted label:  SPANISH
    true label:  ENGLISH  predicted label:  ENGLISH

**Important:** Pytorch 0.4.0 introduced many new enhancements/changes. It's worth the time to read through the migration guide. [PyTorch Documentation: PyTorch 0.4.0 Migration Guide](https://pytorch.org/2018/04/22/0_4_0-migration-guide.html)

## Recurrent Neural Network (RNN)

The idea behind RNN is to make use of sequential information that exists in our dataset. In feedforward neural network, we assume that all inputs and outputs are independent of each other. But for some tasks, this might not be the best way to tackle the problem. For example, in Natural Language Processing (NLP) applications, if we wish to predict the next word in a sentence (one business application of this is [Swiftkey](https://en.wikipedia.org/wiki/SwiftKey)), then we could imagine that knowing the word that comes before it can come in handy.

### Vanilla RNN

The input $x$ will be a sequence of words, and each $x_t$ is a single word. And because of how matrix multiplication works, we can't simply use a word index like (36) as an input, instead we represent each word as a one-hot vector with a size of the total number of vocabulary. For example, the word with index 36 have the value 1 at position 36 and the rest of the value in the vector would all be 0's.

```python
torch.manual_seed(777)

# suppose we have a
# one hot encoding for each char in 'hello'
# and the sequence length for the word 'hello' is 5
seq_len = 5
h = [1, 0, 0, 0]
e = [0, 1, 0, 0]
l = [0, 0, 1, 0]
o = [0, 0, 0, 1]

# here we specify a single RNN cell with the property of
# input_dim (4) -> output_dim (2)
# batch_first explained in the following
rnn_cell = nn.RNN(input_size = 4, hidden_size = 2, batch_first = True)

# our input shape should be of shape
# (batch, seq_len, input_size) when batch_first=True;
# the input size basically refers to the number of features
# (seq_len, batch_size, input_size) when batch_first=False (default)
# thus we reshape our input to the appropriate size, torch.view is
# equivalent to numpy.reshape
inputs = Variable(torch.Tensor([h, e, l, l, o]))
inputs = inputs.view(1, 5, -1)

# our hidden is the weights that gets passed along the cells,
# here we initialize some random values for it:
# (batch, num_layers * num_directions, hidden_size) for batch_first=True
# disregard the second argument as of now
hidden = Variable(torch.zeros(1, 1, 2))
out, hidden = rnn_cell(inputs, hidden)
print('sequence input size', inputs.size())
print('out size', out.size())
print('sequence size', hidden.size())

# the first value returned by the rnn cell is all
# of the hidden state throughout the sequence, while
# the second value is the most recent hidden state;
# hence we can compare the last slice of the the first
# value with the second value to confirm that they are
# the same
print('\ncomparing rnn cell output:')
print(out[:, -1, :])
hidden[0]
```

    sequence input size torch.Size([1, 5, 4])
    out size torch.Size([1, 5, 2])
    sequence size torch.Size([1, 1, 2])

    comparing rnn cell output:
    tensor([[-0.7762,  0.8319]])

    tensor([[-0.7762,  0.8319]])

In the next section, we'll teach our RNN to produce "ihello" from "hihell".

```python
# create an index to character mapping
idx2char = ['h', 'i', 'e', 'l', 'o']

# Teach hihell -> ihello
x_data = [[0, 1, 0, 2, 3, 3]]    # hihell
x_one_hot = [[[1, 0, 0, 0, 0],   # h 0
              [0, 1, 0, 0, 0],   # i 1
              [1, 0, 0, 0, 0],   # h 0
              [0, 0, 1, 0, 0],   # e 2
              [0, 0, 0, 1, 0],   # l 3
              [0, 0, 0, 1, 0]]]  # l 3

x_one_hot = np.asarray(x_one_hot)
y_data = np.asarray([1, 0, 2, 3, 3, 4])  # ihello

# As we have one batch of samples, we will change them to variables only once
inputs = Variable(torch.Tensor(x_one_hot))
labels = Variable(torch.LongTensor(y_data))

# hyperparameters
seq_len = 6      # |hihell| == 6, equivalent to time step
input_size = 5   # one-hot size
batch_size = 1   # one sentence per batch
num_layers = 1   # one-layer rnn
num_classes = 5  # predicting 5 distinct character
hidden_size = 4  # output from the RNN
```

```python
class RNN(nn.Module):
    """
    The RNN model will be a RNN followed by a linear layer,
    i.e. a fully-connected layer
    """
    def __init__(self, seq_len, num_classes, input_size, hidden_size, num_layers):
        super().__init__()
        self.seq_len = seq_len
        self.num_layers = num_layers
        self.input_size = input_size
        self.num_classes = num_classes
        self.hidden_size = hidden_size
        self.rnn = nn.RNN(input_size, hidden_size, batch_first = True)
        self.linear = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        # assuming batch_first = True for RNN cells
        batch_size = x.size(0)
        hidden = self._init_hidden(batch_size)
        x = x.view(batch_size, self.seq_len, self.input_size)

        # apart from the output, rnn also gives us the hidden
        # cell, this gives us the opportunity to pass it to
        # the next cell if needed; we won't be needing it here
        # because the nn.RNN already computed all the time steps
        # for us. rnn_out will of size [batch_size, seq_len, hidden_size]
        rnn_out, _ = self.rnn(x, hidden)
        linear_out = self.linear(rnn_out.view(-1, hidden_size))
        return linear_out

    def _init_hidden(self, batch_size):
        """
        Initialize hidden cell states, assuming
        batch_first = True for RNN cells
        """
        return Variable(torch.zeros(
            batch_size, self.num_layers, self.hidden_size))
```

```python
# Set loss, optimizer and the RNN model
torch.manual_seed(777)
rnn = RNN(seq_len, num_classes, input_size, hidden_size, num_layers)
print('network architecture:\n', rnn)

# train the model
num_epochs = 15
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(rnn.parameters(), lr = 0.1)
for epoch in range(1, num_epochs + 1):
    optimizer.zero_grad()
    outputs = rnn(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

    # check the current predicted string
    # max gives the maximum value and its
    # corresponding index, we will only
    # be needing the index
    _, idx = outputs.max(dim = 1)
    idx = idx.data.numpy()
    result_str = [idx2char[c] for c in idx]
    print('epoch: {}, loss: {:1.3f}'.format(epoch, loss.item()))
    print('Predicted string: ', ''.join(result_str))
```

    network architecture:
     RNN(
      (rnn): RNN(5, 4, batch_first=True)
      (linear): Linear(in_features=4, out_features=5, bias=True)
    )
    epoch: 1, loss: 1.756
    Predicted string:  eeeeee
    epoch: 2, loss: 1.626
    Predicted string:  ehhhhh
    epoch: 3, loss: 1.485
    Predicted string:  elllll
    epoch: 4, loss: 1.405
    Predicted string:  llllll
    epoch: 5, loss: 1.293
    Predicted string:  illlll
    epoch: 6, loss: 1.217
    Predicted string:  iiilll
    epoch: 7, loss: 1.057
    Predicted string:  iollll
    epoch: 8, loss: 0.967
    Predicted string:  ielllo
    epoch: 9, loss: 0.837
    Predicted string:  ihlllo
    epoch: 10, loss: 0.696
    Predicted string:  ihello
    epoch: 11, loss: 0.615
    Predicted string:  ihello
    epoch: 12, loss: 0.535
    Predicted string:  ihhllo
    epoch: 13, loss: 0.452
    Predicted string:  ihhllo
    epoch: 14, loss: 0.387
    Predicted string:  ihello
    epoch: 15, loss: 0.322
    Predicted string:  ihello

### LSTM

The example below uses an LSTM to generate part of speech tags. The usage of LSTM API is essentially the same as the RNN we were using in the last section. Expect in this example, we will prepare the word to index mapping ourselves and as for the modeling part, we will add an embedding layer before the LSTM layer, this is a common technique in NLP applications. So for each word, instead of using the one hot encoding way of representation the data (which can be inefficient and it treats all words as independent entities with no relationships amongst each other), word embeddings will compress them into a lower dimension that encode the semantics of the words, i.e. how similar each word is used within our given corpus.

```python
# These will usually be more like 32 or 64 dimensional.
# We will keep them small for this toy example
EMBEDDING_SIZE = 6
HIDDEN_SIZE = 6

training_data = [
    ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]),
    ("Everybody read that book".split(), ["NN", "V", "DET", "NN"])
]

idx_to_tag = ['DET', 'NN', 'V']
tag_to_idx = {'DET': 0, 'NN': 1, 'V': 2}

word_to_idx = {}
for sent, tags in training_data:
    for word in sent:
        if word not in word_to_idx:
            word_to_idx[word] = len(word_to_idx)

word_to_idx
```

    {'The': 0,
     'dog': 1,
     'ate': 2,
     'the': 3,
     'apple': 4,
     'Everybody': 5,
     'read': 6,
     'that': 7,
     'book': 8}

```python
def prepare_sequence(seq, to_idx):
    """Convert sentence/sequence to torch Tensors"""
    idxs = [to_idx[w] for w in seq]
    tensor = torch.LongTensor(idxs)
    return Variable(tensor)

seq = training_data[0][0]
inputs = prepare_sequence(seq, word_to_idx)
inputs
```

    tensor([ 0,  1,  2,  3,  4])

```python
class LSTMTagger(nn.Module):

    def __init__(self, embedding_size, hidden_size, vocab_size, tagset_size):
        super().__init__()
        self.embedding_size = embedding_size
        self.hidden_size = hidden_size
        self.vocab_size = vocab_size
        self.tagset_size = tagset_size

        self.embedding = nn.Embedding(vocab_size, embedding_size)
        self.lstm = nn.LSTM(embedding_size, hidden_size)
        self.hidden2tag = nn.Linear(hidden_size, tagset_size)

    def forward(self, x):
        embed = self.embedding(x)
        hidden = self._init_hidden()

        # the second dimension refers to the batch size, which we've hard-coded
        # it as 1 throughout the example
        lstm_out, lstm_hidden = self.lstm(embed.view(len(x), 1, -1), hidden)
        output = self.hidden2tag(lstm_out.view(len(x), -1))
        return output

    def _init_hidden(self):
        # the dimension semantics are [num_layers, batch_size, hidden_size]
        return (Variable(torch.rand(1, 1, self.hidden_size)),
                Variable(torch.rand(1, 1, self.hidden_size)))
```

```python
model = LSTMTagger(EMBEDDING_SIZE, HIDDEN_SIZE, len(word_to_idx), len(tag_to_idx))
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr = 0.1)

epochs = 300
for epoch in range(epochs):
    for sentence, tags in training_data:
        model.zero_grad()

        sentence = prepare_sequence(sentence, word_to_idx)
        target = prepare_sequence(tags, tag_to_idx)

        output = model(sentence)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
```

```python
inputs = prepare_sequence(training_data[0][0], word_to_idx)
tag_scores = model(inputs)

# validating that the sentence "the dog ate the apple".
# the correct tag should be DET NOUN VERB DET NOUN
print('expected target: ', training_data[0][1])

tag_scores = tag_scores.data.numpy()
tag = [idx_to_tag[idx] for idx in np.argmax(tag_scores, axis = 1)]
print('generated target: ', tag)
```

    expected target:  ['DET', 'NN', 'V', 'DET', 'NN']
    generated target:  ['DET', 'NN', 'V', 'DET', 'NN']

# Reference

- [Github: Simple PyTorch Tutorials Zero to ALL!](https://github.com/hunkim/PyTorchZeroToAll)
- [Github: PyTorch Tutorial for Deep Learning Researchers](https://github.com/yunjey/pytorch-tutorial)
- [PyTorch Documentation: Deep Learning for NLP with Pytorch](http://pytorch.org/tutorials/beginner/deep_learning_nlp_tutorial.html)
- [PyTorch Documentation: Deep Learning with PyTorch: A 60 Minute Blitz](http://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html)


# Week 1 Workshop/Lab

---

## Setup

```
import sys
from pathlib import Path

# Notebook Library
url = "https://github.com/Data-Science-and-Data-Analytics-Courses/Notebook-Library"
repo = Path("/nblib")
!git clone "{url}" "{repo}"
if repo.parent.as_posix() not in sys.path:
  sys.path.append(repo.parent.as_posix())
%run "{repo}/.Importable.ipynb"

from nblib.imports.Basic import *
from nblib import Git

# Remote
URL = "https://github.com/Data-Science-and-Data-Analytics-Courses/UniMelb-A-First-Course-In-Statistical-Learning-MAST90104_2019_SM2"
REPO = Git.clone(URL, dest="/content")
if REPO.as_posix() not in sys.path:
  sys.path.append(REPO.as_posix())

# Working directory
os.chdir(REPO)

from nblib.imports.RPy import *
from setup.Setup import *

clear_output()
```

## Workshop

### Question
Is
$$
X = \left[ \begin{array} {rrrr}
1 & 1 & −1 & −1 \\
1 & −1 & 1 & −1 \\
1 & −1 & −1 & 1 \\
1 & 1 & 1 & 1 \\
\end{array} \right]
$$
orthogonal? If not, what value of c makes the matrix cX orthogonal?

```r
%%R

print("X orthogonal?")
x <- matrix(c(1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, 1), nrow = 4, ncol = 4, byrow = TRUE)
xTx <- t(x) %*% x
print(xTx)
print(all(xTx == diag(4)))

c <- 1/2
cat("c = ", c, "\n")
y <- c * x
yTy = t(y) %*% y
print(yTy)
print(all(yTy == diag(4)))
```

    [1] "X orthogonal?"
         [,1] [,2] [,3] [,4]
    [1,]    4    0    0    0
    [2,]    0    4    0    0
    [3,]    0    0    4    0
    [4,]    0    0    0    4
    [1] FALSE
    c =  0.5
         [,1] [,2] [,3] [,4]
    [1,]    1    0    0    0
    [2,]    0    1    0    0
    [3,]    0    0    1    0
    [4,]    0    0    0    1
    [1] TRUE

### Question
Show that if X is of full rank, then
$$
I − X(X^T X)^{−1}X^T
$$
is an idempotent matrix.

Let
$$
H = I − X(X^T X)^{−1}X^T \quad (\text{square})
$$

Then
$$
\begin{align}
H^2 &= (I − X(X^T X)^{−1}X^T) (I − X(X^T X)^{−1}X^T) \\
&= I -2X(X^T X)^{−1}X^T + (X(X^T X)^{−1}X^T) (X(X^T X)^{−1}X^T) \\
&= I -2X(X^T X)^{−1}X^T + X(X^T X)^{−1}\left((X^TX)(X^T X)^{−1}\right)X^T \\
&= I -2X(X^T X)^{−1}X^T + X(X^T X)^{−1}X^T & ((X^TX)(X^T X)^{−1} = I) \\
&= I -X(X^T X)^{−1}X^T = H \\
\end{align}
$$

Thus H is idempotent.

```

```


# Compare two possibilities to define the ITCZ position

## Purpose

The ITCZ is typically defined by means of the precipitation centroid. The centroid offers a more stable metric compared to the location of the precipitation maximum, which for example is sensitive to small precipitation changes for the case of a "double ITCZ".

Yet, there are different choices what is meant by the centroid, and also within which latitude range it should be calculated (e.g., between 20N/S deg lat or 30N/S deg lat). The latter point is not studied here. The first point is investigated in the following.

At least two definitions of the centroid have been used:

1. The "Adam" definition, documented in Eq.1a of Adam et al., 2016, J. Climate, Seasonal and Interannual Variations of the Energy Flux Equator and ITCZ. Part I: Zonally Averaged ITCZ Position, https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-15-0512.1 with N=1. Note that in Adam et al., 2016, the latitude boundaries were 20N/S deg lat.

2. The "Voigt" definition used in the Tracmip introduction paper Voigt et al., 2016, JAMES, https://agupubs.onlinelibrary.wiley.com/doi/full/10.1002/2016MS000748. Note that in Voigt et al., 2016, the latitude boundaries were 30N/S deg lat.

Both definitions have been used as illustrated by the following non-exhaustive list:

 * Frierson, D. and Y.-T. Hwang (2012), Extratropical Influence on ITCZ Shifts in Slab Ocean Simulations of Global Warming, Journal of Climate, DOI: 10.1175/JCLI-D-11-00116.1 (Voigt definition with 15N/S deg lat; indeed this is where the "Voigt" definition originates from)
 * Voigt, A. et al. (2014), The radiative impact of clouds on the shift of the Intertropical Convergence Zone, Geophys. Res. Lett., 41, 4308–4315, doi:10.1002/2014GL060354. (Voigt definition with 30N/S deg lat)
 * Voigt, A. et al. (2014), Compensation of Hemispheric Albedo Asymmetries by Shifts of the ITCZ and Tropical Clouds, Journal of Climate, https://doi.org/10.1175/JCLI-D-13-00205.1. (Voigt definition with 30N/S deg lat)
 * Donohoe, A. and A. Voigt (2017), Why Future Shifts in Tropical Precipitation Will Likely Be Small: The Location of the Tropical Rain Belt and the Hemispheric Contrast of Energy Input to the Atmosphere, Climate Extremes: Patterns and Mechanisms, AGU Book. (Voigt definition with 20N/S deg lat)

In the following, we will document to what extent the two definitions differ quantitatively. We do so by using precipitation from the TRACMIP aquaControl simulations that are described in Voigt et al., 2016, JAMES and that we access via the pangeo cloud.

The ITCZ definition problem might seem an academic problem - the precipitation is what it is, independent of how we decide to diagnose the ITCZ position. But in fact the choice of definition can be important for conceptual pictures of the ITCZ and tropical rainfall, such as those embodied in the so-called energetic framework that link the ITCZ position to cross-equatorial atmosphere energy transport. The ratio of ITCZ position and energy transport could thus be sensitive to how the ITCZ position is diagnosed.

## Background

The Adam-based definition uses the equation:

\begin{equation}
  \varphi_\text{Adam}= \frac{ \int_{-\varphi_0}^{\varphi_0} \varphi P(\varphi) \cos\varphi d\varphi }{ \int_{-\varphi_0}^{\varphi_0} P(\varphi) \cos\varphi d\varphi } .
\end{equation}

This defintion of the ITCZ position is in analogy to a lever, where the force is the product of the weight (precipition P) and the distance from the fulcrum (abs(latitude-ITCZ position)). Thus, for the Adam definition one has

\begin{equation}
  \int_{-\varphi_0}^{\varphi_\text{Adam}} abs(\varphi-\varphi_\text{Adam}) P(\varphi) \cos\varphi d\varphi = \int_{\varphi_\text{Adam}}^{\varphi_0} abs(\varphi-\varphi_\text{Adam}) P(\varphi) \cos\varphi d\varphi .
\end{equation}

The Voigt-based definition instead follows the idea that the ITCZ is given by the latitude for which the sum of the area-weighted precipitation south and north of it are the same:

\begin{equation}
  \int_{-\varphi_0}^{\varphi_\text{Voigt}} P(\varphi) \cos\varphi d\varphi = \int_{\varphi_\text{Voigt}}^{\varphi_0} P(\varphi) \cos\varphi d\varphi .
\end{equation}

I.e., while the Adam definition weighs precip more strongly according to its distance from the "fulcrum", the Voigt definition just has two buckets of precip that need to have the same filling level but are at the same distance from the fulcrum.

This shows that while both definitions are closely related, the Adam definition gives more weight to latitudes further away from the diagnosed ITCZ. As a result, it will tend to put the ITCZ closer to the equator than the Voigt definition, for which it does not matter whether the precipitation falls near or far from the diagnosed ITCZ.

## Analysis

### Load Tracmip aquaControl precipitation

```python
from intake import open_catalog
# get whole pangeo catalogue
cat = open_catalog("https://raw.githubusercontent.com/pangeo-data/pangeo-datastore/master/intake-catalogs/climate.yaml")
# select tracmip collection
col = cat.tracmip()
# load data into dictionary
ds_dict = col.search(frequency='Amon', experiment=['aquaControl'],
                     variable=['pr']).to_dataset_dict(zarr_kwargs={'consolidated': True})
```

    Dataset(s):   0%|                                       | 0/14 [00:00<?, ?it/s]

    --> The keys in the returned dictionary of datasets are constructed as follows:
    	'model.experiment.frequency'

    Dataset(s): 100%|██████████████████████████████| 14/14 [00:09<00:00,  1.47it/s]

```python
# uncomment the following line to check content of dataset dictionary ds_dict
#print(ds_dict.keys())
# lists of models
models = list(col.df.model.unique())
```

### Calculate ITCZ positions according to Adam and Voigt definitions

Dictionary of model colors, same as in Voigt et al. 2016, JAMES, TRACMIP introduction paper

```python
import numpy as np
dict_color = {'AM21': np.array([1. , 0.8, 0.6]), 'CAM3':np.array([0.50196078, 0.50196078, 0.50196078]),
              'CAM4': np.array([0.58039216, 1.        , 0.70980392]), 'CAM5Nor':np.array([0.76078431, 0.        , 0.53333333]),
              'CNRM-AM5': np.array([0.        , 0.2       , 0.50196078]), 'ECHAM61':np.array([0.        , 0.45882353, 0.8627451 ]),
              'ECHAM63':np.array([0.6       , 0.24705882, 0.        ]), 'GISS-ModelE2':np.array([0.61568627, 0.8       , 0.        ]),
              'LMDZ5A':np.array([0.29803922, 0.        , 0.36078431]), 'MetUM-CTL':np.array([0.09803922, 0.09803922, 0.09803922]),
              'MetUM-ENT':np.array([0.        , 0.36078431, 0.19215686]), 'MIROC5':np.array([0.16862745, 0.80784314, 0.28235294]),
              'MPAS':np.array([0.56078431, 0.48627451, 0.        ]), 'CALTECH': np.array([1.        , 0.64313725, 0.01960784])}
```

Load my implementation of the ITCZ definitions

```python
import itcz
```

**Calculate ITCZ positions using the zonal-mean time-mean precipitation from the last 30 years of the aquaControl simulation using the latitude boundaries of 20N/S deg lat**

```python
for mod in models:
    ds_dict[mod+'.aquaControl.Amon']['itcz_adam'] = (
         itcz.get_itczposition_adam(ds_dict[mod+'.aquaControl.Amon']['pr'][-360:,:,:].mean(['time','lon']),
                             ds_dict[mod+'.aquaControl.Amon'].lat, latboundary=20, dlat=0.1) )
    ds_dict[mod+'.aquaControl.Amon']['itcz_voigt'] = (
         itcz.get_itczposition_voigt(ds_dict[mod+'.aquaControl.Amon']['pr'][-360:,:,:].mean(['time','lon']),
                             ds_dict[mod+'.aquaControl.Amon'].lat, latboundary=20, dlat=0.05) )
```

**Scatter plot of Adam versus Voigt definitions (left panel) and the ratio of the two (right panel):**

This shows that the two definitions agree well, but that the Voigt definition yields more poleward ITCZ position, in agreement with the considerations in the background section.

```python
import matplotlib.pyplot as plt
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
for mod in models:
    plt.plot(ds_dict[mod+'.aquaControl.Amon']['itcz_adam'],
             ds_dict[mod+'.aquaControl.Amon']['itcz_voigt'],'o', color=dict_color[mod])
plt.xlabel('Adam ITCZ (deg lat)'); plt.ylabel('Voigt ITCZ (deg lat)');

plt.subplot(1,2,2)
for mod in models:
    plt.plot(ds_dict[mod+'.aquaControl.Amon']['itcz_adam'],
             ds_dict[mod+'.aquaControl.Amon']['itcz_voigt']/ds_dict[mod+'.aquaControl.Amon']['itcz_adam'],'x', color=dict_color[mod])
plt.xlabel('Adam ITCZ (deg lat)'); plt.ylabel('Voigt ITCZ / Adam ITCZ');
```

**Test that the Voigt and Adam results agree with the integral equations given in the background section.**

We use the criterion that for both definitions the l.h.s and r.h.s agree within 1%. Note that the ITCZ positions are recalculated within the test subroutines.

```python
for mod in models:
    lhs, rhs = itcz.test_itczposition_adam(ds_dict[mod+'.aquaControl.Amon']['pr'][-360:,:,:].mean(['time','lon']),
                            ds_dict[mod+'.aquaControl.Amon'].lat, 20, 0.1)
    if (np.abs(lhs-rhs)>0.01*rhs):
        print(mod, ': Adam test FAILED!')
    else:
        print(mod, ': Adam test passed!')
```

    AM21 : Adam test passed!
    CAM4 : Adam test passed!
    CAM5Nor : Adam test passed!
    CNRM-AM5 : Adam test passed!
    ECHAM61 : Adam test passed!
    ECHAM63 : Adam test passed!
    GISS-ModelE2 : Adam test passed!
    MIROC5 : Adam test passed!
    MPAS : Adam test passed!
    MetUM-CTL : Adam test passed!
    MetUM-ENT : Adam test passed!
    CAM3 : Adam test passed!
    LMDZ5A : Adam test passed!
    CALTECH : Adam test passed!

```python
for mod in models:
    lhs, rhs = itcz.test_itczposition_voigt(ds_dict[mod+'.aquaControl.Amon']['pr'][-360:,:,:].mean(['time','lon']),
                            ds_dict[mod+'.aquaControl.Amon'].lat, 20, 0.05)
    if (np.abs(lhs-rhs)>0.01*rhs):
        print(mod, ': Voigt test FAILED!')
    else:
        print(mod, ': Voigt test passed!')
```

    AM21 : Voigt test passed!
    CAM4 : Voigt test passed!
    CAM5Nor : Voigt test passed!
    CNRM-AM5 : Voigt test passed!
    ECHAM61 : Voigt test passed!
    ECHAM63 : Voigt test passed!
    GISS-ModelE2 : Voigt test passed!
    MIROC5 : Voigt test passed!
    MPAS : Voigt test passed!
    MetUM-CTL : Voigt test passed!
    MetUM-ENT : Voigt test passed!
    CAM3 : Voigt test passed!
    LMDZ5A : Voigt test passed!
    CALTECH : Voigt test passed!


# FFM232, Klassisk fysik och vektorfält - Föreläsningsanteckningar

 **[Christian Forssén](http://fy.chalmers.se/subatom/tsp/), Institutionen för fysik, Chalmers, Göteborg, Sverige**

Date: **Sep 9, 2016**

# 12. Tensorer

* Fysikaliska lagar skall inte bero på i vilket koordinatsystem de beskrivs.

* Detta kan vi åstadkomma genom att skriva dessa lagar som en likhet mellan två objekt vilka vi vet transformerar likadant under en koordinattransformation.

* Vi kommer att kalla sådana objekt för *tensorer*.

* En invarians kan kopplas till en symmetri (t.ex. rotationssymmetri). Invariansen kan kodas in genom att använda"tensorspråket".

* Kan generaliseras till större symmetrier. T.ex. Lorentzinvarians i speciell relativitetsteori vilken involverar rum-tiden (fyra dimensioner); och mer allmänna koordinattransformationer i allmän relativitetsteori.

Plan:
* Hur beskriva transformation mellan cartesiska koordinatsystem

* Transformationsegenskaper hos: Skalär, vektor, ... generell tensor

* Visa att diverse objekt verkligen är tensorer

* Fysikaliskt exempel: Tröghetstensorn

## Koordinattransformationer

När vi byter koordinater från ett Cartesiskt högersystem, med koordinater
$x_i$, till ett annat (med origo i samma punkt), med koordinater $x'_i$, relateras

$$
\begin{equation}
x'_i=L_{ij}x_j,
\label{eq:transformation} \tag{1}
\end{equation}
$$

där $\mathbf{L}$ är en ortogonal matris som uppfyller $\mathbf{L}\mathbf{L}^t = \mathbf{I}=\mathbf{L}^t\mathbf{L}$. Från detta följer direkt att $\det(\mathbf{L}^t\mathbf{L}) =  \det(\mathbf{L})^2 = \det\mathbf{I}=1$ vilket ger $\det\mathbf{L}=\pm 1$.

[Rita 1: $x'y'$-system som är roterat en vinkel $\alpha$ relativt ett $xy$-system.]

$$
\begin{equation}
\begin{pmatrix}
x' \\ y'
\end{pmatrix}
=
\begin{pmatrix}
\cos\alpha & \sin\alpha \\
-\sin\alpha & \cos\alpha
\end{pmatrix}
\begin{pmatrix}
x \\ y
\end{pmatrix}
\end{equation}
$$

[Comment 2: Om $\mathbf{L}$ transformerar ett högersystem till ett högersystem gäller plustecknet, $\det\mathbf{L}=1$. Kolla exemplet ovan att $\mathbf{L}^t \mathbf{L} = \mathbf{I}$.]

I indexnotation

$$
\begin{equation}
\left( L^t L \right)_{ij} = \left( L^t \right)_{ik} L_{kj}
= L_{ki}L_{kj}=\delta_{ij}.
\end{equation}
$$

Ett allmänt uttryck för determinanten av en $(3\times3)$-matris är

$$
\begin{equation}
\det\mathbf{M}=\varepsilon^{ijk}M_{1i}M_{2j}M_{3k}
\end{equation}
$$

[Comment 3: Kontrollera gärna. Detta uttryck ger en summa med sex nollskilda termer (3 positiva och 3 negativa) vilket alltså motsvarar determinanten.]

Notera att vi genom att derivera Ekv. [(1)](#eq:transformation) kan skriva matrisen $\mathbf{L}$ som $L_{ij}=\frac{\partial x'_i}{\partial x_j}$. Eftersom matrisen är ortogonal gäller den inversa relationen $x_i=L_{ji}x'_j$, och vi har också $L_{ji}=\frac{\partial x_i}{\partial x'_j}$.

## Skalärer och vektorer
En skalär $s$ (= enklaste exempel på en tensor) kännetecknas av att den tar samma värde i alla koordinatsystem, dvs. $s'=s$.

[Comment 4: Det är viktigt att förstå att det är transformationsregeln som är det viktiga. Det räcker inte med att "$s$ är ett tal".]

[Rita 5: En punkt P i ovanstående två koordinatsystem och illustrera dess $x'$- och $x$-koordinat.]

Sålunda är $x$-koordinaten för en punkt i $\mathbf{R}^3$ inte en skalär, medan t.ex. temperaturen i en punkt är en skalär.

En vektor $\vec v$ är en uppsättning tal som beter sig likadant som ortvektorns komponenter när vi byter system, dvs.

$$
\begin{equation}
v'_i=L_{ij}v_j
\end{equation}
$$

[Comment 6: Det är denna transformationsregel som definierar vilka uppsättningar av tre (eller $D$) tal som får privilegiet att kallas vektor.]

## Tensorer
[Comment 7: Nu är det rättframt att gå vidare och definiera objekt,  tensorer, som har fler än ett index. En matris kan, som vi redan sett, skrivas som en tensor med två index, $T_{ij}$. Men inget hindrar att man har ett godtyckligt antal, säg $p$.]

Transformationsregeln för en tensor med två index (tänk matris) är

$$
\begin{equation}
T'_{ij} = L_{ik} L_{jl} T_{kl} \left( = L_{ik} T_{kl} \left( L^t \right)_{lj} \right),
\end{equation}
$$

vilket ju motsvarar $\mathbf{T}' = \mathbf{L} \mathbf{T} \mathbf{L}^t$.

En tensor med $p$ index (en tensor av rank $p$) skrivs $T_{i_1i_2\ldots i_p}$.  och allmänt

$$
\begin{equation}
T'_{i_1\ldots i_p}=L_{i_1j_1}\ldots L_{i_pj_p}T_{j_1\ldots j_p}.
\end{equation}
$$

Poängen med detta är att man kan multiplicera samman tensorer och
vara säker på att resultatet blir en tensor.

### Skalärprodukt

Är $\vec{u} \cdot \vec{v}$ en skalär? Hur beter den sig vid ett koordinatbyte?

$$
\begin{equation}
u_i' v_i' = L_{ij} u_j L_{ik} v_k = \delta_{jk} u_j v_k = u_k v_k
\end{equation}
$$

Resultatet är alltså oberoende av koordinatsystem, dvs det är en skalär.

### Produkt av tensorer

Produkten av två tensorer är också en tensor.
* $c_{ij}=a_i b_j$ är också en tensor.

* $u_i=M_{ij}v_j$ är också en tensor.

[Comment 8: I första fallet kan man "kontrahera" två index, såsom man bildar skalärprodukten.]

[Comment 9: För det andra fallet har vi $M'_{ij}v'_j=L_{ik}L_{jl}M_{kl}L_{jm}v_m=L_{ik}\delta_{lm}M_{kl}v_m=L_{ik}M_{kl}v_l
=L_{ik}u_k$ (där vi i första steget har använt att $\mathbf{L}$ är ortogonal). Om $M_{ij}$ och $v_i$ är tensorer är alltså även $u_i$ det. Det allmänna beviset går likadant (och innefattar förstås det faktum att skalärprodukten av två vektorer är en skalär).]

### Exempel: Kryssprodukt

Är $(\vec a\times\vec b)_i = \varepsilon_{ijk}a_jb_k$ en tensor? Dvs är resultatet en vektor?

[Comment 10: I linjär algebrakurserna har ni säkert visat att detta är en vektor.]

Räknereglerna ovan ger att vi bara måste visa att $\varepsilon_{ijk}$ är en tensor. Transformationsreglerna säger:

$$
\begin{equation}
\varepsilon'_{ijk}=L_{il}L_{jm}L_{kn}\varepsilon_{lmn}
\end{equation}
$$

Vi kommer att visa att $\varepsilon$ är helt invariant under koordinattransformationer. Men låt oss först visa att $\varepsilon'_{ijk}$ är antisymmetriskt. Byt plats på två index

$$
\begin{equation}
\varepsilon'_{jik}=L_{jl}L_{im}L_{kn}\varepsilon_{lmn} = L_{jm}L_{il}L_{kn}\varepsilon_{mln} = - L_{il} L_{jm}L_{kn}\varepsilon_{lmn} = - \varepsilon'_{ijk},
\end{equation}
$$

där vi först bytte namn på två summationsindex, och sedan bytte plats på dem och utnyttjade att $\varepsilon_{ijk}$ är antisymmetrisk.

Den bevisade antisymmetrin betyder ju också att element med två lika index måste vara noll. T.ex. $\varepsilon'_{iik} = -\varepsilon'_{iik}$.

$\varepsilon'_{ijk}$ är alltså proportionell mot $\varepsilon_{ijk}$. Visa därför en permutation, t.ex. $ijk=123$.

$$
\begin{equation}
\varepsilon'_{123}=L_{1l}L_{2m}L_{3n}\varepsilon_{lmn} = \det \mathbf{L} = +1,
\end{equation}
$$

för en högertransformation.

Så $\varepsilon'_{ijk}=\varepsilon_{ijk}$, Levi--Civita-tensorn är en *invariant tensor*. Den enda andra invarianta tensorn är Kroneckers delta. Visa själv att $\delta'_{ij}=\delta_{ij}$.

### Vektoroperatorn

Vi behöver också kunna derivera. Låt oss visa det litet triviala påståendet att gradienten
av en skalär är en vektor. Vi har $(\nabla\phi)'_i=\frac{\partial}{\partial x'_i}\phi'=\frac{\partial \phi}{\partial x_j} \frac{\partial x_j}{\partial x'_i}=
L_{ij} \frac{\partial \phi}{\partial x_j}=L_{ij}(\nabla\phi)_j$. Detta visar att $\partial _i$ är en vektoroperator, och man kan sedan använda den för att konstruera andra derivator (divergens, rotation osv.). Samma regler gäller för $\partial _i$ som för andra tensorer.

## Tröghetstensorn
Ett annat klassiskt exempel är tröghetstensorn,

$$
\begin{equation}
I_{ij}=\int_VdV\,\rho(r^2\delta_{ij}-x_ix_j),
\end{equation}
$$

som man räknade ut i stelkroppsdynamik. Den relaterar rörelsemängdsmomentet till
rotationsvektorn enligt $L_i=I_{ij}\omega_j$. I och med att den innehåller upprepade
kryssprodukter är det enklare att härleda den i tensorformalism.

Ett litet volymelement $dV$ av en stel kropp har massan $dm=\rho dV$,
och om kroppen roterar med en rotationsvektor $\omega_i$ har
det hastigheten $v_i=(\vec\omega\times\vec{r})_i=\varepsilon_{ijk}\omega_jx_k$.
Dess rörelsemängd är $\mbox{d}p_i=\mbox{d}mv_i=\mbox{d}m\varepsilon_{ijk}\omega_jx_k$. Bidraget till rörelsemängdsmomentet från volymelementet är

$$
\begin{equation}
\mbox{d}L_i = \varepsilon_{ijk}x_j \mbox{d}p_k = \mbox{d}m\varepsilon_{ijk}x_j\varepsilon_{klm}\omega_lx_m
=\mbox{d}m(\delta_{il}\delta_{jm}-\delta_{im}\delta_{jl})x_jx_m\omega_l \nonumber
\end{equation}
$$

$$
\begin{equation}
=\mbox{d}m(r^2\omega_i-x_ix_j\omega_j)=\mbox{d}V\rho(r^2\delta_{ij}-x_ix_j)\omega_j
\end{equation}
$$

och totalt blir detta

$$
\begin{equation}
L_i = \int_V \mbox{d}V\,\rho(r^2\delta_{ij}-x_ix_j) \omega_j = I_{ij} \omega_j,
\end{equation}
$$


# radian
> ### cos direction
>> ### $
\begin {array}{rr}
\frac{\sqrt{3}}{2} = (3 \times 2) & \iff & \cos \frac{\pi}{6}\\
\frac{\sqrt{2}}{2} = (2 \times 2) & \iff & \cos \frac{\pi}{4}\\
\frac{1}{2} = (1 + 2) & \iff & \cos \frac{\pi}{3}\\
\cos \frac{\pi}{4} = (4 \div 2) & \iff & \frac{\sqrt{2}}{2}\\
\cos \frac{\pi}{6} = (6 \div 2) & \iff & \frac{\sqrt{3}}{2}\\
\cos \frac{\pi}{3} = (3 \div 2) & \iff & \frac{1}{2}\\
\end{array}
$

```python
import IPython as IPy
IPy.display.IFrame('https://audioscenedescriptionformat.readthedocs.io/en/latest/rotation-matrices.html',width=640,height=480)
```

---
# [Azimuth(yaw)(heading)](https://audioscenedescriptionformat.readthedocs.io/en/latest/rotation-matrices.html)
- rotation around the z-axis

# Elevation(pitch)
- rotation around the x-axix

# Roll(bank)
- rotation around the y-axis

---
> ## $\color{red}{\text{Azimuth (yaw)}}$ angle $\alpha$
>> ### 0(zero) when pointing $\color{magenta}{north}$(i.e.(that is, in other word) along the positive y-axix)
>> ### rotating around the $\color{magenta}{z-axis}$ (which points up)
>> ### positive when rotating towards $\color{magenta}{west}$(right hand rule)

```python
import sympy as sm
# vector a
a_x, a_y, a_z = sm.symbols('a_x:z')

# cylindrical coordinates (r, phi, z)
r_z, phi_z = sm.symbols('r_z phi_z')

# vector a (r,phi,z) (r*cos(phi), r*sin(phi), z)
a = sm.Matrix([r_z*sm.cos(phi_z), r_z*sm.sin(phi_z), a_z])
```

```python
# let's rotate vector a by an azimuth angle \alpha with vector b
alpha = sm.symbols('alpha')
b = sm.Matrix([r_z*sm.cos(phi_z + alpha), r_z*sm.sin(phi_z + alpha),a_z])
# what T? s.t T * \vec{a} = \vec{b}
# 변환된 \vec{b}에서 일반화시킨 벡터 a의 계수를 뽑아내서 행열로 T로 만든다.
# 일차 선형 변환임으로 계수는 미분으로도 구 할 수 있다.
b
```

$\displaystyle \left[\begin{matrix}r_{z} \cos{\left(\alpha + \phi_{z} \right)}\\r_{z} \sin{\left(\alpha + \phi_{z} \right)}\\a_{z}\end{matrix}\right]$

```python
b.expand(trig=True)
```

$\displaystyle \left[\begin{matrix}- r_{z} \sin{\left(\alpha \right)} \sin{\left(\phi_{z} \right)} + r_{z} \cos{\left(\alpha \right)} \cos{\left(\phi_{z} \right)}\\r_{z} \sin{\left(\alpha \right)} \cos{\left(\phi_{z} \right)} + r_{z} \sin{\left(\phi_{z} \right)} \cos{\left(\alpha \right)}\\a_{z}\end{matrix}\right]$

```python
b = b.expand(trig=True).subs((list(zip(a,[a_x,a_y,a_z]))))
b
for i in zip(a,[a_x,a_y,a_z]):
    print(list(i))

list(zip(a,[a_x,a_y,a_z]))
```

    [r_z*cos(phi_z), a_x]
    [r_z*sin(phi_z), a_y]
    [a_z, a_z]

    [(r_z*cos(phi_z), a_x), (r_z*sin(phi_z), a_y), (a_z, a_z)]

```python
[[i.coeff(j) for j in [a_x,a_y,a_z]] for i in b]
Yaw = sm.Matrix([[i.coeff(j) for j in [a_x,a_y,a_z]] for i in b])
Yaw
```

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

```python
Azimuth = b.diff(a_x).row_join(b.diff(a_y)).row_join(b.diff(a_z))
Azimuth
```

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

```python
Yaw.subs(alpha,sm.pi/2)*sm.Matrix([0,1,0])
Azimuth.subs(alpha,sm.pi/4)*sm.Matrix([0,1,0])
```

$\displaystyle \left[\begin{matrix}- \frac{\sqrt{2}}{2}\\\frac{\sqrt{2}}{2}\\0\end{matrix}\right]$

> ## $\color{red}{\text{Elevation (pitch)}}$ angle $\beta$
>> ### $\color{magenta}{pitch}$
>>> ### 투수가 발을 높이 들고 볼을 던지려는 현상
>> ### 0(zero) in the $\color{magenta}{horizontal}$ plane.
>> ### rotating around the local $\color{magenta}{x-axis}$
>> ### positive when the $\color{magenta}{nose}$ goes up (right hand rule)

```python
import sympy as sm
beta = sm.symbols('beta')
x = sm.MatrixSymbol('x', 3,1)
k = sm.MatrixSymbol('k',3,1)
A = sm.MatrixSymbol('A',3,3)
i,j = sm.symbols('i j', positive=True)
A.as_explicit()
A.adjoint().as_explicit()
A.inv().as_explicit()
sm.Eq(A*x, k)

r, theta, z = sm.symbols('r theta z')
x_0, x_1, x_2 = sm.symbols('x_0:3')
k = sm.Matrix(k).subs(
    [(k[0],x_0),
     (k[1],r*sm.cos(theta0+theta)),
     (k[2],r*sm.sin(theta0+theta))])
k.expand(trig=True)
## A*x = k 에서 A를 구하려면, k 벡터 성분이 x벡터 성분에 어떤 행열곱 연산을 해주었나를 찾는것이다.
# K 성분에서 x성분의 계수가 무엇인가 찾아서 행열곱 순서를 생각해서 A를 만들어 간다.
# 1차식에서 독립변수의 게수를 구하는 것은 그 독립변수로 미분해주면된다. 즉 미분게수가 계수가 된다.

```

$\displaystyle \left[\begin{matrix}x_{0}\\- r \sin{\left(\theta \right)} \sin{\left(\theta_{0} \right)} + r \cos{\left(\theta \right)} \cos{\left(\theta_{0} \right)}\\r \sin{\left(\theta \right)} \cos{\left(\theta_{0} \right)} + r \sin{\left(\theta_{0} \right)} \cos{\left(\theta \right)}\end{matrix}\right]$

```python
x0 = sm.Matrix(x).subs([(x[0], r*sm.cos(theta0) ),(x[1], r*sm.sin(theta0)),(x[2], x_2)])
x0
```

$\displaystyle \left[\begin{matrix}r \cos{\left(\theta_{0} \right)}\\r \sin{\left(\theta_{0} \right)}\\x_{2}\end{matrix}\right]$

> ## $\color{red}{\text{Roll (bank)}}$ angle $\gamma$
>> ### $\color{magenta}{bank}$
>>> ### 은행
>> ### 0(zero) when the top of the object points to the $\color{magenta}{zenith}$(which is just the normal upright orientation)
>> ### rotaition around the local $\color{magenta}{y-axis}$
>> ### positive when the object is leaning towards $\color{magenta}{starbord}$(right hand rule .우현 )

```python
import sympy as sm
import sympy.vector
sm.algebras.quaternion.Quaternion(2,3,1,4)
```

$\displaystyle 2 + 3 i + 1 j + 4 k$

```python
theta,x,y,z = sm.symbols('theta x y z')
a0,a1,a2,a3,b0,b1,b2,b3 = sm.symbols('a_(0:4), b_(0:4)')
q1 = sm.algebras.quaternion.Quaternion(a0,a1,a2,a3)
q2 = sm.algebras.quaternion.Quaternion(b0,b1,b2,b3)
q1.to_rotation_matrix()

# rotation 90 wrt axis(1,1,1)
## (cos(90/2) + sin(90/2))*(\vec{v})*(cos(90/2)-sin(90/2))
```

$\displaystyle \left[\begin{matrix}- \frac{2 \left(a_{2}^{2} + a_{3}^{2}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} + 1 & \frac{2 \left(- a_{0} a_{3} + a_{1} a_{2}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} & \frac{2 \left(a_{0} a_{2} + a_{1} a_{3}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}}\\\frac{2 \left(a_{0} a_{3} + a_{1} a_{2}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} & - \frac{2 \left(a_{1}^{2} + a_{3}^{2}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} + 1 & \frac{2 \left(- a_{0} a_{1} + a_{2} a_{3}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}}\\\frac{2 \left(- a_{0} a_{2} + a_{1} a_{3}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} & \frac{2 \left(a_{0} a_{1} + a_{2} a_{3}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} & - \frac{2 \left(a_{1}^{2} + a_{2}^{2}\right)}{a_{0}^{2} + a_{1}^{2} + a_{2}^{2} + a_{3}^{2}} + 1\end{matrix}\right]$

```python
# rotation around a axis
```

```python
# [cos(theta/2)]^2 + [(sin(\theta/2)i + sin(\theta/2)j + sin(\theta/2)k)]^2
q1.from_axis_angle((1,0,0),sm.pi)
```

$\displaystyle 0 + 1 i + 0 j + 0 k$

```python
q1.from_axis_angle((0,1,0),sm.pi/2)
```

$\displaystyle \frac{\sqrt{2}}{2} + 0 i + \frac{\sqrt{2}}{2} j + 0 k$

```python
q1.from_axis_angle((0,0,1),2*sm.pi/6)
```

$\displaystyle \frac{\sqrt{3}}{2} + 0 i + 0 j + \frac{1}{2} k$

```python
q1.from_axis_angle((1,0,0),2*sm.pi/3)
```

$\displaystyle \frac{1}{2} + \frac{\sqrt{3}}{2} i + 0 j + 0 k$

```python
# 6/16 = 3/8 + 3/8 = 3/4
q1.from_axis_angle((1,1,0),2*sm.pi/3)
```

$\displaystyle \frac{1}{2} + \frac{\sqrt{6}}{4} i + \frac{\sqrt{6}}{4} j + 0 k$

```python
# (1/2)^2 + (1/2)^2 + (1/2)^2 = 3/4 = \sqrt{3}/2
q1.from_axis_angle((1,1,1),2*sm.pi/3)
```

$\displaystyle \frac{1}{2} + \frac{1}{2} i + \frac{1}{2} j + \frac{1}{2} k$

```python

```

```python

```

```python
sm.sqrt((sm.sqrt(3)/6)**2 + (sm.sqrt(3)/6)**2 + (sm.sqrt(3)/3)**2)
(sm.sqrt(2)/2)/(sm.sqrt(7)/14)
sm.cos(sm.pi/6)
```

$\displaystyle \frac{\sqrt{3}}{2}$

```python
(sm.sqrt(3)/3)/(sm.sqrt(3)/6)
```

$\displaystyle 2$

```python
q1.from_axis_angle((1,1,1),sm.pi/2)
```

$\displaystyle \frac{\sqrt{2}}{2} + \frac{\sqrt{6}}{6} i + \frac{\sqrt{6}}{6} j + \frac{\sqrt{6}}{6} k$

```python
N = sm.vector.CoordSys3D('N')
r,i,j,k = sm.symbols('r i j k')
Q = N.orient_new_quaternion('Q',r,i,j,k)
Q.base_vectors()
Q.base_scalars()
```

    (Q.x, Q.y, Q.z)

```python
sm.cos(sm.pi/4)
```

$\displaystyle \frac{\sqrt{2}}{2}$

```python

```


# Hamiltonian Tomography

## Contents
* [Introduction](#introduction)
* [Fitting Functions](#fitting-functions)
* [Using the Pulse Simulator](#using-the-pulse-simulator)
* [Run Experiment on Backend Model Simulation](#run-experiment-model)
* [Fitting the Simulated Results](#fitting-the-simulated-results)
* [Measure ZI (Stark Shift) via CR Ramsey Experiment](#measure-zi-via-ramsey)
* [References](#references)

## Introduction

The cross resonance gate for entangling qubits was introduced in [this section](https://qiskit.org/textbook/ch-quantum-hardware/cQED-JC-SW.html#6.-The-Cross-Resonance-Entangling-Gate-) of the Qiskit Textbook, where it is assumed the transmon is a qubit and the Schrieffer-Wolff transformation is applied to yield an effective Hamiltonian

$$
\tilde{H}_{\rm eff}^{\rm CR} = - \frac{\Delta_{12}}{2}\sigma_1^z
+ \frac{\Omega(t)}{2} \left(\sigma_2^x - \frac{J}{2\Delta_{12}} \sigma_1^z \sigma_2^x \right),
$$

where $\Delta_{12} = \tilde{\omega}_1-\tilde{\omega}_2$ is the difference between the dressed frequencies of the qubits, $\Omega$ is the cross resonance drive strength, and $J$ is the qubit-qubit coupling. We will use a common simplified notation for these interaction terms where the Pauli matrix is represented by a capital letter (with a hat to denote that it is an operator) and the qubit is represented by its position in a string, so for example we wish to isolate the $\hat{Z}\hat{X} = \hat{Z} \otimes \hat{X} = \sigma_1^z \otimes \sigma_2^x = \sigma_1^z \sigma_2^x$ interaction that is used to build the controlled-NOT gate from the $\hat{Z}\hat{I} = \sigma_1^z \otimes \sigma_2^0$ and $\hat{I}\hat{X} = \sigma_1^0 \otimes \sigma_2^x$ terms. Here the matrix $\sigma_i^0$ represents the identity matrix on qubit $i$.

In addition to understanding these extra terms, since the [transmon](https://qiskit.org/textbook/ch-quantum-hardware/transmon-physics.html#4.-The-Quantized-Transmon-) has higher energy levels and actual experiments may have other interactions, due to crosstalk for example, when applying an entangling operation, it is not always obvious which Pauli rotations will be generated. Here we assume a cross resonance Hamiltonian of the [following form](https://arxiv.org/abs/1603.04821):

$$
\hat{H} = \frac{\hat{Z} \otimes \hat{A}}{2} + \frac{\hat{I} \otimes \hat{B}}{2} =
a_{x} \hat{Z}\hat{X} + a_{y} \hat{Z}\hat{Y} + a_{z} \hat{Z}\hat{Z} + b_{x}  \hat{I}\hat{X} + b_{y} \hat{I}\hat{Y} + b_{z} \hat{I}\hat{Z}
$$

where we will omit the Kronecker product symbol $\otimes$ for succinctness.  We refer to the first Pauli operator acting on the control qubit and second operators acting on the target qubits, as in the effective Hamiltonian above.  While the form of the cross resonance Hamiltonian is known, the individual coefficients $a_{\mu}, b_{\nu}$ are not known. Note that these coefficients are also referred to as the strengths of the interaction they correspond to, i.e. $a_x$ is the $ZX$ interaction rate, etc. We must then find a way of extracting the coefficients from measurements made on the system after the cross resonance pulse is applied for different durations. Before we proceed, it should be noted that cross resonance operation also generates a $\hat{Z}\hat{I}$ interaction arising from a Stark shift (off-resonant drive that dressed the qubit frequency). This term can be extracted by preforming a [Ramsey experiment](https://qiskit.org/textbook/ch-quantum-hardware/calibrating-qubits-pulse.html#4.1-Measuring-the-qubit-Frequency-Precisely-using-a-Ramsey-Experiment-) on the control qubit. We will discuss this Ramsey procedure later on, so for know let us focus on the Hamiltonian that we wrote down.

The coefficients $a_{\mu}, b_{\nu}$ (interaction rates) will be extracted by taking six different measurements as a function of the duration of the pulse.  The six measurements are of the expectation value of each Pauli term on the target qubit with the control qubit either in the ground or excited state. In the next section we will show how these measurements give us information about the coefficients.

## Fitting Functions

In order to extract the coefficients $a_{\mu}, b_{\nu}$, we need to know what function we expect to fit to the measurement data.  The data we will be looking at will be the expectation value of Pauli operators as a function of pulse duration.  In the Heisenberg picture of quantum mechanics, the evolution of the expectation value of an operator can be given as

$$\langle \hat{O}(t) \rangle = \langle e^{i\hat{H}t} \hat{O} e^{-i\hat{H}t} \rangle$$

Let $dt$ be an infinitesimally small time increment.  Then we have

$$\langle \hat{O}(t+dt) \rangle = \langle (1+i\hat{H} dt)\hat{O}(t)(1-i\hat{H} dt) \rangle =
\langle \hat{O}(t) \rangle +i dt \langle \left[\hat{H},\hat{O}\right] \rangle
\Longrightarrow \frac{d\langle \hat{O} \rangle}{dt} = i \left[\hat{H},\hat{O}\right]$$

to first order in $dt$. We can evaluate the commutator for each of the Pauli operators:

\begin{equation}
\begin{split}
&\left[\hat{H}, \hat{I}\hat{X}\right] = 2 i \left(a_{y} \hat{Z}\hat{Z} - a_{z} \hat{Z}\hat{Y} + b_{y} \hat{I}\hat{Z} - b_{z} \hat{I}\hat{Y}\right) \\
&\left[\hat{H},\hat{I}\hat{Y}\right] = 2 i \left(-a_{x} \hat{Z}\hat{Z} + a_{z} \hat{Z}\hat{X} - b_{x} \hat{I}\hat{Z} + b_{z} \hat{I}\hat{X}\right)\\
&\left[\hat{H}, \hat{I}\hat{Z}\right] = 2 i \left(a_{x} \hat{Z}\hat{Y} - a_{y} \hat{Z}\hat{X} + b_{x} \hat{I}\hat{Y} - b_{y} \hat{I}\hat{X}\right)
\end{split}
\end{equation}

If we let $n$ be the expectation value of the Pauli $\hat{Z}$ operator on the control qubit, then we can write down these commutators in terms of the expectation values of the target qubit:

\begin{equation}
\begin{split}
&i\langle\left[\hat{H},\hat{I}\hat{X} \right]\rangle_{\rm control} = 2 \left(n a_{z} + b_{z}\right)\langle\hat{Y}\rangle - 2 \left(n a_{y} + b_{y}\right)\langle\hat{Z}\rangle
\\
&i\langle\left[\hat{H}, \hat{I}\hat{Y}\right]\rangle_{\rm control} = 2\left(n a_{x} + b_{x}\right) \langle\hat{Z}\rangle-2  \left(n a_{z} + b_{z}\right) \langle\hat{X}\rangle
\\
&i\langle\left[\hat{H}, \hat{I}\hat{Z}\right]\rangle_{\rm control} = 2  \left(n a_{y} + b_{y}\right) \langle\hat{X}\rangle - 2 \left(n a_{x} + b_{x}\right) \langle\hat{Y}\rangle
\end{split}
\end{equation}

where the expectation values on the right hand side are understood to be those of the target qubit, which will also be the case of the following discussion. Let us define $\vec{r}_n = \{\langle\hat{X}\rangle, \langle\hat{Y}\rangle, \langle\hat{Z}\rangle\}_n$ then we can use these commutators to write down a matrix equation for the time dependence of $\vec{r}$ depending on the Pauli-$Z$ value $n$ of the state of the control qubit. Then putting the above equations together

$$ \frac{d}{dt} \begin{bmatrix}
\langle \hat{X} \rangle \\ \langle \hat{Y} \rangle \\ \langle \hat{Z} \rangle \end{bmatrix} =
2 \begin{bmatrix} 0 & na_z + b_z & -n a_y - b_y \\
-na_z - b_z & 0 & n a_x + b_x \\
na_y + b_y & -na_x - b_x & 0 \end{bmatrix} \begin{bmatrix}
\langle \hat{X} \rangle \\ \langle \hat{Y} \rangle \\ \langle \hat{Z} \rangle \end{bmatrix}
$$

or more compactly,

$$\frac{d\vec{r}_n(t)}{dt} = G_n \vec{r}_n(t),$$

where

$$
G_n = 2 \begin{bmatrix} 0 & na_z + b_z & -n a_y - b_y \\
-na_z - b_z & 0 & n a_x + b_x \\
na_y + b_y & -na_x - b_x & 0 \end{bmatrix} \equiv \begin{bmatrix}0 & \Delta^n & -\Omega_y^n\\-\Delta^n & 0 & \Omega_x^n \\ \Omega_y^n & -\Omega_x^n & 0\end{bmatrix}.
$$

Since $G_n$ is time independent we can easily integrate the differential equation with the initial state corresponding to $t=0$, yielding

$$\vec{r}_n(t) = e^{G_n t} \vec{r}_n(0).$$

We can find the matrix exponential, $e^{G_n t}$, by finding the eigenvalues and eigenvectors of $G_n$.  The eigenvalues of $G_n$ are

$$
\vec{g}_{n} = \{0, -i\sqrt{\Delta^2+\Omega_x^2+\Omega_y^2} , i\sqrt{\Delta^2+\Omega_x^2+\Omega_y^2}\}_n,
$$

where for notational simplicity, the subscript $n$ denotes the appropriate values of $\Delta, \Omega_x, \Omega_y,$ given the state of the control qubit. We will not write down the eigenvectors because they are too cumbersome but it is straightforward to find them.  Let $U$ be the transformation into the eigenbasis and let $\hat{D}_n$ be the diagonal matrix of the eigenvalues.  Then we can rewrite the time dependence of $\vec{r}_n(t)$ as

$$\vec{r}_n(t) = U^{\dagger} e^{\hat{D}_n t} U\vec{r}_n(0).$$

Setting ${\vec{r}_n(0) = \{0,0,1}\}$ (which corresponds the target qubit starting in the $|0\rangle$ state) we have that

\begin{equation}
\begin{split}
&\langle \hat{X}(t) \rangle_n = \frac{1}{\Omega^2}\left(-\Delta \Omega_x + \Delta\Omega_x\cos(\Omega t) + \Omega \Omega_y \sin(\Omega t)\right)
\\
&\langle \hat{Y}(t) \rangle_n = \frac{1}{\Omega^2}\left(\Delta \Omega_y - \Delta\Omega_y\cos(\Omega t) - \Omega \Omega_x \sin(\Omega t)\right)
\\
&\langle \hat{Z}(t) \rangle_n = \frac{1}{\Omega^2}\left(\Delta^2 + \left(\Omega_x^2+\Omega_y^2\right)\cos(\Omega t) \right)
\end{split}
\end{equation}

where $\Omega = \sqrt{\Delta^2+\Omega_x^2+\Omega_y^2}$ for each control qubit preparation $n$. In the subsequent sections, we will often drop the hat (^) on the operators.

## Using the Pulse Simulator

We will simulate on a model of a real device using the Pulse Simulator. First, load necessary libraries.

```python
from qiskit import pulse
from qiskit.pulse import Play, Schedule, DriveChannel, ControlChannel, Waveform
from qiskit.pulse.library import drag, GaussianSquare
from qiskit.visualization import SchedStyle
from qiskit.test.mock import FakeAthens
style = SchedStyle(figsize=(20, 10))
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('dark_background')
plt.rcParams['figure.figsize'] = [15, 15]
backend = FakeAthens()
```

Next, save the fake backend configuration and the sampling time $dt$. We will save the Hamiltonian parameters here for building a Duffing oscillator model later.

```python
backend_config = backend.configuration()
ham_params = backend_config.hamiltonian['vars']
dt = backend_config.dt
print(f"Sampling time: {dt*1e9} ns")
```

    Sampling time: 0.2222222222222222 ns

The `instruction_schedule_map` provides the parameters of the default calibrated pulses used by the backend. In particular, we see that this "native gate set" consists of a subset of operations you can perform on a [`QuantumCircuit`](https://qiskit.org/documentation/apidoc/circuit.html#gates-and-instructions), and is the gate set that the `QuantumCircuit` is transpiled to by Qiskit.

```python
backend_defaults = backend.defaults()
inst_sched_map = backend_defaults.instruction_schedule_map
inst_sched_map.instructions
```

    ['cx', 'id', 'measure', 'u1', 'u2', 'u3', 'x']

Here we see the single-qubit ["U-gates"](https://qiskit.org/textbook/ch-states/single-qubit-gates.html#generalU3) `u1`, `u2`, and `u3`, controlled-NOT `cx`, identity `id`, measurement `measure` and $\pi$-pulse `x` for echoes. In particular, this is useful for retrieving pulses from the native gate set and manipulating them. For example, instead of building measurement pulses from scratch, we can retrieve it from the `instruction_schedule_map`.

```python
meas = inst_sched_map.get('measure', qubits=range(backend_config.n_qubits))
meas.exclude(channels=[pulse.AcquireChannel(i)
                        for i in range(backend_config.n_qubits)]).draw(style=style)
```

For Hamiltonian tomography, we will also need the cross resonance pulse, which we can extract from the controlled-NOT.

```python
def cx_pulse_instructions(qc: int, qt: int) -> Schedule:
    """Retrieve the CNOT pulse schedule for the given
    qubit pair from the backend defaults.

    Args:
      qc: control qubit index
      qt: target qubit index
    """
    if [qc, qt] not in backend_config.coupling_map:
        print('Qubit pair has no direct cross resonance!')
    else:
        cx = inst_sched_map.get('cx', qubits=[qc, qt])
        return cx

def get_cr_chan_inst(qc: int, qt: int) -> (ControlChannel, Waveform):
    """Retrieve the cross resonance pulse channel and waveform
    for the qubit pair from the cx (CNOT) schedule.

    Args:
      qc: control qubit index
      qt: target qubit index
    """
    cx = cx_pulse_instructions(qc, qt)
    idx = 0
    # look for first Play instruction on a ControlChannel
    while (type(cx.instructions[idx][1].channels[0]) is not ControlChannel) or \
        (type(cx.instructions[idx][1]) is not Play):
        idx += 1
    return (cx.instructions[idx][1].channels[0], cx.instructions[idx][1].pulse)
```

Retrieve the cross resonance waveform and the control channel (`uchan`) it is played on.

```python
uchan, cr_pulse = get_cr_chan_inst(1, 0)
```

Cross resonance pulses are of type [`GaussianSquare`](https://qiskit.org/documentation/stubs/qiskit.pulse.library.GaussianSquare.html), a square pulse with a Gaussian rise and fall. Currently the waveform samples are returned from the `instruction_schedule_map`, so we must elucidate the `GaussianSquare` parameters so we can easily build our own cross resonance pulses. In particular, we wish to find the amplitude because it is typically calibrated to be the largest amplitude that does not result in leakage to the higher order so that the shortest gate length can be achieved.

```python
# middle sample will contain the amplitude
cr_amp = cr_pulse.samples[len(cr_pulse.samples)//2]
cr_dur = cr_pulse.duration
cr_risefall = 128 # guess
cr_width = cr_dur - 2*cr_risefall
my_cr_pulse = GaussianSquare(duration=cr_dur, amp=cr_amp, sigma=cr_risefall//2, width=cr_width)
```

Let's build a test schedule to visualize the default cross resonance pulse against the one we just constructed.

```python
with pulse.build(name="Compare CR Pulses") as cr_test_sched:
    pulse.play(cr_pulse, ControlChannel(0))
    pulse.play(my_cr_pulse, ControlChannel(1))

cr_test_sched.draw(style=style)
```

Pretty good! This will be close enough for the Hamiltonian tomography experiment. Now, this pulse nominally executes $ZX(\theta=\pi/4)$ corresponding to the [RZXGate](https://qiskit.org/documentation/stubs/qiskit.circuit.library.RZXGate.html) because the cross resonance pulse is echoed: the first half will be a positive rotation dependent on the state of the control qubit, followed by an "echo pulse" that flips the control qubit, followed by a negative rotation dependent on the new state of the control qubit. This turns out to be equivalent to a $ZX(\theta=\pi/2)$, but we are just dealing with the first part of the pulse so that we can observe the full effect of the cross resonance interaction. We keep this in mind because this particular `cr_dur` will only take us an angle of $\theta=\pi/4$ around the Bloch sphere, and for the Hamiltonian tomography experiment we wish to traverse the Bloch sphere several times.

```python
def build_cr_pulse(qc: int, qt: int, width: int, control='0',
                   basis='Z', phase=0.0, framechange=0.0) -> Schedule:
    """Build a cross resonance pulse schedule for a given set of parameters.

    Args:
      qc: control qubit index
      qt: target qubit index
      width: width of cross resonance pulse (in dt)
      control: control qubit prepared as '0' or '1'
      basis: measurement basis of target qubit
      phase: phase offset of cross resonance pulse (rad)
      framechange: compensated Z-rotation on control performed in software (rad)
    """
    with pulse.build() as sched:
        with pulse.align_sequential():
            if control == '1':
                Xp = inst_sched_map.get('x', qubits=[qc])
                pulse.call(Xp) # flip control from |0> to |1>

            duration = width + 2*cr_risefall
            with pulse.phase_offset(phase, uchan):
                pulse.play(GaussianSquare(duration=duration, amp=cr_amp, sigma=cr_risefall//2, width=width), uchan)
            pulse.shift_phase(framechange, DriveChannel(qc))

            # rotate target for appropriate Pauli measurement
            if basis == 'X':
                Y90m = inst_sched_map.get('u2', P0=np.pi, P1=np.pi, qubits=[qt])
                pulse.call(Y90m)
            elif basis == 'Y':
                X90p = inst_sched_map.get('u2', P0=-np.pi/2, P1=np.pi/2, qubits=[qt])
                pulse.call(X90p)

            return sched

def build_cr_scheds(qc: int, qt: int, cr_times, phase=0.0, ZI_MHz=0.0) -> np.array:
    """Build an array of cross resonance schedules for the
    Hamiltonian tomography experiment.

    Args:
      qc: control qubit index
      qt: target qubit index
      cr_times: array of widths of the cross resonance pulses
      phase: phase offset of the cross resonance pulse (rad)
      ZI_MHz: ZI interaction rate (in MHz) to correct for with frame change
    """
    scheds = []
    for width in cr_times:
        framechange = 2*np.pi*int(width)*dt*ZI_MHz*1e6
        for basis in ['X', 'Y', 'Z']:
            for control in ['0', '1']:
                with pulse.build(name="CR width %i" %width) as sched:
                    with pulse.align_sequential():
                        pulse.call(build_cr_pulse(qc, qt, int(width), control=control, \
                                                  basis=basis, phase=phase, framechange=framechange))
                        pulse.call(meas)
                scheds.append(sched)
    return scheds
```

```python
qt = 0
qc = 1

# remember samples must be in multiples of 16
cr_times = 16*np.linspace(0, 500, 21)
cr_scheds = build_cr_scheds(qc, qt, cr_times)
cr_scheds[-1].exclude(channels=[pulse.AcquireChannel(i)
                        for i in range(backend_config.n_qubits)]).draw(style=style)
```

Note how the final schedule consists of the control in the $|1\rangle$ state due to the $\pi$-pulse on `d1` channel before the cross resonance pulse and this is measured in the $Z$-basis because there is no subsequent pulse on the `d0` channel.

## Run Experiment on Backend Model Simulation

We will construct a [Duffing oscillator](https://qiskit.org/textbook/ch-quantum-hardware/transmon-physics.html#4.-The-Quantized-Transmon-) model based on the Hamiltonian model of `ibmq_athens`. Then we collect the relevant Hamiltonian parameters from the `params` extracted from the backend configuration and create the model from them.

```python
from qiskit.providers.aer.pulse import duffing_system_model

dim_oscillators = 3 # cutoff Hilbert space dimension per transmon

SF = 1/(2*np.pi) # scale factor to convert from angular frequency to Hz

oscillator_freqs = [] # qubit transition frequencies
anharm_freqs = [] # qubit anharmonicities
drive_strengths = [] # drive Rabi strength
coupling_dict = {} # coupling strength between qubit pairs

for key in ham_params:
    if 'wq' in key:
        oscillator_freqs.append(ham_params[key]*SF)
    elif 'delta' in key:
        anharm_freqs.append(ham_params[key]*SF)
    elif 'omegad' in key:
        drive_strengths.append(ham_params[key]*SF)
    elif 'jq' in key:
        _, q0str, q1str = key.split('q')
        q0idx = int(q0str)
        q1idx = int(q1str)
        coupling_dict[(q0idx, q1idx)] = ham_params[key]*SF

# create the model
backend_model = duffing_system_model(dim_oscillators=dim_oscillators,
                                       oscillator_freqs=oscillator_freqs,
                                       anharm_freqs=anharm_freqs,
                                       drive_strengths=drive_strengths,
                                       coupling_dict=coupling_dict,
                                       dt=dt)
```

```python
from qiskit import assemble
from qiskit.providers.aer import PulseSimulator
backend_sim = PulseSimulator()
qubit_lo_freq = backend_model.hamiltonian.get_qubit_lo_from_drift()
```

```python
def run_pulse(sched):
    """Runs the scheduled experiment on the simulated backend.

    Args:
      sched: pulse schedule to run
    """
    # assemble the qobj
    test_qobj = assemble(sched,
                        backend=backend_sim,
                        qubit_lo_freq=qubit_lo_freq,
                        meas_level=1,
                        meas_return='avg',
                        shots=5012)

    # run simulation
    sim_result = backend_sim.run(test_qobj, system_model=backend_model).result()

    return sim_result.get_memory(0)

def run_ham_tomo(cr_times, cr_scheds):
    """Run Hamiltonian tomography experiment and return results.

    Args:
      cr_times: widths of cross resonance pulses
      cr_scheds: array of pulse schedules for Ham Tomo experiment
    """
    # expectation values of target conditioned on control
    avg_t_c = np.zeros((6, len(cr_times)), dtype=complex)

    # sanity check: expectation values of control conditioned on control
    avg_c_c = np.zeros((6, len(cr_times)), dtype=complex)

    for ii in range(len(cr_scheds)):
        #if ii % 6 == 0:
        #    print(str(int(ii//6))+': Performing simulations for CR tone width of '+str(int(cr_times[ii//6]*dt*1e9))+' ns!')
        result = run_pulse(cr_scheds[ii])
        avg_t_c[ii % 6, ii // 6] = 1 - 2*result[qt]
        avg_c_c[ii % 6, ii // 6] = result[qc]

    return np.real(avg_t_c), np.real(avg_c_c)
```

#### **Warning!** The Pulse Simulator is computationally intensive and each experiment consisting of runs of 21 schedules and 5012 shots may take tens of minutes up to hours depending on CPU performance. The schedules with longer cross resonance pulses are more computationally intensive than those with shorter ones.

```python
avg_t_c, avg_c_c = run_ham_tomo(cr_times, cr_scheds)
```

## Fitting the Simulated Results

Using the `scipy` package, the fitting functions below will fit the Hamiltonian tomography data, Pauli expectations of the target qubit $\langle X(t) \rangle, \langle Y(t) \rangle, \langle Z(t) \rangle$, for the control prepared in either the ground or excited state. Note that we must use a trick to concatenate all the data into a single array by `tile`ing the time array and `vstack`ing the data so we can use the `curve_fit` function.

```python
from scipy.optimize import curve_fit

def get_omega(eDelta, eOmega_x, eOmega_y):
    """Return \Omega from parameter arguments."""
    eOmega = np.sqrt(eDelta**2 + eOmega_x**2 + eOmega_y**2)
    return eOmega

def avg_X(t, eDelta, eOmega_x, eOmega_y):
    """Return average X Pauli measurement vs time t"""
    eOmega = get_omega(eDelta, eOmega_x, eOmega_y)
    eXt = (-eDelta*eOmega_x + eDelta*eOmega_x*np.cos(eOmega*t) + \
           eOmega*eOmega_y*np.sin(eOmega*t)) / eOmega**2
    return eXt

def avg_Y(t, eDelta, eOmega_x, eOmega_y):
    """Return average Y Pauli measurement vs time t"""
    eOmega = get_omega(eDelta, eOmega_x, eOmega_y)
    eYt = (eDelta*eOmega_y - eDelta*eOmega_y*np.cos(eOmega*t) - \
           eOmega*eOmega_x*np.sin(eOmega*t)) / eOmega**2
    return eYt

def avg_Z(t, eDelta, eOmega_x, eOmega_y):
    """Return average Z Pauli measurement vs time t"""
    eOmega = get_omega(eDelta, eOmega_x, eOmega_y)
    eZt = (eDelta**2 + (eOmega_x**2 + eOmega_y**2)*np.cos(eOmega*t)) / eOmega**2
    return eZt

def rt_evol(ts, eDelta, eOmega_x, eOmega_y):
    """Stack average X,Y,Z Pauli measurements vertically."""
    return np.vstack([avg_X(ts, eDelta, eOmega_x, eOmega_y), \
                     avg_Y(ts, eDelta, eOmega_x, eOmega_y), \
                     avg_Z(ts, eDelta, eOmega_x, eOmega_y)])

def rt_flat(ts, eDelta, eOmega_x, eOmega_y):
    """Flatten X,Y,Z Pauli measurement data into 1D array."""
    return rt_evol(ts[0:len(ts)//3], eDelta, eOmega_x, eOmega_y).flatten()

def fit_rt_evol(ts, eXt, eYt, eZt, p0):
    """Use curve_fit to determine fit parameters of X,Y,Z Pauli measurements together."""
    rt_vec = np.asarray([eXt, eYt, eZt])
    return curve_fit(rt_flat, np.tile(ts, 3), rt_vec.flatten(), p0=p0)
```

### Plotting Functions

The above fits provide the parameters $\Omega^i_x, \Omega^i_y$, and $\Delta^i$ for the control qubit prepared in states $i = |0\rangle, |1\rangle$ (corresponding to $n=\pm 1$ in the equations above). The interaction rates (coefficients $a_\mu, b_\nu$ of the operators) are then determined as

$$
IX = \frac{1}{2} \left( \Omega^0_x + \Omega^1_x\right) \qquad ZX = \frac{1}{2} \left( \Omega^0_x - \Omega^1_x\right) \\
IY = \frac{1}{2} \left( \Omega^0_y + \Omega^1_y\right) \qquad ZY = \frac{1}{2} \left( \Omega^0_y - \Omega^1_y\right) \\
IZ = \frac{1}{2} \left( \Delta^0 + \Delta^1\right) \qquad ZZ = \frac{1}{2} \left( \Delta^0 - \Delta^1\right)
$$

```python
def get_interation_rates_MHz(ground_fit, excited_fit):
    """Determine interaction rates from fits to ground and excited control qubit data."""
    Delta0 = (ground_fit[0]/dt)/1e6
    Omega0_x = (ground_fit[1]/dt)/1e6
    Omega0_y = (ground_fit[2]/dt)/1e6
    Delta1 = (excited_fit[0]/dt)/1e6
    Omega1_x = (excited_fit[1]/dt)/1e6
    Omega1_y = (excited_fit[2]/dt)/1e6

    IX = 0.5*(Omega0_x + Omega1_x)
    IY = 0.5*(Omega0_y + Omega1_y)
    IZ = 0.5*(Delta0 + Delta1)
    ZX = 0.5*(Omega0_x - Omega1_x)
    ZY = 0.5*(Omega0_y - Omega1_y)
    ZZ = 0.5*(Delta0 - Delta1)

    return [[IX, IY, IZ], [ZX, ZY, ZZ]]

def plot_cr_ham_tomo(cr_times, avg_t_c, avg_c_c, ground_fit, excited_fit):
    """Plot Hamiltonian tomography data and curve fits with interaction rates."""
    coeffs = get_interation_rates_MHz(ground_fit, excited_fit)
    fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(15,15))
    ax1.scatter(cr_times, avg_t_c[0,:].real, lw=3.0, color='blue', label='ctrl in |0>')
    ax1.plot(cr_times, avg_X(cr_times, *ground_fit), lw=3.0, color='blue')
    ax1.scatter(cr_times, avg_t_c[1,:].real, lw=3.0, color='red', label='ctrl in |1>')
    ax1.plot(cr_times, avg_X(cr_times, *excited_fit), lw=3.0, color='red')
    ax1.set_ylabel('<X(t)>', fontsize=20)
    ax1.set_title('Pauli Expectation Value', fontsize=20)
    ax1.legend(loc=4, fontsize=14)
    ax2.scatter(cr_times, avg_t_c[2,:].real, lw=3.0, color='blue', label='ctrl in |0>')
    ax2.plot(cr_times, avg_Y(cr_times, *ground_fit), lw=3.0, color='blue')
    ax2.scatter(cr_times, avg_t_c[3,:].real, lw=3.0, color='red', label='ctrl in |1>')
    ax2.plot(cr_times, avg_Y(cr_times, *excited_fit), lw=3.0, color='red')
    ax2.set_title('IX = %.3f MHz   IY = %.3f MHz   IZ = %.3f MHz' % \
              (coeffs[0][0], coeffs[0][1], coeffs[0][2]), fontsize=20)
    ax2.set_ylabel('<Y(t)>', fontsize=20)
    ax2.legend(loc=4, fontsize=14)
    ax3.scatter(cr_times, avg_t_c[4,:].real, lw=3.0, color='blue', label='ctrl in |0>')
    ax3.plot(cr_times, avg_Z(cr_times, *ground_fit), lw=3.0, color='blue')
    ax3.scatter(cr_times, avg_t_c[5,:].real, lw=3.0, color='red', label='ctrl in |1>')
    ax3.plot(cr_times, avg_Z(cr_times, *excited_fit), lw=3.0, color='red')
    ax3.set_title('ZX = %.3f MHz   ZY = %.3f MHz   ZZ = %.3f MHz' % \
              (coeffs[1][0], coeffs[1][1], coeffs[1][2]), fontsize=20)
    ax3.set_ylabel('<Z(t)>', fontsize=20)
    ax3.set_xlabel('time (dt)', fontsize=20)
    ax3.legend(loc=4, fontsize=14)
```

### Fit and Plot

```python
ground_fit,_ = fit_rt_evol(cr_times, avg_t_c[0,:], avg_t_c[2,:], avg_t_c[4,:], p0=[0.0002, 0.0002, 0.0005])
excited_fit,_ = fit_rt_evol(cr_times, avg_t_c[1,:], avg_t_c[3,:], avg_t_c[5,:], p0=[0.0002, 0.001, 0.001])

plot_cr_ham_tomo(cr_times, avg_t_c, avg_c_c, ground_fit, excited_fit)
```

Note here that the magnitude of the $ZY$ interaction is a lot larger than the desired $ZX$ interaction. This is because the cross resonance pulse is out of phase with the single-qubit drive $IX$ on the target qubit (not the $IX$ here induced by the cross resonance pulse). We can determine this from the interaction rates and shift the phase of the cross resonance pulse in the next Hamiltonian tomography experiment.

```python
coeffs = get_interation_rates_MHz(ground_fit, excited_fit)
ZX_rate = coeffs[1][0]
ZY_rate = coeffs[1][1]
phase = -np.arctan2(ZY_rate, ZX_rate)
print('Phase from ZY/ZX ratio is '+str(phase))
```

    Phase from ZY/ZX ratio is -1.5398023429098322

```python
cr_scheds = build_cr_scheds(qc, qt, cr_times, phase=phase)
avg_t_c, avg_c_c = run_ham_tomo(cr_times, cr_scheds)
```

```python
ground_fit,_ = fit_rt_evol(cr_times, avg_t_c[0,:], avg_t_c[2,:], avg_t_c[4,:], p0=[0.0002, 0.0002, 0.0005])
excited_fit,_ = fit_rt_evol(cr_times, avg_t_c[1,:], avg_t_c[3,:], avg_t_c[5,:], p0=[0.0002, 0.001, 0.001])

plot_cr_ham_tomo(cr_times, avg_t_c, avg_c_c, ground_fit, excited_fit)
```

Now we can see that the bulk of the cross resonance pulse provides the $ZX$-interaction that we can use to entangle qubits.

## Measure ZI (Stark Shift) via CR Ramsey Experiment

Here we measure the $ZI$ interaction term via a [Ramsey experiment](https://qiskit.org/textbook/ch-quantum-hardware/calibrating-qubits-pulse.html#4.1-Measuring-the-qubit-Frequency-Precisely-using-a-Ramsey-Experiment-), recalling that the resulting oscillation are a result of the difference between the qubit and drive frequency. Since the frequency (Stark) shift and $ZI$ interaction are equivalent because a frequency shift induces a $Z$-rotation on the control qubit, we can measure this shift and compensate for it with a [frame change](https://arxiv.org/abs/1612.00858).

```python
def build_cr_ramsey_scheds(qc: int, qt: int, cr_times, phase=0.0, ZI_MHz=0.0) -> np.array:
    """Build array of pulse schedules for CR Ramsey experiment.

    Args:
      qc: control qubit index
      qt: target qubit index
      cr_times: width of cross resonance pulses (in dt)
      phase: phase offset of cross resonance pulse (rad)
      ZI_MHz: Z-rotation rate of control qubit (in MHz) compensated in software by frame change
    """
    X90p = inst_sched_map.get('u2', P0=-np.pi/2, P1=np.pi/2, qubits=[qc])
    scheds = []
    for width in cr_times:
        with pulse.build() as sched:
            with pulse.align_sequential():
                pulse.call(X90p)
                framechange = 2*np.pi*int(width)*dt*ZI_MHz*1e6
                pulse.call(build_cr_pulse(qc, qt, int(width), control='0', \
                                            basis='Z', phase=phase, framechange=framechange))
                pulse.call(X90p)
                pulse.call(meas)

        scheds.append(sched)
    return scheds
```

```python
cr_ramsey_times = 16*np.linspace(0, 100, 21)
cr_ramsey_scheds = build_cr_ramsey_scheds(qc, qt, cr_ramsey_times)
cr_ramsey_scheds[-1].exclude(channels=[pulse.AcquireChannel(i)
                        for i in range(backend_config.n_qubits)]).draw(style=style)
```

```python
cr_ramsey_result = []
for sched in cr_ramsey_scheds:
    results = run_pulse(sched)
    cr_ramsey_result.append(np.real(1-2*results[qc]))
```

### Fitting Functions for the CR Ramsey Experiment

We will fit the results to a decaying sinusoid, where the frequency of oscillation is the frequency offset. We will also need to take care of the relation between the control and target qubit frequencies, because that will effect whether the control qubit Stark shift is higher or lower in frequency.

```python
def decay_sin(t, f, a, phi, tau, offset):
    """Fit function for exponentially-decaying sinusoid."""
    return a*np.exp(-t/tau)*np.sin(2*np.pi*f*t - phi) + offset

def fit_decay_sin(ts, values, p0):
    """Perform fit of decaying sinusoid."""
    return curve_fit(decay_sin, ts, values, p0=p0)
```

```python
def plot_cr_ramsey(cr_ramsey_times, cr_ramsey_result, ramsey_fit):
    """Plot CR Ramsey experiment and fit with ZI interaction rate."""
    fig, ax = plt.subplots(1, 1, figsize=(15,5))
    ax.scatter(cr_ramsey_times, cr_ramsey_result, lw=3.0, color='red')
    ax.plot(cr_ramsey_times, decay_sin(cr_ramsey_times, *ramsey_fit), lw=3.0, color='red')
    ax.set_ylabel('<Z(t)>', fontsize=20)
    ax.set_title('CR Ramsey Rate (ZI = %.3f MHz)' % ((ramsey_fit[0]/dt)/1e6), fontsize=20)
    ax.set_xlabel('time (dt)', fontsize=20)
```

```python
ramsey_fit,_ = fit_decay_sin(cr_ramsey_times, cr_ramsey_result, p0=[0.0025, 1, -np.pi/2, 300, 0.5])
plot_cr_ramsey(cr_ramsey_times, cr_ramsey_result, ramsey_fit)
```

```python
# the sign accounts for the relation b/w control and target frequencies
ZI_rate = np.sign(oscillator_freqs[qc]-oscillator_freqs[qt])*(ramsey_fit[0]/dt)/1e6
print('Shift frame according to ZI rate of %.3f MHz' % ZI_rate)
```

    Shift frame according to ZI rate of 9.725 MHz

Now we will rebuild the Ramsey schedule to compensate for the Stark shift and rerun the experiment.

```python
# run simulation to longer times
cr_ramsey_times = 16*np.linspace(0, 250, 21)
cr_ramsey_scheds = build_cr_ramsey_scheds(qc, qt, cr_ramsey_times, ZI_MHz=ZI_rate)

cr_ramsey_result = []
for sched in cr_ramsey_scheds:
    result = run_pulse(sched)
    cr_ramsey_result.append(np.real(1-2*result[qc]))
```

```python
ramsey_fit,_ = fit_decay_sin(cr_ramsey_times, cr_ramsey_result, p0=[0.00001, 0.1, 0, 300, -0.1])
plot_cr_ramsey(cr_ramsey_times, cr_ramsey_result, ramsey_fit)
```

We can see that we have substantially (but not totally) reduced the frequency shift (due to higher-order levels, etc.). At this point, we could return to the Hamiltonian tomography experiment
```
cr_scheds = build_cr_scheds(qc, qt, cr_times, phase=phase, ZI_MHz=ZI_rate)
```
However, since the frame change only affects the control qubit, the results would be identical to the second one.

## References

[1] S Sheldon, E Magesan, JM Chow, and JM Gambetta, "Procedure for systematically tuning up crosstalk in the cross resonance gate," *Phys Rev A* **93**, 060302 (2016)

[2] DC McKay, CJ Wood, S Sheldon, JM Chow, and JM Gambetta, "Efficient Z-Gates for Quantum Computing," *Phys Rev A* **96**, 022330 (2017)

```python
import qiskit
qiskit.__qiskit_version__
```

    {'qiskit-terra': '0.16.0',
     'qiskit-aer': '0.7.0',
     'qiskit-ignis': '0.5.0',
     'qiskit-ibmq-provider': '0.11.0',
     'qiskit-aqua': '0.8.0',
     'qiskit': '0.23.0'}