## Heat equation

The heat equation is a parabolic partial differential equation that describes the distribution of heat (or variation in temperature) in a given region over time. The general form of the equation in any coordinate system is given by:

\begin{align*}
\frac{\partial u}{\partial t} - \phi \nabla^2 u = f
\end{align*}

We will work here with the heat equation in one spatial dimension. This can be formulated as:

\begin{align*}
\mathcal{L}_{\bar{x}}^{\phi}u(\bar{x}) = \frac{\partial}{\partial t}u(\bar{x}) - \phi \frac{\partial^2}{\partial x^2}u(\bar{x}) = f(\bar{x}),
\end{align*}
where $\bar{x} = (t, x) \in \mathbb{R}^2$.

The fundamental solution to the heat equation gives us:

\begin{align*}
u(x,t) &= e^{-t}sin(2\pi x) \\
f(x,t) &= e^{-t}(4\pi^2 - 1)sin(2\pi x)
\end{align*}

#### Simulate data

```python
import time
import numpy as np
import sympy as sp
from scipy.optimize import minimize
from scipy.optimize import minimize_scalar
import matplotlib.pyplot as plt
```

```python
np.random.seed(int(time.time()))
def get_simulated_data(n):
    t = np.random.rand(n)
    x = np.random.rand(n)
    y_u = np.multiply(np.exp(-t), np.sin(2*np.pi*x))
    y_f = (4*np.pi**2 - 1) * np.multiply(np.exp(-t), np.sin(2*np.pi*x))
    return (t, x, y_u, y_f)

(t, x, y_u, y_f) = get_simulated_data(10)
```

#### Evaluate kernels

We use a reduced version of the kernel here.

1. $k_{uu}(x_i, x_j, t_i, t_j; \theta) =  e^ \left[ -\theta_1 (x_i-x_j)^2 - \theta_2 (t_i-t_j)^2 \right]$
2. $k_{ff}(\bar{x}_i,\bar{x}_j;\theta,\phi) = \mathcal{L}_{\bar{x}_i}^\phi \mathcal{L}_{\bar{x}_j}^\phi k_{uu}(\bar{x}_i, \bar{x}_j; \theta) = \mathcal{L}_{\bar{x}_i}^\phi \left[ \frac{\partial}{\partial t_j}k_{uu} - \phi \frac{\partial^2}{\partial x_j^2} k_{uu} \right] \\
= \frac{\partial}{\partial t_i}\frac{\partial}{\partial t_j}k_{uu} - \phi \left[ \frac{\partial}{\partial t_i}\frac{\partial^2}{\partial x_j^2}k_{uu} + \frac{\partial^2}{\partial x_i^2}\frac{\partial}{\partial t_j}k_{uu} \right] + \phi^2 \frac{\partial^2}{\partial x_i^2}\frac{\partial^2}{\partial x_j^2}k_{uu}$
3. $k_{fu}(\bar{x}_i,\bar{x}_j;\theta,\phi) = \mathcal{L}_{\bar{x}_i}^\phi k_{uu}(\bar{x}_i, \bar{x}_j; \theta)
= \frac{\partial}{\partial t_i}k_{uu} - \phi \frac{\partial^2}{\partial x_i^2}k_{uu}$

```python
x_i, x_j, t_i, t_j, theta1, theta2, phi = sp.symbols('x_i x_j t_i t_j theta1 theta2 phi')
```

```python
kuu_sym = sp.exp(-theta1*(x_i - x_j)**2 - theta2*(t_i - t_j)**2)
kuu_fn = sp.lambdify((x_i, x_j, t_i, t_j, theta1, theta2), kuu_sym, "numpy")
def kuu(t, x, theta1, theta2):
    k = np.zeros((t.size, t.size))
    for i in range(t.size):
        for j in range(t.size):
            k[i,j] = kuu_fn(x[i], x[j], t[i], t[j], theta1, theta2)
    return k
```

```python
kff_sym = sp.diff(kuu_sym, t_j, t_i) \
        - phi*sp.diff(kuu_sym,x_j,x_j,t_i) \
        - phi*sp.diff(kuu_sym,t_j,x_i,x_i) \
        + phi**2*sp.diff(kuu_sym,x_j,x_j,x_i,x_i)
kff_fn = sp.lambdify((x_i, x_j, t_i, t_j, theta1, theta2, phi), kff_sym, "numpy")
def kff(t, x, theta1, theta2, p):
    k = np.zeros((t.size, t.size))
    for i in range(t.size):
        for j in range(t.size):
            k[i,j] = kff_fn(x[i], x[j], t[i], t[j], theta1, theta2, p)
    return k
```

```python
kfu_sym = sp.diff(kuu_sym,t_i) - phi*sp.diff(kuu_sym,x_i,x_i)
kfu_fn = sp.lambdify((x_i, x_j, t_i, t_j, theta1, theta2, phi), kfu_sym, "numpy")
def kfu(t, x, theta1, theta2, p):
    k = np.zeros((t.size, t.size))
    for i in range(t.size):
        for j in range(t.size):
            k[i,j] = kfu_fn(x[i], x[j], t[i], t[j], theta1, theta2, p)
    return k
```

```python
def kuf(t, x, theta1, theta2, p):
    return kfu(t, x, theta1, theta2, p).T
```

#### create covariance matrix and NLML

```
params = [sig_u, l_u, phi]
```

```python
def nlml(params, t, x, y1, y2, s):
    params = np.exp(params)
    K = np.block([
        [
            kuu(t, x, params[0], params[1]) + s*np.identity(x.size),
            kuf(t, x, params[0], params[1], params[2])
        ],
        [
            kfu(t, x, params[0], params[1], params[2]),
            kff(t, x, params[0], params[1], params[2]) + s*np.identity(x.size)
        ]
    ])
    y = np.concatenate((y1, y2))
    val = 0.5*(np.log(abs(np.linalg.det(K))) + np.mat(y) * np.linalg.inv(K) * np.mat(y).T)
    return val.item(0)
```

#### Optimize hyperparameters

```python
nlml((1,1,0), t, x, y_u, y_f, 1e-6)
```

    10.08849356029074

```python
%%timeit
nlml_wp = lambda params: nlml(params, t, x, y_u, y_f, 1e-7)
minimize(
    nlml_wp,
    np.random.rand(3),
    method="Nelder-Mead",
    options={'maxiter' : 5000, 'fatol' : 0.001})
```

    2.13 s ± 267 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

```python
def minimize_restarts(t, x, y_u, y_f, n = 10):
    nlml_wp = lambda params: nlml(params, t, x, y_u, y_f, 1e-7)
    all_results = []
    for it in range(0,n):
        all_results.append(
            minimize(
                nlml_wp,
                np.random.rand(3),
                method="Nelder-Mead",
                options={'maxiter' : 5000, 'fatol' : 0.001}))
    filtered_results = [m for m in all_results if 0 == m.status]
    return min(filtered_results, key = lambda x: x.fun)
```

```python
m = minimize_restarts(t, x, y_u, y_f, 20)
m
```

     final_simplex: (array([[ 1.51464808, -1.4236345 , -0.03028208],
           [ 1.51462468, -1.42355069, -0.03028317],
           [ 1.51466738, -1.42367271, -0.03028443],
           [ 1.51467467, -1.42354775, -0.03028619]]), array([-3.63412215, -3.63412211, -3.63412211, -3.63412206]))
               fun: -3.6341221452010437
           message: 'Optimization terminated successfully.'
              nfev: 176
               nit: 100
            status: 0
           success: True
                 x: array([ 1.51464808, -1.4236345 , -0.03028208])

##### Estimated value of $\alpha$

```python
np.exp(m.x[2])
```

    0.9701718328188159

#### Analysis

##### Contour lines for the likelihood

```python
delta = 0.01
theta1_range = np.arange(1, 2, delta)
theta2_range = np.arange(-2, 0, delta)
theta1_mesh, theta2_mesh = np.meshgrid(theta1_range, theta2_range)
nlml_mesh_fn = lambda mesh1, mesh2: nlml(np.array([mesh1, mesh2, 0]), t, x, y_u, y_f, 1e-7)
nlml_mesh = np.zeros(theta1_mesh.shape)
for i in range(nlml_mesh.shape[0]):
    for j in range(nlml_mesh.shape[1]):
        nlml_mesh[i][j] = nlml_mesh_fn(theta1_mesh[i][j], theta2_mesh[i][j])
```

```python
contour_range = np.logspace(0, np.log(np.max(nlml_mesh) - np.min(nlml_mesh)), num=15, base=np.exp(1))
contour_range = np.min(nlml_mesh) + contour_range

f, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10,3))
f.suptitle("NLML contour lines")
cs1 = ax1.contour(theta1_mesh, theta2_mesh, nlml_mesh, contour_range)
plt.clabel(cs1, inline=1, fontsize=5)
ax1.set(xlabel= r"$ln(\theta_1)$", ylabel= r"$ln(\theta_2)$")
cs2 = ax2.contour(np.exp(theta1_mesh), np.exp(theta2_mesh), nlml_mesh, contour_range)
plt.clabel(cs1, inline=1, fontsize=5)
ax2.set(xlabel= r"$\theta_1$", ylabel= r"$\theta_2$")
```

    [<matplotlib.text.Text at 0x10d094160>, <matplotlib.text.Text at 0x10d082a58>]

```python
plt.show()
```

##### Profile likelihood

```python
theta1_optim = np.zeros(theta1_range.size)
for i in range(theta1_range.size):
    nlml_opt_theta2 = lambda t2 : nlml(np.array([theta1_range[i], t2, 0]), t, x, y_u, y_f, 1e-7)
    m = minimize_scalar(nlml_opt_theta2)
    theta1_optim[i] = m['fun']
```

```python
theta2_optim = np.zeros(theta2_range.size)
for i in range(theta2_range.size):
    def nlml_opt_theta1(t1):
        return nlml(np.array([t1, theta2_range[i], 0]), t, x, y_u, y_f, 1e-7)
    m = minimize_scalar(nlml_opt_theta1)
    theta2_optim[i] = m['fun']
```

```python
f, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, sharey=True, figsize=(10,3))
f.suptitle("Profile likelihood")
ax1.plot(theta1_range, theta1_optim)
ax1.set(xlabel= r"$ln(\theta_1)$", ylabel= "nlml")
ax2.plot(theta2_range, theta2_optim, 'r')
ax2.set(xlabel= r"$ln(\theta_2)$", ylabel= "nlml")
```

    [<matplotlib.text.Text at 0x10d45e518>, <matplotlib.text.Text at 0x10d454400>]

```python
plt.show()
```

##### Errors

We generate 5 set of samples for each number of data points(n) in the range (5,25) as earlier. The absolute error in the parameter estimate and the computation times are plotted in the following figure.

```python
n_range = np.arange(5, 25)
plot_data = np.zeros((5, n_range.size, 4))
for j in range(plot_data.shape[0]):
    print("iter ", j)
    for i in range(plot_data.shape[1]):
        print("# points: ", n_range[i])
        start_time = time.time()
        (t, x, y1, y2) = get_simulated_data(n_range[i])
        nlml_wp = lambda params: nlml(params, t, x, y1, y2, 1e-7)
        m = minimize(nlml_wp, np.random.rand(4), method="Nelder-Mead")
        end_time = time.time()
        plot_data[j,i,:] = np.array([m.nfev, m.nit, np.exp(m.x[2]), end_time - start_time])
```

    iter  0
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  1
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  2
    # points:  5
    # points:  6

    /usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: overflow encountered in exp

    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  3
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  4
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24

```python
from matplotlib.ticker import MaxNLocator

f, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(12,4))

for i in range(1,4):
    ax1.plot(n_range[5:], abs(plot_data[i,5:,2] -1), "og")
ax1.set(xlabel = "Number of data points", ylabel = "Absolute error")
ax1.set_title("(A) Error in estimate of the parameter")
ax1.plot(n_range[5:], np.amin(abs(np.amin(plot_data, axis=2) - 1), axis=0)[5:])
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
ax1.axhline(0.01, color='black', linestyle='-.')
ax1.axhline(0.004, color='red', linestyle='-.')
ax1.axhline(0.002, color='black', linestyle='-.')

for i in range(1,4):
    ax2.plot(n_range[5:], plot_data[i,5:,3], "og")
ax2.set(xlabel = "Number of data points", ylabel = "Execution time")
ax2.set_title("(B) Execution time benchmark")
ax2.xaxis.set_major_locator(MaxNLocator(integer=True))
```

```python
plt.show()
```

The minimum error for each value of n is bounded by 0.002 for n > 10. The computation time also increases monotonically with n.

##### With the full kernel

```python
theta, l1, l2 = sp.symbols('theta l1 l2')

kuu_sym = theta*sp.exp(-(x_i - x_j)**2/(2*l1) - (t_i - t_j)**2/(2*l2))
kuu_fn = sp.lambdify((x_i, x_j, t_i, t_j, theta, l1, l2), kuu_sym, "numpy")
def kuu(t, x, theta, l1, l2):
    k = np.zeros((t.size, t.size))
    for i in range(t.size):
        for j in range(t.size):
            k[i,j] = kuu_fn(x[i], x[j], t[i], t[j], theta, l1, l2)
    return k

kff_sym = sp.diff(kuu_sym, t_j, t_i) \
        - phi*sp.diff(kuu_sym,x_j,x_j,t_i) \
        - phi*sp.diff(kuu_sym,t_j,x_i,x_i) \
        + phi**2*sp.diff(kuu_sym,x_j,x_j,x_i,x_i)
kff_fn = sp.lambdify((x_i, x_j, t_i, t_j, theta, l1, l2, phi), kff_sym, "numpy")
def kff(t, x, theta, l1, l2, p):
    k = np.zeros((t.size, t.size))
    for i in range(t.size):
        for j in range(t.size):
            k[i,j] = kff_fn(x[i], x[j], t[i], t[j], theta, l1, l2, p)
    return k

kfu_sym = sp.diff(kuu_sym,t_i) - phi*sp.diff(kuu_sym,x_i,x_i)
kfu_fn = sp.lambdify((x_i, x_j, t_i, t_j, theta, l1, l2, phi), kfu_sym, "numpy")
def kfu(t, x, theta, l1, l2, p):
    k = np.zeros((t.size, t.size))
    for i in range(t.size):
        for j in range(t.size):
            k[i,j] = kfu_fn(x[i], x[j], t[i], t[j], theta, l1, l2, p)
    return k

def kuf(t, x, theta, l1, l2, p):
    return kfu(t, x, theta, l1, l2, p).T

def nlml(params, t, x, y1, y2, s):
    params = np.exp(params)
    K = np.block([
        [
            kuu(t, x, params[0], params[1], params[2]) + s*np.identity(x.size),
            kuf(t, x, params[0], params[1], params[2], params[3])
        ],
        [
            kfu(t, x, params[0], params[1], params[2], params[3]),
            kff(t, x, params[0], params[1], params[2], params[3]) + s*np.identity(x.size)
        ]
    ])
    y = np.concatenate((y1, y2))
    val = 0.5*(np.log(abs(np.linalg.det(K))) + np.mat(y) * np.linalg.inv(K) * np.mat(y).T)
    return val.item(0)

(t, x, y_u, y_f) = get_simulated_data(10)
```

```python
nlml((1,1,1,0), t, x, y_u, y_f, 1e-6)
```

    1100767.8910308597

```python
%%timeit
nlml_wp = lambda params: nlml(params, t, x, y_u, y_f, 1e-7)
minimize(
    nlml_wp,
    np.random.rand(4),
    method="Nelder-Mead",
    options={'maxiter' : 5000, 'fatol' : 0.001})
```

    6.93 s ± 1.94 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

The reduced kernel takes less time for optimization as compared to the full one.

```python
n_range = np.arange(5, 25)
plot_data_r = np.zeros((5, n_range.size, 4))
for j in range(plot_data_r.shape[0]):
    print("iter ", j)
    for i in range(plot_data_r.shape[1]):
        print("# points: ", n_range[i])
        start_time = time.time()
        (t, x, y1, y2) = get_simulated_data(n_range[i])
        nlml_wp = lambda params: nlml(params, t, x, y1, y2, 1e-7)
        m = minimize(nlml_wp, np.random.rand(4), method="Nelder-Mead")
        end_time = time.time()
        plot_data_r[j,i,:] = np.array([m.nfev, m.nit, np.exp(m.x[3]), end_time - start_time])
```

    iter  0
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  1
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  2
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24
    iter  3
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24

    /usr/local/lib/python3.6/site-packages/numpy/linalg/linalg.py:1874: RuntimeWarning: overflow encountered in det
      r = _umath_linalg.det(a, signature=signature)

    iter  4
    # points:  5
    # points:  6
    # points:  7
    # points:  8
    # points:  9
    # points:  10
    # points:  11
    # points:  12
    # points:  13
    # points:  14
    # points:  15
    # points:  16
    # points:  17
    # points:  18
    # points:  19
    # points:  20
    # points:  21
    # points:  22
    # points:  23
    # points:  24

```python
f, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(12,4))

for i in range(plot_data.shape[0]):
    if(i != 3):
        ax1.plot(n_range[5:], abs(plot_data_r[i,5:,2] -1), "og")
ax1.set(xlabel = "Number of data points", ylabel = "Absolute error")
ax1.set_title("Error in estimate of the parameter")
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
ax1.axhline(0.01, color='black', linestyle='-.')
ax1.axhline(0.004, color='red', linestyle='-.')
ax1.axhline(0.0025, color='black', linestyle='-.')

for i in range(plot_data.shape[0]):
    if(i != 3):
        ax2.plot(n_range[5:], plot_data_r[i,5:,3], "og")
ax2.set(xlabel = "Number of data points", ylabel = "Execution time")
ax2.set_title("Execution time benchmark")
ax2.xaxis.set_major_locator(MaxNLocator(integer=True))
```

```python
plt.show()
```

The same analysis as earlier in the chapter was done for the full kernel given by $\theta exp((\mathbf{x}-\mathbf{y})^T \Sigma (\mathbf{x}-\mathbf{y}))$ where $\Sigma = diag([l_1, l_2])$.

It can be noticed that the error in parameter estimate is slightly lower for the full kernel but the difference is not very significant. Meanwhile, the execution times are significantly different for both the cases. For 10 data points, the minimizer takes an average of 2.13 seconds for the reduced kernel while it is 6.93 seconds for the full kernel. This shows that not all hyperparameters might be necessary to get acceptable results. For some specific problems, having an intuition on the choice of kernels might be fruitful in the end.


This notebook will replicate H2 (minimal basis, B-K) energy evaluation using pytket, pytket_qiskit and pytket_honeywell

python~=3.7
requirements:
pytket==0.4.3
pytket_qiskit>0.3.4
pytket_honeywell==0.0.1

sympy
numpy

```python
import os
from functools import reduce
import operator
import copy
from collections import Counter

import sympy
import numpy as np

from pytket.circuit import Circuit
from pytket.extensions.qiskit import AerBackend, IBMQBackend
from pytket.extensions.honeywell import HoneywellBackend
from pytket.circuit import PauliExpBox, fresh_symbol
from pytket.pauli import Pauli
from pytket.utils.measurements import append_pauli_measurement
from pytket.utils import expectation_from_counts
from pytket.passes import DecomposeBoxes

from h2_hamiltonians import bond_length_hams

```

Generate ansatz circuit using pytket using known operators

```python
ansatz = Circuit(4,4)
param = fresh_symbol("t")
coeff = -2*param/sympy.pi
box = PauliExpBox((Pauli.Y, Pauli.Z, Pauli.X, Pauli.Z), coeff)
ansatz.X(0)
ansatz.add_pauliexpbox(box, ansatz.qubits)
DecomposeBoxes().apply(ansatz)

```

    True

Set up hamiltonian processing code

```python
contracted_measurement_bases = ('Z', 'X', 'Y')

def get_energy_from_counts(coeff_shots):
    return sum(coeff * expectation_from_counts(shots) for coeff, shots in coeff_shots)

def operator_only_counts(counts, operator_qubs):
    filterstate = lambda s: tuple(np.array(s)[list(operator_qubs)])
    filtereredcounters = (Counter({filterstate(state):count}) for state, count in counts.items())
    return reduce(operator.add, filtereredcounters)

def submit_hamiltonian(state_circ, measurementops, n_shots, backend, base_name='ansatz'):

    circuits = []
    circuit_names = []
    for entry, basis in zip(measurementops,contracted_measurement_bases):
        meas_circ = state_circ.copy()
        append_pauli_measurement(entry, meas_circ)
        meas_circ = backend.get_compiled_circuit(meas_circ)
        circuit_names += [f'{base_name}_{basis}']
        circuits.append(meas_circ)

    print(circuit_names)
    return backend.process_circuits(circuits, n_shots=n_shots)

def calculate_hamiltonian(measured_counts, hamiltonian):
    hamcopy = copy.copy(hamiltonian)
    constant_coeff = hamcopy.pop(tuple())
    hamoptups, coeff_list = zip(*hamcopy.items())
    converted_shot_list = []

    measurement_mapper = lambda opdict: opdict[0] if 0 in opdict else 'Z'

    hamop2measop = {tp: measurement_mapper(dict(tp)) for tp in hamoptups}

    meascounts_map = dict(zip(contracted_measurement_bases, measured_counts))

    for (optup, coeff) in zip(hamoptups, coeff_list):
        op_qubs, _ = zip(*optup)
        selected_meas_counts = meascounts_map[hamop2measop[optup]]
        converted_shot_list.append(operator_only_counts(selected_meas_counts, op_qubs))

    return get_energy_from_counts (zip(coeff_list, converted_shot_list)) + constant_coeff
```

Set up minimal measurement bases required to measure whole hamiltonian

```python
measurement_ops = [((0, 'Z'), (1, 'Z'), (2, 'Z'), (3, 'Z')),
((0, 'X'), (1, 'Z'), (2, 'X'), (3, 'Z')), ((0, 'Y'), (1, 'Z'), (2, 'Y'), (3, 'Z'))]
```

Set up honeywell API key and machine name

```python
hwell_apikey = '<apikey>'
hwell_machine = "HQS-LT-1.0-APIVAL"
```

```python
bond_length = 0.735
popt = bond_length_hams[bond_length]["optimal_parameter"]
hamopt = bond_length_hams[bond_length]["hamiltonian"]

aerbackend = AerBackend()
backend = HoneywellBackend(hwell_apikey, device_name=hwell_machine, label='h2_exp')
# backend = aerbackend
backend = IBMQBackend('ibmq_burlington', hub='ibmq')
state_circuit = ansatz.copy()
state_circuit.symbol_substitution({param:popt})

sub_jobs = submit_hamiltonian(state_circuit, measurement_ops, 100, backend)
print(sub_jobs)
results = [backend.run_circuit(job).get_counts() for job in sub_jobs]

calculate_hamiltonian(results, hamopt)
```

    ['ansatz_Z', 'ansatz_X', 'ansatz_Y']
    [('5e554bacd8204b0018fd539f', 0), ('5e554bacd8204b0018fd539f', 1), ('5e554bacd8204b0018fd539f', 2)]
    Job Status: job has successfully run
    Job Status: job has successfully run
    Job Status: job has successfully run

    -0.6357045412185943

```python

retrieve_ids = ['', '', '']
retrieved_results = [backend.run_circuit(JobHandle(id)).get_counts() for id in retrieve_ids]

calculate_hamiltonian(retrieved_results, hamopt)
```


# 3.2 Incompressible Navier-Stokes equations
We are solving the unsteady Navier Stokes equations:

Find $(u,p):[0,T] \to (H_{0,D}^1)^d \times L^2$, s.t.
\begin{align}
\int_{\Omega} \partial_t u \cdot v + \int_{\Omega} \nu \nabla u : \nabla v + u \cdot \nabla u\cdot v - \int_{\Omega} \operatorname{div}(v) p &= \int f v  && \forall v \in (H_{0,D}^1)^d, \\
- \int_{\Omega} \operatorname{div}(u) q &= 0 && \forall q \in L^2, \\
\quad u(t=0) & = u_0
\end{align}

```python
# You will need Netgen/Ngsolve to run this
from ngsolve import *
from ngsolve.webgui import Draw
from netgen.geom2d import SplineGeometry

# Standard numerical Python imports
import numpy as np
import scipy.sparse as sp
from scipy.linalg import cholesky
from scipy.sparse.linalg import svds, spsolve

# Optional, for visualization of the eigenvalues
import matplotlib.pyplot as plt
```

## Function for generating data

Generates FOM solutions.  Based on the official Ngsolve NSE tutorial.

```python
def generate_NSE_data(h, deg, nus, tEnd, filename):

    # Def mesh geometry
    geo = SplineGeometry()
    geo.AddRectangle( (0, 0), (2, 0.41), bcs = ("wall", "outlet", "wall", "inlet"))
    geo.AddCircle ( (0.2, 0.2), r=0.05, leftdomain=0, rightdomain=1, bc="cyl")
    mesh = Mesh( geo.GenerateMesh(maxh=h))
    mesh.Curve(3);

    # Def FE space and solution grid function
    V = VectorH1(mesh,order=deg, dirichlet="wall|cyl|inlet")
    Q = H1(mesh,order=deg-1)
    X = FESpace([V,Q])
    gfu = GridFunction(X)
    velocity = gfu.components[0]

    print(f'velocity DOFs is {V.ndofglobal}')

    # parabolic inflow at bc=1:
    uin = CoefficientFunction((1.5*4*y*(0.41-y)/(0.41*0.41),0))
    gfu.components[0].Set(uin, definedon=mesh.Boundaries("inlet"))

    (u,p), (v,q) = X.TnT()

    savez_dict = dict() # for storing snapshots

    for i, nu in enumerate(nus):

        a = BilinearForm(X)
        stokes = (nu*InnerProduct(Grad(u)+Grad(u).trans, Grad(v))
                  -div(u)*q-div(v)*p)*dx
        a += stokes
        a.Assemble()

        f = LinearForm(X)
        f.Assemble()

        inv_stokes = a.mat.Inverse(X.FreeDofs())

        res = f.vec.CreateVector()
        res.data = f.vec - a.mat*gfu.vec
        gfu.vec.data += inv_stokes * res

        # IMEX time discretization

        dt = 0.00005

        # matrix for implicit part of IMEX(1) scheme:
        mstar = BilinearForm(X)
        mstar += InnerProduct(u,v)*dx + dt*stokes
        mstar.Assemble()
        inv = mstar.mat.Inverse(X.FreeDofs())

        conv = LinearForm(X)
        conv += InnerProduct(Grad(velocity)*velocity,v)*dx

        t, tend = 0, 0

        # implicit Euler/explicit Euler splitting method:
        tend += tEnd

        uh = gfu.components[0]

        ns = int(tend/dt/50) # save snapshot every 50 time steps
        snaps = np.zeros([ns, uh.vec.size])

        count, count_loc = 0, 0

        while t < tend-0.5*dt:
            print (f'nu is {nu} and t={t}', end='\r')

            conv.Assemble()
            res.data = a.mat * gfu.vec + conv.vec
            gfu.vec.data -= dt * inv * res

            if count_loc == 49:
                snaps[count,:] = uh.vec.data
                count += 1
                count_loc = 0
            else:
                count_loc += 1

            t = t + dt

        savez_dict['NSE_nu'+str(i)] = snaps.T
    np.savez(filename+'.npz', **savez_dict)

    return 'data generated'
```

# Class which manages the ROM

Basically all the nodal value manipulations follow from the fact that Netgen
stores its vector-valued function DOFs on top of each other in equal-sized pieces.

```python
class MyROM:

    def __init__(self, h, deg):
        '''
        Defines mesh and FE space.  Computes mass matrix. Note that
        (h, deg) should match the data loaded in "file".
        '''
        # Def mesh geometry and FE space
        geo = SplineGeometry()
        geo.AddRectangle((0, 0), (2, 0.41), bcs = ("wall", "outlet", "wall", "inlet"))
        geo.AddCircle ((0.2, 0.2), r=0.05, leftdomain=0, rightdomain=1, bc="cyl")
        self.mesh = Mesh(geo.GenerateMesh(maxh=h))
        self.mesh.Curve(3)
        self.V = VectorH1(self.mesh, order=deg, dirichlet="wall|cyl|inlet")

        # Compute mass matrix
        u, v = self.V.TnT()
        mass = BilinearForm(self.V, symmetric=True)
        mass += SymbolicBFI(InnerProduct(u, v))
        mass.Assemble()
        rows, cols, vals = mass.mat.COO()
        self.M = sp.csr_matrix((vals, (rows, cols)))

    def load_data_from_file(self, file, nSnaps):
        '''
        Loads snapshots from file and computes center component-wise.

        Outputs:  u -- [2n, nSnaps] array of stacked velocity components.
                  ubar -- [2n, 1] array of corresponding means.
        '''
        self.u = np.load(file, allow_pickle=True)[:,-nSnaps:]
        n = int(self.u.shape[0]/2)
        self.ubar = np.concatenate((np.mean(self.u[:n],axis=1),
                                    np.mean(self.u[n:],axis=1)),
                                   axis=0).reshape(-1,1)
        self.nSnaps = nSnaps

    def load_data_from_array(self, array, nSnaps):
        '''
        Loads snapshots from array and computes center component-wise.

        '''
        self.u = array[:,-nSnaps:]
        n = int(self.u.shape[0]/2)
        self.ubar = np.concatenate((np.mean(self.u[:n],axis=1),
                                    np.mean(self.u[n:],axis=1)),
                                   axis=0).reshape(-1,1)
        self.nSnaps = nSnaps

    def generate_POD_basis(self, numBasis=100):
        '''
        Generates POD basis from snapshot data.

        Inputs:   numBasis -- number of singular vectors to compute.

        Outputs:  POD -- [2n, numBasis] array of singular vectors.
                  ews -- length numBasis array of largest singular values
                       in descending order.
        '''
        # Compute SVD
        R = cholesky(self.M.todense())  # wish it were sparse, but oh well
        RuTilde = R @ (self.u - self.ubar)
        U, S, _ = svds(RuTilde, k=numBasis, which='LM')

        # Because we want descending order...
        U = np.flip(U, axis=1)
        self.ews = np.flip(S)

        # Compute POD basis
        self.POD = spsolve(R, U[:,:numBasis])
        print(f'POD basis of size {numBasis} has been generated')

    def plot_evals(self, n):
        '''
        Plots magnitude of first n eigenvalues.
        '''
        x = np.array([i+1 for i,e in enumerate(self.ews)])[:n]

        fig = plt.figure()
        ax = fig.add_subplot(1, 1, 1)
        ax.scatter(x, self.ews[:n])
        ax.set_yscale('log')
        plt.show()

    def project(self, u, n):
        '''
        Projects full-order object to reduced space of size n.
        '''
        return self.POD[:,:n].T @ self.M @ (u - self.ubar)

    def reconstruct(self, a):
        '''
        Reconstructs full-order object from reduced-order one.
        '''
        return self.ubar + self.POD[:,:a.shape[0]] @ a

    def compute_L2_error(self, approx):
        '''
        Computes relative L2 error between snapshots and approximation.
        '''
        diff = self.u - approx
        num2 = np.trace(diff.T @ self.M @ diff)
        den2 = np.trace(self.u.T @ self.M @ self.u)
        return np.sqrt(num2/den2)

    def test_POD_projection(self):
        '''
        Tests the convergence of the computed POD basis.
        '''
        for dim in [2**i for i in range(6)]:
            # Compute % of squared EWs captured in first q_dim dimensions
            percent_energy = (np.sum(self.ews[:dim]**2)
                              / np.sum(self.ews**2) * 100)

            # Reconstruction after projection
            aa = self.project(self.u, dim)
            projRecon = self.reconstruct(aa)

            # Relative L2 error in mass norm (no ode solve)
            error = self.compute_L2_error(projRecon)
            print(f'dim is {dim}, % energy is {percent_energy:.3f}, '
                  + f'and compression error is {error:.7f}')

    def assemble(self, dim, nu):
        '''
        Assembles the right-hand side (RHS) of the NSE-ROM.
        Must be called after POD basis has been generated.

        Inputs:   dim -- reduced basis dimension.
                  nu -- viscosity in NSE

        Outputs:  [self.r{i} for i in range(3)] representing the
                  rank i terms of the RHS computed in terms of the
                  POD basis. Respective sizes are [dim], [dim,dim],
                  and [dim,dim,dim].
        '''
        self.dim = dim
        u, v = self.V.TnT()

        # Def ubar (grid function)
        ubarG = GridFunction(self.V)
        ubarG.vec.FV().NumPy()[:] = self.ubar.flatten()

        # Def convection of center (coefficient function)
        uguC = ubarG.vec.CreateVector()
        uguC = Grad(ubarG)*ubarG  # Grad is Jacobian J_ij = \nabla_j u_i

        # Def rank 1 terms in RHS
        rank1 = LinearForm(self.V)
        rank1 += SymbolicLFI(InnerProduct(uguC, v)
                             + nu*InnerProduct(Grad(ubarG)+Grad(ubarG).trans, Grad(v)))
        rank1.Assemble()

        # Def rank 2 terms in RHS
        rank2 = BilinearForm(self.V)
        rank2 += SymbolicBFI(InnerProduct(Grad(ubarG)*u+Grad(u)*ubarG, v)
                             + nu*InnerProduct(Grad(u)+Grad(u).trans, Grad(v)))
        rank2.Assemble()
        rows, cols, vals = rank2.mat.COO()
        rank2Mat = sp.csr_matrix((vals, (rows, cols)))

        # Def rank 3 term in RHS using list of rank 2s (no tensors in Netgen)
        rank3 = [0 for b in range(dim)]
        rank3Mat = [0 for b in range(dim)]
        for b in range(dim):
            phibG = GridFunction(self.V)
            phibG.vec.FV().NumPy()[:] = self.POD[:,b]
            rank3[b] = BilinearForm(self.V)
            rank3[b] += SymbolicBFI(InnerProduct(Grad(u)*phibG, v))
            rank3[b].Assemble()
            rows, cols, vals = rank3[b].mat.COO()
            rank3Mat[b] = sp.csr_matrix((vals, (rows, cols)))

        # Compute reduced quantities
        self.r1 = self.POD[:,:dim].T @ rank1.vec.FV().NumPy()[:]  #r_c
        self.r2 = self.POD[:,:dim].T @ rank2Mat.T @ self.POD[:,:dim]  #S_ac
        self.r3 = np.array([self.POD[:,:dim].T @ rank3Mat[b].T @ self.POD[:,:dim]
                            for b in range(dim)])  #T_bac

    def integrate_fEuler(self, dt, Nt, nSub=100):
        '''
        Forward Euler time integration of the ROM system.
        Includes optional substepping for finer time scales.

        Inputs:   dt -- length of time step
                  Nt -- number of time steps.
                  nSub -- parameter controlling the substepping.

        Outputs:  a -- [dim, Nt] array containing the reduced solution.
        '''
        Ns = self.u.shape[1]

        # Initial data
        a = np.zeros([self.dim, Nt])
        a[:,0] = (self.u[:,Ns-Nt]-self.ubar.flatten()) @ self.M @ self.POD[:,:self.dim]

        # Simple forward Euler with substepping
        for i in range(1, Nt):
            aOld = a[:,i-1]
            for j in range(nSub):
                rhsn = np.dot(aOld, self.r2) + np.dot(aOld, np.dot(aOld, self.r3))
                aOld = aOld - dt / nSub * (rhsn + self.r1)  # note the minus sign
            a[:,i] = aOld

        self.a = a

    def test_ROM_approximation(self, nu, nSub):
        '''
        Tests the convergence of the computed ROM approximation.
        '''
        for dim in [2**i for i in range(6)]:
            # Compute % of squared EWs captured in first q_dim dimensions
            percent_energy = (np.sum(self.ews[:dim]**2)
                              / np.sum(self.ews**2) * 100)

            self.assemble(dim, nu)
            self.integrate_fEuler(0.0025, self.nSnaps, nSub)
            uTilde = self.reconstruct(self.a)
            error = self.compute_L2_error(uTilde)
            print(f'dim is {dim}, % energy is {percent_energy:.3f}, '
                  + f'and prediction error is {error:.7f}')
```

# Testing

### Beware, this section will download about 120MB of data.

Reynolds number 10, P2/P1 elements.  Snapshots in [0,1] starting from rest.

```python
# Generate laminar flow data
generate_NSE_data(0.04, 2, [0.01], 1, 'laminar')
repo = np.load('laminar.npz')
snaps = repo['NSE_nu0']

rom = MyROM(0.04, 2)

rom.load_data_from_array(snaps, 400)
rom.generate_POD_basis()
rom.plot_evals(25)
rom.test_POD_projection()
rom.test_ROM_approximation(0.01, 400)
```

Reynolds number 214, P3/P2 elements.  Snapshots in [0,1] starting from rest.

```python
generate_NSE_data(0.04, 3, [5e-4, 6e-4, 7e-4], 1, 'periodic')
repo = np.load('periodic.npz')
snaps = repo['NSE_nu0']

rom = MyROM(0.04, 3)
rom.load_data_from_array(snaps, 400)
rom.generate_POD_basis()
rom.plot_evals(40)
rom.test_POD_projection()
rom.test_ROM_approximation(5e-4, 400)
```

Little animations which show the evolution of the first component of velocity.
Top is FOM solution and bottom is ROM solution.

```python
## Visualization for n=16
rom.assemble(16, 5e-4)
rom.integrate_fEuler(0.0025, 400, nSub=100)
uTilde = rom.reconstruct(rom.a)

gfut = GridFunction(rom.V, multidim=0)
for i,col in enumerate(snaps.T[::10]):
    gfut.AddMultiDimComponent(col[:11466])
Draw (gfut, rom.mesh, interpolate_multidim=True, animate=True)

gfut = GridFunction(rom.V, multidim=0)
for i,col in enumerate(uTilde.T[::10]):
    gfut.AddMultiDimComponent(col[:11466])
Draw (gfut, rom.mesh, interpolate_multidim=True, animate=True)
```

    WebGuiWidget(value={'ngsolve_version': '6.2.2105', 'mesh_dim': 2, 'order2d': 2, 'order3d': 2, 'draw_vol': Fals…

    WebGuiWidget(value={'ngsolve_version': '6.2.2105', 'mesh_dim': 2, 'order2d': 2, 'order3d': 2, 'draw_vol': Fals…

    BaseWebGuiScene

Reynolds number 214, P3/P2 elements.  Snapshots in [0.5,1].

```python
rom = MyROM(0.04, 3)
rom.load_data_from_array(snaps, 200)
rom.generate_POD_basis()
rom.plot_evals(40)
rom.test_POD_projection()
rom.test_ROM_approximation(5e-4, 200)
```

## Experiment from paper

Re 214, 155 for training, Re 180 for testing, P3/P2 elements.

```python
repo = np.load('periodic.npz')
snaps = np.concatenate((repo['NSE_nu0'], repo['NSE_nu2']), axis=1)

rom = MyROM(0.04, 3)
rom.load_data_from_array(snaps, 800)
rom.generate_POD_basis()
rom.plot_evals(40)

# Have to redefine some stuff for accurate testing
rom.u = repo['NSE_nu1']
rom.nSnaps = 400

rom.test_POD_projection()
rom.test_ROM_approximation(6e-4, 400)
```

Same little animations.  The trend is still well-captured.

```python
## Visualization for n=16
rom.assemble(16, 6e-4)
rom.integrate_fEuler(0.0025, 400, nSub=100)
uTilde = rom.reconstruct(rom.a)

gfut = GridFunction(rom.V, multidim=0)
for i,col in enumerate(snaps.T[::10]):
    gfut.AddMultiDimComponent(col[:11466])
Draw (gfut, rom.mesh, interpolate_multidim=True, animate=True)

gfut = GridFunction(rom.V, multidim=0)
for i,col in enumerate(uTilde.T[::10]):
    gfut.AddMultiDimComponent(col[:11466])
Draw (gfut, rom.mesh, interpolate_multidim=True, animate=True)
```

    WebGuiWidget(value={'ngsolve_version': '6.2.2105', 'mesh_dim': 2, 'order2d': 2, 'order3d': 2, 'draw_vol': Fals…

    WebGuiWidget(value={'ngsolve_version': '6.2.2105', 'mesh_dim': 2, 'order2d': 2, 'order3d': 2, 'draw_vol': Fals…

    BaseWebGuiScene


# Traveling Salesman Problem

## Objective and Prerequisites

In this example, you’ll learn how to tackle one of the most famous combinatorial optimization problems in existence: the Traveling Salesman Problem (TSP). The goal of the TSP – to find the shortest possible route that visits each city once and returns to the original city – is simple, but solving the problem is a complex and challenging endeavor. We’ll show you how to do it!

This modeling example is at the advanced level, where we assume that you know Python and the Gurobi Python API and that you have advanced knowledge of building mathematical optimization models. Typically, the objective function and/or constraints of these examples are complex or require advanced features of the Gurobi Python API.

**Download the Repository**

You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip).

**Gurobi License**

In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-RAO-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_traveling-salesman_COM_EVAL_GITHUB_&utm_term=traveling-salesman-problem&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-RAO-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_traveling-salesman_ACADEMIC_EVAL_GITHUB_&utm_term=traveling-salesman-problem&utm_content=C_JPM) as an *academic user*.

## Motivation

The Traveling Salesman Problem (TSP) is one of the most famous combinatorial optimization problems. This problem is very easy to explain, but very complicated to solve – even for instances with a small number of cities. More detailed information on the TSP can be found in the book The Traveling Salesman Problem: A Computational Study [1], or at the TSP home page [2]. If you are interested in the history and mathematical background of the TSP, we recommend that you watch the video by William Cook [3].

The origin of the traveling salesman problem is not very clear; it is mentioned in an 1832 manual for traveling salesman, which included example tours of 45 German cities but was not formulated as a mathematical problem. However, in the 1800s, mathematicians William Rowan Hamilton and Thomas Kirkman devised mathematical formulations of the problem.

It seems that the general form of the Traveling Salesman Problem was first studied by Karl Menger in Vienna and Harvard in the 1930s.

The problem became more and more popular in the 1950s and 1960s. In particular, George Dantzig, D. Ray Fulkerson, and Selmer M. Johnson at the RAND Corporation solved the 48-state problem by formulating it as a linear programming problem. The methods they described in their paper on this topic set the foundation for future work in combinatorial optimization, especially highlighting the importance of cutting planes.

In the early 1970s, the concept of P vs. NP problems created excitement in the theoretical computer science community. In 1972, Richard Karp demonstrated that the Hamiltonian cycle problem was NP-complete, implying that the traveling salesman problem was NP-hard.

Increasingly sophisticated codes led to rapid increases in the sizes of the traveling salesman problems solved. Dantzig, Fulkerson, and Johnson had solved a 48-city instance of the problem in 1954. Martin Grötschel more than doubled this 23 years later, solving a 120-city instance in 1977. Harlan Crowder and Manfred W. Padberg again more than doubled this in just 3 years, with a 318-city solution.

In 1987, rapid improvements were made, culminating in a 2,392-city solution by Padberg and Giovanni Rinaldi. In the following two decades, great strides were made with David L. Applegate, Robert E. Bixby, Vasek Chvátal, & William J. Cook solving a 3,308-city instance in 1992, a 7,397-city instance in 1994, a 24,978-city instance in 2004, and an 85,900-city instance in 2006 – which is the largest 2-D Euclidean TSP instance ever solved. William Cook et. al. wrote a program called Concorde TSP Solver for solving the TSP [4]. Concorde is a computer code for the symmetric TSP and some related network optimization problems. The code is written in the ANSI C programming language and it has been used to obtain the optimal solutions to the full set of 110 TSPLIB instances, the largest instance is a 109,399 node 3-D “star” instance.

The continued interest in the TSP can be explained by its success as a general engine of discovery and a steady stream of new applications. Some of the general applications of the TSP are as follows:
* Scheduling and routing problems.
* Genome sequencing.
* Drilling problems.
* Aiming telescopes and x-rays.
* Data clustering.
* Machine scheduling.

We use this classic combinatorial optimization problem to demonstrate how Gurobi can be used to easily and effectively solve small-sized problem instances of the TSP. However, in order to be able to solve larger instances, one needs more sophisticated techniques – such as those implemented in the Concord TSP Solver.

## Problem Description
The TSP can be defined as follows: for a given list of cities and the distances between each pair of them, we want to find the shortest possible route that goes to each city once and returns to the origin city.

There is a class of Traveling Salesman Problems that assumes that the distance of going from city $i$ to city $j$  is the same as going form city $j$ to city $i$, this type of Traveling Salesman Problem  is also known as the symmetric Traveling Salesman Problem. In this example, we use Euclidean distances, but the TSP model formulation is valid independent of the way in which the individual distances are determined.

## Solution Approach

Mathematical programming is a declarative approach where the modeler formulates a mathematical optimization model that captures the key aspects of a complex decision problem. The Gurobi Optimizer solves such models using state-of-the-art mathematics and computer science.

A mathematical optimization model has five components, namely:

* Sets and indices.
* Parameters.
* Decision variables.
* Objective function(s).
* Constraints.

We now present a MIP formulation of the TSP that identifies the shortest route that goes to all the cities once and returns to the origin city.

## TSP Model Formulation

### Sets and Indices
$i, j \in Capitals $: indices and set of US capital cities.

$\text{Pairings}= \{(i,j) \in Capitals \times Capitals \}$: Set of allowed pairings

$S \subset Capitals$: A subset of the set of US capital cities.

$G = (Capitals, Pairings)$: A graph where the set $Capitals$ defines the set of nodes and the set $Pairings$ defines the set of edges.

### Parameters

$d_{i, j} \in \mathbb{R}^+$: Distance from capital city $i$ to capital city $j$, for all $(i, j) \in Pairings$.

Notice that the distance from capital city $i$ to capital city $j$ is the same as the distance from capital city $j$ to capital city $i$, i.e. $d_{i, j} = d_{j, i}$. For this reason, this TSP is also called the symmetric Traveling Salesman Problem.

### Decision Variables
$x_{i, j} \in \{0, 1\}$: This variable is equal to 1, if we decide to connect city $i$ with city $j$. Otherwise, the decision variable is equal to zero.

### Objective Function
- **Shortest Route**. Minimize the total distance of a route. A route is a sequence of capital cities where the salesperson visits each city only once and returns to the starting capital city.

\begin{equation}
\text{Min} \quad Z = \sum_{(i,j) \in \text{Pairings}}d_{i,j} \cdot x_{i,j}
\tag{0}
\end{equation}

### Constraints
- **Symmetry Constraints**. For each edge $(i,j)$, ensure that the city capitals $i$ and $j$ are connected, if the former is visited immediately before or after visiting the latter.

\begin{equation}
x_{i, j} = x_{j, i} \quad \forall (i, j) \in Pairings
\tag{1}
\end{equation}

- **Entering and leaving a capital city**. For each capital city $i$, ensure that this city is connected to two other cities.

\begin{equation}
\sum_{(i,j) \in \text{Pairings}}x_{i,j} = 2 \quad \forall  i \in Capitals
\tag{2}
\end{equation}

- **Subtour elimination**. These constraints ensure that for any subset of cities $S$ of the set of $Capitals$, there is no cycle. That is, there is no route that visits all the cities in the subset and returns to the origin city.

\begin{equation}
\sum_{(i \neq j) \in S}x_{i,j} \leq |S|-1 \quad \forall  S \subset  Capitals
\tag{3}
\end{equation}

- **Remark**. In general, if the number of cities of the TSP is $n$, then the possible number of routes is n\!.
Since there are an exponential number of constraints ($2^{n} - 2$) to eliminate cycles, we use lazy constraints to dynamically eliminate those cycles.

## Python Implementation

Consider a salesperson that needs to visit customers at each state capital of the continental US. The salesperson wants to identify the shortest route that goes to all the state capitals.

This modeling example requires the following libraries that are not part of the standard Python distribution:
* **folium**: to create maps.
* **gurobipy**: provides Gurobi algorithms to solve MIP models.

### Reading Input Data
The capital names and coordinates are read from a json file.

```python
import json

# Read capital names and coordinates from json file
capitals_json = json.load(open('capitals.json'))
capitals = []
coordinates = {}
for state in capitals_json:
    if state not in ['AK', 'HI']:
      capital = capitals_json[state]['capital']
      capitals.append(capital)
      coordinates[capital] = (float(capitals_json[state]['lat']), float(capitals_json[state]['long']))
```

### Data computation
The following function calculates the distance for each pair of state capitals. Since we are solving the _symmetric_ traveling salesman problem, we use _combinations_ of cities.

```python
import math
from itertools import combinations

# Compute pairwise distance matrix

def distance(city1, city2):
    c1 = coordinates[city1]
    c2 = coordinates[city2]
    diff = (c1[0]-c2[0], c1[1]-c2[1])
    return math.sqrt(diff[0]*diff[0]+diff[1]*diff[1])

dist = {(c1, c2): distance(c1, c2) for c1, c2 in combinations(capitals, 2)}
```

### Model Code
We now write the model for the TSP, by defining decision variables, constraints, and objective function. Because this is the _symmetric_ traveling salesman problem, we can make it more efficient by setting the _object_ x[j,i] to x[i,j], instead of a constraint.

```python
import gurobipy as gp
from gurobipy import GRB

# tested with Python 3.7 & Gurobi 9.0.0

m = gp.Model()

# Variables: is city 'i' adjacent to city 'j' on the tour?
vars = m.addVars(dist.keys(), obj=dist, vtype=GRB.BINARY, name='x')

# Symmetric direction: Copy the object
for i, j in vars.keys():
    vars[j, i] = vars[i, j]  # edge in opposite direction

# Constraints: two edges incident to each city
cons = m.addConstrs(vars.sum(c, '*') == 2 for c in capitals)
```

### Callback Definition
Subtour constraints prevent multiple loops in a TSP tour. Because there are an exponential number of these constraints, we don't want to add them all to the model. Instead, we use a callback function to find violated subtour constraints and add them to the model as lazy constraints.

```python
# Callback - use lazy constraints to eliminate sub-tours

def subtourelim(model, where):
    if where == GRB.Callback.MIPSOL:
        # make a list of edges selected in the solution
        vals = model.cbGetSolution(model._vars)
        selected = gp.tuplelist((i, j) for i, j in model._vars.keys()
                             if vals[i, j] > 0.5)
        # find the shortest cycle in the selected edge list
        tour = subtour(selected)
        if len(tour) < len(capitals):
            # add subtour elimination constr. for every pair of cities in subtour
            model.cbLazy(gp.quicksum(model._vars[i, j] for i, j in combinations(tour, 2))
                         <= len(tour)-1)

# Given a tuplelist of edges, find the shortest subtour

def subtour(edges):
    unvisited = capitals[:]
    cycle = capitals[:] # Dummy - guaranteed to be replaced
    while unvisited:  # true if list is non-empty
        thiscycle = []
        neighbors = unvisited
        while neighbors:
            current = neighbors[0]
            thiscycle.append(current)
            unvisited.remove(current)
            neighbors = [j for i, j in edges.select(current, '*')
                         if j in unvisited]
        if len(thiscycle) <= len(cycle):
            cycle = thiscycle # New shortest subtour
    return cycle
```

## Solve the model

```python
m._vars = vars
m.Params.lazyConstraints = 1
m.optimize(subtourelim)
```

## Analysis

We retrieve the optimal solution of the TSP and verify that the optimal route (or tour) goes to all the cities and returns to the origin city.

```python
# Retrieve solution

vals = m.getAttr('x', vars)
selected = gp.tuplelist((i, j) for i, j in vals.keys() if vals[i, j] > 0.5)

tour = subtour(selected)
assert len(tour) == len(capitals)
```

The optimal route is displayed in the following map.

```python
# Map the solution

import folium

map = folium.Map(location=[40,-95], zoom_start = 4)

points = []
for city in tour:
  points.append(coordinates[city])
points.append(points[0])

folium.PolyLine(points).add_to(map)

map
```

## Conclusions

The Traveling Salesman Problem (TSP) is the most popular combinatorial optimization problem. This problem is very easy to explain, although it is very complicated to solve. The largest TSP problem solved has 85,900 cities. The TSP is a source of discovery for new approaches to solve complex combinatorial optimization problems and has led to many applications.

In this modeling example, we have shown how to formulate the symmetric Traveling Salesman Problem as a MIP problem. We also showed how to dynamically eliminate subtours by using lazy constraints.

## References

[1] D. L. Applegate, R. E. Bixby, V. Chvatal and W. J. Cook , The Traveling Salesman Problem: A Computational Study, Princeton University Press, Princeton, 2006.

[2] http://www.math.uwaterloo.ca/tsp/index.html

[3] https://www.youtube.com/watch?v=q8nQTNvCrjE&t=35s

[4] http://www.math.uwaterloo.ca/tsp/concorde.html

Copyright © 2020 Gurobi Optimization, LLC


```python
# Header starts here.
from sympy.physics.units import *
from sympy import *

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

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""

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

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

half = S(1)/2

# Header ends here.
#
def k(phi):
    """ computes element stiffness matrix wrt global CSYS """
    # phi is angle between:
    # vector along global x axis and
    # vector along 1-2-axis of rod.
    # counted positively about z
    (c, s) = ( cos(phi), sin(phi) )
    (cc, ss, sc) = ( c*c, s*s, s*c)
    return Matrix(
        [
        [ cc,  sc, -cc, -sc],
        [ sc,  ss, -sc, -ss],
        [-cc, -sc,  cc,  sc],
        [-sc, -ss,  sc,  ss],
        ])

def assemble(K,k, i,j):
    I, J = (i-1)*2, (j-1)*2
    K[I:I+2, I:I+2]+=k[0:2,0:2]
    K[I:I+2, J:J+2]+=k[0:2,2:4]
    K[J:J+2, I:I+2]+=k[2:4,0:2]
    K[J:J+2, J:J+2]+=k[2:4,2:4]
    return K

F, l, E, A = var("F, l, E, A")
EA = E*A
r = sqrt(2)
sub_list = [
    ( F,        5 *kN     ),
    ( E,      200 *GPa    ),
    ( A,       25 *mm**2  ),
    ( l,     1707 *mm     ),
    ]

# Element stiffness matrices:
p1, p2, p3 = 45 *deg, 90 *deg, 135 *deg
l1, l2, l3 = r*l, l, r*l

k1 = EA/l1 * k(p1)
# k2 see below
k3 = EA/l3 * k(p3)

symm = True
# symm = False

if not symm:
    k2 = EA/l2 * k(p2)
    K1, K2, K3 = zeros(8,8), zeros(8,8), zeros(8,8)
    K1 = assemble(K1,k1,1,4)
    K2 = assemble(K2,k2,2,4)
    K3 = assemble(K3,k3,3,4)

    pprint("\nK1 / (EA / √2 l):")
    pprint( K1 / (EA/(r*l)) )
    pprint("\nK2 / (EA / √2 l):")
    pprint( K2 / (EA/(r*l)) )
    pprint("\nK3 / (EA / √2 l):")
    pprint( K3 / (EA/(r*l)) )

    # System stiffness matrix:
    pprint("\nK / (EA / √2 l):")
    K = K1 + K2 + K3
    tmp = K
    tmp /= EA / (r*l)
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)

    u1x,u1y,u2x,u2y,u3x,u3y,u4x,u4y = var('u1x,u1y,u2x,u2y,u3x,u3y,u4x,u4y')
    F1x,F1y,F2x,F2y,F3x,F3y,F4x,F4y = var("F1x,F1y,F2x,F2y,F3x,F3y,F4x,F4y")

    u = Matrix([ 0 , 0 , 0 , 0 , 0 , 0 ,u4x,u4y])
    f = Matrix([F1x,F1y,F2x,F2y,F3x,F3y, 0 ,-F ])

    unks = [u4x,u4y,F1x,F1y,F2x,F2y,F3x,F3y]

    eq = Eq(K*u, f)
    sol = solve(eq,unks)

    u4x,u4y,F1x,F1y,F2x,F2y,F3x,F3y = \
        sol[u4x],sol[u4y],sol[F1x],sol[F1y],sol[F2x],sol[F2y],sol[F3x],sol[F3y]

    pprint("\n(u4x, u4y) / (Fl / EA):")
    tmp = Matrix([u4x, u4y])
    tmp /= F*l/EA
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)
    pprint("\nu4y / mm:")
    tmp = u4y
    tmp = tmp.subs(sub_list)
    tmp /= mm
    tmp = iso_round(tmp,0.0001)
    pprint(tmp)

    pprint("\n(F2x, F2y) / F:")
    tmp = Matrix([F2x, F2y])
    tmp /= F
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)
    pprint("\n(F2x, F2y) / N:")
    tmp = Matrix([F2x, F2y])
    tmp = tmp.subs(sub_list)
    tmp /= Newton
    tmp = iso_round(tmp, 0.1)
    pprint(tmp)
    # K1 / (EA / √2 l):
    # ⎡1/2   1/2   0  0  0  0  -1/2  -1/2⎤
    # ⎢                                  ⎥
    # ⎢1/2   1/2   0  0  0  0  -1/2  -1/2⎥
    # ⎢                                  ⎥
    # ⎢ 0     0    0  0  0  0   0     0  ⎥
    # ⎢                                  ⎥
    # ⎢ 0     0    0  0  0  0   0     0  ⎥
    # ⎢                                  ⎥
    # ⎢ 0     0    0  0  0  0   0     0  ⎥
    # ⎢                                  ⎥
    # ⎢ 0     0    0  0  0  0   0     0  ⎥
    # ⎢                                  ⎥
    # ⎢-1/2  -1/2  0  0  0  0  1/2   1/2 ⎥
    # ⎢                                  ⎥
    # ⎣-1/2  -1/2  0  0  0  0  1/2   1/2 ⎦
    #
    # K2 / (EA / √2 l):
    # ⎡0  0  0   0   0  0  0   0 ⎤
    # ⎢                          ⎥
    # ⎢0  0  0   0   0  0  0   0 ⎥
    # ⎢                          ⎥
    # ⎢0  0  0   0   0  0  0   0 ⎥
    # ⎢                          ⎥
    # ⎢0  0  0  √2   0  0  0  -√2⎥
    # ⎢                          ⎥
    # ⎢0  0  0   0   0  0  0   0 ⎥
    # ⎢                          ⎥
    # ⎢0  0  0   0   0  0  0   0 ⎥
    # ⎢                          ⎥
    # ⎢0  0  0   0   0  0  0   0 ⎥
    # ⎢                          ⎥
    # ⎣0  0  0  -√2  0  0  0  √2 ⎦
    #
    # K3 / (EA / √2 l):
    # ⎡0  0  0  0   0     0     0     0  ⎤
    # ⎢                                  ⎥
    # ⎢0  0  0  0   0     0     0     0  ⎥
    # ⎢                                  ⎥
    # ⎢0  0  0  0   0     0     0     0  ⎥
    # ⎢                                  ⎥
    # ⎢0  0  0  0   0     0     0     0  ⎥
    # ⎢                                  ⎥
    # ⎢0  0  0  0  1/2   -1/2  -1/2  1/2 ⎥
    # ⎢                                  ⎥
    # ⎢0  0  0  0  -1/2  1/2   1/2   -1/2⎥
    # ⎢                                  ⎥
    # ⎢0  0  0  0  -1/2  1/2   1/2   -1/2⎥
    # ⎢                                  ⎥
    # ⎣0  0  0  0  1/2   -1/2  -1/2  1/2 ⎦
    #
    # K / (EA / √2 l):
    # ⎡1/2   1/2   0   0    0     0    -1/2   -1/2 ⎤
    # ⎢                                            ⎥
    # ⎢1/2   1/2   0   0    0     0    -1/2   -1/2 ⎥
    # ⎢                                            ⎥
    # ⎢ 0     0    0   0    0     0     0      0   ⎥
    # ⎢                                            ⎥
    # ⎢ 0     0    0  √2    0     0     0     -√2  ⎥
    # ⎢                                            ⎥
    # ⎢ 0     0    0   0   1/2   -1/2  -1/2   1/2  ⎥
    # ⎢                                            ⎥
    # ⎢ 0     0    0   0   -1/2  1/2   1/2    -1/2 ⎥
    # ⎢                                            ⎥
    # ⎢-1/2  -1/2  0   0   -1/2  1/2    1      0   ⎥
    # ⎢                                            ⎥
    # ⎣-1/2  -1/2  0  -√2  1/2   -1/2   0    1 + √2⎦
    #
    # (u4x, u4y) / (Fl / EA):
    # ⎡   0   ⎤
    # ⎢       ⎥
    # ⎣-2 + √2⎦
    #
    # u4y / mm:
    # -0.9999
    #
    # (F2x, F2y) / F:
    # ⎡   0   ⎤
    # ⎢       ⎥
    # ⎣-√2 + 2⎦
    #
    # (F2x, F2y) / N:
    # ⎡ 0.0  ⎤
    # ⎢      ⎥
    # ⎣2928.9⎦

elif symm:
    k2 = EA/2/l2 * k(p2)
    K1, K2 = zeros(6,6), zeros(6,6)
    K1 = assemble(K1,k1,1,3) # Node 4 will third node out of three
    K2 = assemble(K2,k2,2,3) # Node 4 will be in last two lines of 6x6-system matrix

    pprint("\nK1 / (EA / √2 l):")
    tmp = K1 / (EA/(r*l))
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)
    pprint("\nK2 / (EA / √2 l):")
    tmp = K2 / (EA/(r*l))
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)

    # System stiffness matrix:
    pprint("\nK / (EA / √2 l):")
    K = K1 + K2
    tmp = K
    tmp /= EA / (r*l)
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)

    u1x,u1y, u2x, u2y,u4x,u4y = var('u1x,u1y, u2x, u2y,u4x,u4y')
    F1x,F1y,F2px,F2py,F4x,F4y = var("F1x,F1y,F2px,F2py,F4x,F4y")

    u = Matrix([ 0 , 0 , 0  ,  0 , 0  , u4y])
    f = Matrix([F1x,F1y,F2px,F2py,F4x ,-F/2])

    unks = [u4y,F1x,F1y,F2px,F2py,F4x]

    eq = Eq(K*u, f)
    sol = solve(eq,unks)
    u4y,F1x,F1y,F2px,F2py,F4x = \
        sol[u4y],sol[F1x],sol[F1y],sol[F2px],sol[F2py],sol[F4x]

    pprint("\nu4y / (Fl / EA):")
    tmp = u4y
    tmp /= F*l/EA
    tmp = tmp.simplify()
    pprint(tmp)

    pprint("\n(F2'x, F2'y) / F:")
    tmp = Matrix([F2px, F2py])
    tmp /= F
    tmp = tmp.applyfunc(simplify)
    pprint(tmp)

    # K1 / (EA / √2 l):
    # ⎡1/2   1/2   0  0  -1/2  -1/2⎤
    # ⎢                            ⎥
    # ⎢1/2   1/2   0  0  -1/2  -1/2⎥
    # ⎢                            ⎥
    # ⎢ 0     0    0  0   0     0  ⎥
    # ⎢                            ⎥
    # ⎢ 0     0    0  0   0     0  ⎥
    # ⎢                            ⎥
    # ⎢-1/2  -1/2  0  0  1/2   1/2 ⎥
    # ⎢                            ⎥
    # ⎣-1/2  -1/2  0  0  1/2   1/2 ⎦
    #
    # K2 / (EA / √2 l):
    # ⎡0  0  0   0    0   0  ⎤
    # ⎢                      ⎥
    # ⎢0  0  0   0    0   0  ⎥
    # ⎢                      ⎥
    # ⎢0  0  0   0    0   0  ⎥
    # ⎢                      ⎥
    # ⎢          √2      -√2 ⎥
    # ⎢0  0  0   ──   0  ────⎥
    # ⎢          2        2  ⎥
    # ⎢                      ⎥
    # ⎢0  0  0   0    0   0  ⎥
    # ⎢                      ⎥
    # ⎢         -√2       √2 ⎥
    # ⎢0  0  0  ────  0   ── ⎥
    # ⎣          2        2  ⎦
    #
    # K / (EA / √2 l):
    # ⎡1/2   1/2   0   0    -1/2   -1/2 ⎤
    # ⎢                                 ⎥
    # ⎢1/2   1/2   0   0    -1/2   -1/2 ⎥
    # ⎢                                 ⎥
    # ⎢ 0     0    0   0     0      0   ⎥
    # ⎢                                 ⎥
    # ⎢                √2          -√2  ⎥
    # ⎢ 0     0    0   ──    0     ──── ⎥
    # ⎢                2            2   ⎥
    # ⎢                                 ⎥
    # ⎢-1/2  -1/2  0   0    1/2    1/2  ⎥
    # ⎢                                 ⎥
    # ⎢               -√2         1   √2⎥
    # ⎢-1/2  -1/2  0  ────  1/2   ─ + ──⎥
    # ⎣                2          2   2 ⎦
    #
    # u4y / (Fl / EA):
    # -2 + √2
    #
    # (F2'x, F2'y) / F:
    # ⎡   0    ⎤
    # ⎢        ⎥
    # ⎢  √2    ⎥
    # ⎢- ── + 1⎥
    # ⎣  2     ⎦

```