```python
!pip install econml
```

    Collecting econml
      Downloading https://files.pythonhosted.org/packages/fe/4d/a94c40f3af6fabc3160d4be7378913dcb652fe8a930e3f9ae523934aab2f/econml-0.10.0-cp37-cp37m-manylinux2010_x86_64.whl (3.0MB)
         |████████████████████████████████| 3.0MB 6.6MB/s
    Collecting dowhy
      Downloading https://files.pythonhosted.org/packages/c4/e0/c1480994d39eaa66faa7d4106a4daea7b345dcc31ad83196460c2676701c/dowhy-0.6-py3-none-any.whl (123kB)
         |████████████████████████████████| 133kB 37.4MB/s
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from econml) (1.19.5)
    Collecting shap~=0.38.1
      Downloading https://files.pythonhosted.org/packages/44/20/54381999efe3000f70a7f68af79ba857cfa3f82278ab0e02e6ba1c06b002/shap-0.38.1.tar.gz (352kB)
         |████████████████████████████████| 358kB 30.0MB/s
    Collecting sparse
      Downloading https://files.pythonhosted.org/packages/46/20/c409a0d0ea7623a936080e0038eb77a02e62629a07944706c26f24ebcbb8/sparse-0.12.0-py2.py3-none-any.whl (76kB)
         |████████████████████████████████| 81kB 7.9MB/s
    Requirement already satisfied: numba!=0.42.1 in /usr/local/lib/python3.7/dist-packages (from econml) (0.51.2)
    Requirement already satisfied: statsmodels>=0.9 in /usr/local/lib/python3.7/dist-packages (from econml) (0.10.2)
    Requirement already satisfied: scikit-learn>0.22.0 in /usr/local/lib/python3.7/dist-packages (from econml) (0.22.2.post1)
    Requirement already satisfied: scipy>1.4.0 in /usr/local/lib/python3.7/dist-packages (from econml) (1.4.1)
    Requirement already satisfied: graphviz in /usr/local/lib/python3.7/dist-packages (from econml) (0.10.1)
    Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from econml) (1.1.5)
    Requirement already satisfied: joblib>=0.13.0 in /usr/local/lib/python3.7/dist-packages (from econml) (1.0.1)
    Requirement already satisfied: sympy>=1.4 in /usr/local/lib/python3.7/dist-packages (from dowhy->econml) (1.7.1)
    Collecting pydot>=1.4
      Downloading https://files.pythonhosted.org/packages/ea/76/75b1bb82e9bad3e3d656556eaa353d8cd17c4254393b08ec9786ac8ed273/pydot-1.4.2-py2.py3-none-any.whl
    Requirement already satisfied: networkx>=2.0 in /usr/local/lib/python3.7/dist-packages (from dowhy->econml) (2.5.1)
    Requirement already satisfied: tqdm>4.25.0 in /usr/local/lib/python3.7/dist-packages (from shap~=0.38.1->econml) (4.41.1)
    Collecting slicer==0.0.7
      Downloading https://files.pythonhosted.org/packages/78/c2/b3f55dfdb8af9812fdb9baf70cacf3b9e82e505b2bd4324d588888b81202/slicer-0.0.7-py3-none-any.whl
    Requirement already satisfied: cloudpickle in /usr/local/lib/python3.7/dist-packages (from shap~=0.38.1->econml) (1.3.0)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from numba!=0.42.1->econml) (56.0.0)
    Requirement already satisfied: llvmlite<0.35,>=0.34.0.dev0 in /usr/local/lib/python3.7/dist-packages (from numba!=0.42.1->econml) (0.34.0)
    Requirement already satisfied: patsy>=0.4.0 in /usr/local/lib/python3.7/dist-packages (from statsmodels>=0.9->econml) (0.5.1)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas->econml) (2018.9)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas->econml) (2.8.1)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.4->dowhy->econml) (1.2.1)
    Requirement already satisfied: pyparsing>=2.1.4 in /usr/local/lib/python3.7/dist-packages (from pydot>=1.4->dowhy->econml) (2.4.7)
    Requirement already satisfied: decorator<5,>=4.3 in /usr/local/lib/python3.7/dist-packages (from networkx>=2.0->dowhy->econml) (4.4.2)
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from patsy>=0.4.0->statsmodels>=0.9->econml) (1.15.0)
    Building wheels for collected packages: shap
      Building wheel for shap (setup.py) ... done
      Created wheel for shap: filename=shap-0.38.1-cp37-cp37m-linux_x86_64.whl size=490498 sha256=d13ab7c39debe73384eb735766167906abeace88737bd4415955f661167a8435
      Stored in directory: /root/.cache/pip/wheels/a8/fb/e4/88012be41842b9be62ae18d82d1b1e880daf8539d1fef1fa00
    Successfully built shap
    Installing collected packages: pydot, dowhy, slicer, shap, sparse, econml
      Found existing installation: pydot 1.3.0
        Uninstalling pydot-1.3.0:
          Successfully uninstalled pydot-1.3.0
    Successfully installed dowhy-0.6 econml-0.10.0 pydot-1.4.2 shap-0.38.1 slicer-0.0.7 sparse-0.12.0

```python
from econml.policy import PolicyTree
from econml.cate_interpreter import SingleTreePolicyInterpreter
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
```

    /usr/local/lib/python3.7/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
      import pandas.util.testing as tm

```python
Q = pd.read_csv("CATE average.csv", index_col=0)
```

```python
Q
```

      job search
      vocational
      computer
      language

      1
      -1.280430
      2.556203
      1.482988
      0.885377

      2
      -1.171464
      2.273266
      2.403585
      2.758233

      3
      -1.589187
      2.933885
      2.899676
      2.048820

      4
      -1.285102
      2.904400
      4.035316
      1.410639

      5
      -1.804006
      2.012930
      4.175508
      0.424482

      ...
      ...
      ...
      ...
      ...

      62557
      -1.754338
      2.213183
      2.095295
      3.119136

      62558
      -2.180696
      2.489197
      2.776620
      2.639378

      62559
      -0.992194
      3.140326
      4.095090
      1.963767

      62560
      -1.407170
      3.003563
      2.470632
      3.149166

      62561
      -0.661846
      2.912520
      1.920541
      0.546992

62561 rows × 4 columns

```python
Q['nontr'] = 0
Q = Q[["nontr", "job search", "vocational", "computer", "language"]]
```

```python
X = pd.read_csv("x_clean.csv", index_col=0)
```

```python
X = X[["age", "female", "employability", "past_income", "gdp_pc", "unemp_rate"]]
```

```python
policy = PolicyTree(honest=True, max_depth=6, random_state=0).fit(X, Q)
```

```python
policy.feature_importances_
```

    array([ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
            0.00000000e+00,  8.60450514e-04,  0.00000000e+00,  8.78323978e-17,
            0.00000000e+00,  0.00000000e+00,  5.48952486e-17, -2.63497193e-14,
            0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
            4.49658380e-03,  9.21500148e-01,  0.00000000e+00,  0.00000000e+00,
            2.53825944e-02,  0.00000000e+00,  4.67163211e-02,  1.04390229e-03,
            0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
            0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
            0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
            0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
           -2.19580995e-18,  0.00000000e+00])

```python
plt.figure(figsize=(25,10))
policy.plot(treatment_names=['nontr', 'jobsh', 'vocat', 'compu', 'langu'])
plt.show()
plt.savefig("policy tree.png")
```

```python
policy.predict([[0, 0, 1, 5, 2, 0]])
```

    array([1])

```python
Q.mean()
```

    nontr         0.000000
    job search   -1.039687
    vocational    2.751877
    computer      3.152447
    language      1.664231
    dtype: float64

```python
for i in range(len(X.columns)):
  print(i, X.columns[i])
```

    0 age
    1 female
    2 employability
    3 past_income
    4 gdp_pc
    5 unemp_rate

```python

```


```python
# This cell is mandatory in all Dymos documentation notebooks.
missing_packages = []
try:
    import openmdao.api as om
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !python -m pip install openmdao[notebooks]
    else:
        missing_packages.append('openmdao')
try:
    import dymos as dm
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !python -m pip install dymos
    else:
        missing_packages.append('dymos')
try:
    import pyoptsparse
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !pip install -q condacolab
        import condacolab
        condacolab.install_miniconda()
        !conda install -c conda-forge pyoptsparse
    else:
        missing_packages.append('pyoptsparse')
if missing_packages:
    raise EnvironmentError('This notebook requires the following packages '
                           'please install them and restart this notebook\'s runtime: {",".join(missing_packages)}')
```

(getting_started:collocation:what_is_collocation)=
# What is collocation?

Dymos is focused on direct optimization methods based on an implicit integration technique, collocation.

The _state_ of a dynamic system defines its configuration at a point in time.
Common state variables for dynamic systems are position, velocity, and mass.
Given an initial state, a time duration, and ordinary differential equations that define the evolution of the states in time, the state of the system at the end of the time duration can be determined.
This is known as the _initial value problem_ (IVP) and there are numerous techniques and software packages that can be used to solve the IVP.
The initial value problem is typically the basis for the simulation of systems.
This time-marching approach is the basis for _shooting methods_ in optimization.
While Dymos supports shooting methods, its focus is on implicit collocation techniques, for performance reasons.

First, a few assumptions about the dynamic system:

1.  Over portions of the trajectory, the state is continuous (there are no instantaneous changes in the states)
2.  Over portions of the trajectory, the state is continuous in its first derivative w.r.t. time. (there are no instantaneous changes in the state rates)

If those two conditions are met, then it's reasonable to fit a polynomial to a plot of the state value over time.
Given the initial or final value of a state, and values of its rate at some number of points across the trajectory, then a polynomial can be fit whose initial or final value matches, and whose rates match the sampled rates.
The value of the state at any point throughout the trajectory can then be inferred by interpolating the collocated polynomial.

Solving this trajectory implicitly basically means _guessing_ values for the states along the trajectory.
While one could use a power series polynomial coefficients ($a t^2 + b t + c$), these coefficients can have vastly different values depending on the shape of the curve, making them difficult to scale numerically.
Instead, Lagrange interpolating polynomials are typically used, where the actual values to be interpolated at various points are the implicit variables.
This makes scaling the problem easier, and gives the implicit variables more intuitive meaning, making it easier to provide an initial guess.

## Example:  Simulating the fall of an object

As a "hello, world" problem for Dymos, consider the vertical fall of an object acting under gravity - like Galileo dropping cannonballs from the Tower of Pisa.
Ignoring aerodynamics and treating the ball as a point mass in a rectilinear gravity field, the motion is governed by:

\begin{align}
    \ddot{y} = -g
\end{align}

The techniques in Dymos are generally centered around first-order dynamics, so we convert the above equation into the following system of ODEs.

\begin{align}
    \dot{y} &= v_y \\
    \dot{v_y} &= -g
\end{align}

One common way to propagate this trajectory would be to use an "explicit time-marching" approach.
That is, we can use Euler's method or a Runge-Kutta approach to propagate the trajectory of the ball, one small slice of time at a time.
When performing trajectory optimization, the trajectory is typically evaluated at least dozens of time.
If the ODE for the system is computationally expensive, this time-marching approach can take a very long time.

Instead, we can use an implicit simulation technique to more efficiently model the trajectory.
First, let's assume that the value of the _states_ $y$ and $v_y$ smoothly vary in time (there's no teleportation or instantaneous changes in velocity).
Now we can fit a polynomial to the plot of each of these states with respect to time.
In this case, basic physics tells us that $v_y$ should be linear with time and $y$ should be quadratic with time.

Now for the simulation, lets solve a _boundary value problem_.
We know the initial and final height of the ball.
We know the initial velocity but not the terminal velocity.

_How long does it take the ball to reach the ground from an initial height of 100 m?_

We will explain how this problem is solved using the two implicit simulation techniques in Dymos:  high-order Gauss-Lobatto collocation, and the Radau Pseudospectral Method.

## Finding the trajectory using high-order Legendre-Gauss-Lobatto collocation

Using a 3rd-order polynomial segment to mimic the trajectory, we have two states and thus two polynomials.
Legendre-Gauss-Lobatto (LGL) collocation uses knowledge of the states and state-rates to form an interpolating polynomial.
A third-order polynomial requires four pieces of information to define it.
We'll take two points along the trajectory, and use those to guess the state values.
In Dymos, these points where state values are provided are called the _state discretization nodes_.
In LGL collocation, these points are chosen to be the LGL _nodes_ in the dimensionless time of the segment, which we call $\tau_s$.

Our initial guess for the trajectory might look something like this:

| Variables  | Description       | Value          | Notes                  |
|------------|-------------------|----------------|------------------------|
| $y_0$      | initial height    | 100 m          | fixed                  |
| $y_f$      | final height      | 0 m            | fixed                  |
| $v_{y0}$   | initial velocity  | 0 m/s          | fixed                  |
| $v_{yf}$   | final velocity    | -50 m/s        | free                   |
| $t_0$      | initial time      | 0              | fixed                  |
| $t_d$      | time duration     | 5              | free                   |
| $g$        | grav. acceleration| 9.80665 m/s^2  | assumed                |

To form 3rd-order polynomials for the states we need four pieces of information, but so far we only have the two endpoint values.
We'll use the state rates at the state discretization nodes to provide the other two values needed for interpolation.
To obtain these, we evaluate our ODE at the state discretization nodes.

\begin{align}
    \dot{y}_0 &= 0 \\
    \dot{y}_f &= -50 \\
    \dot{v}_{y0} &= -9.80665 \\
    \dot{v}_{yf} &= -9.80665
\end{align}

But how do we know that our assumed trajectory is correct?
In LGL collocation, we check the interpolated slope of the polynomial at a 3rd point (the collocation node) and compare it to an evaluation of the ODE at the same point.
If the difference between the two is sufficiently close to zero, we can be somewhat confident that our polynomial is a reasonable match for the true time-history.
We call this difference in slope the collocation _defect_.

Having computed the ODE at the state discretization nodes, we can form interpolants for the values and the slope of the states at the collocation nodes.
Dymos uses Lagrange interpolants, such that the inputs to the system are the values of the states and controls, rather than less intuitive coefficients.
In this case, the state values at our single collocation node are:

\begin{align}
    y_m &= 50 \\
    v_{ym} &= -25
\end{align}

The polynomial slopes at the collocation node are:

\begin{align}
    y'_m &= 50 \\
    v'_{ym} &= -25
\end{align}

Now, having the state and time values at the collocation nodes, we can again evaluate the ODE.

\begin{align}
    \dot{y}_m &= -17 \\
    \dot{v}_{ym} &= -9.80665
\end{align}

In this case the known variables are the value of the polynomial at the left endpoint ($y_0$), and the value of the independent variable at the left and right endpoints of the interval to be fitted ($x_0$, $x_1$)
In this case, there are no _control_ inputs to the ODE.
However, if there were, the value of the control would also need to be provided at the collocation node.
Thus, in high-order Gauss-Lobatto collocation, the _control input nodes_ include both the state discretization nodes and the collocation nodes.

The free variables in the problem ($t_{duration}$ and $vy_f$) can be iterated until the difference between the interpolated state rates and the ODE-computed state rates is zero.

## Finding the trajectory using the Radau Pseudospectral Method

Again, there are two states, and two corresponding 3rd-order polynomials representing the time history.
As with the Gauss-Lobatto method, a third-order polynomial requires four pieces of information to define it.
In the case of the Radau Pseudospectral Method, we'll use four state values, guessed at the Radau nodes in dimensionless time in addition to the right endpoint of the interval (the _state discretization nodes_).

A linear interpolation of the endpoint values will serve as the initial guess in this instance.
The three Radau nodes that don't include the right endpoint will serve as the _collocation nodes_ in this case.
Since these are a subset of the state discretization nodes, no interpolation of values is necessary...all of the state values have been provided.

The derivative of the interpolating polynomial is computed using a differentiation matrix and the state discretization values.

Now the ODE can be evaluated to calculate the state rates, and the results compared to the interpolated slopes.

Again, the free variables are iterated until the defects are zero and any boundary conditions are met.

## Multiple Segments

The simple examples above use one single 3rd-order polynomial to represent the time history of the states.
With more complex states, a single 3rd-order polynomial would not be capable of replicating the time history.
In practice, we can use more polynomial segments _and_ higher-order polynomials to better match the dynamics.

In the case of adding more segments, one can choose whether the segment boundaries are shared by the segments, or independent.
If they are shared, fewer design variables are necessary, but it may be more difficult for the optimizer to search for a solution.
This is called "compressed" transcription.
On the other hand, we can doubly-specify state values at segment boundaries as design variables, thus increasing the size of the problem.
In this "uncompressed" formulation, continuity constraints are imposed at the segment bounds to ensure that there are no discontinuities in the state time-history at segment bounds.

## LGL vs LGR Collocation

Gauss-Lobatto collocation results in smaller NLP sizes for the optimizer since fewer states are treated as design variables.
The cost of this reduction is the interpolation step in the Gauss-Lobatto algorithm.
While the Radau Pseudospectral method requires one evaluation of the ODE to assess the defects, the Gauss-Lobatto method requires two.
In addition, the interpolation step used by the Gauss-Lobatto method can result in interpolated state values falling well outside the user's expected range if the initial guess is not sufficiently accurate.
This can cause convergence issues if there are nonlinear solvers within the ODE that rely on a reasonable guess to achieve convergence.

## Satisfying Defects with a Nonlinear Solver

Typically, collocation problems are solved by posing the defects as constraints to an optimizer.
In Dymos, another option exists.
One can use an embedded Newton-solver satisfy the defect and continuity constraints.
This mode, which we call `solve_segments`, gives the Newton-Solver control over all but the first or final state value in the phase.
The optimizer only controls the initial or final value, and the solver is responsible for converging the trajectory time history.
This results in a shooting method, but one that is mathematically consistent with the collocation methods.
This `solve_segments` capability is useful if a user simply wants to propagate a dynamic system and not optimize it.

```python

```


```
!pip install cirq
```

    Collecting cirq
      Downloading https://files.pythonhosted.org/packages/53/29/a66c4c28306dae359745e37c4c10120e477da44cb050d06d8ceb1117a22a/cirq-0.7.0-py3-none-any.whl (1.2MB)
         |████████████████████████████████| 1.2MB 2.8MB/s
    Requirement already satisfied: dataclasses; python_version < "3.7" in /usr/local/lib/python3.6/dist-packages (from cirq) (0.7)
    Collecting protobuf==3.8.0
      Downloading https://files.pythonhosted.org/packages/d2/fb/29de8d08967f0cce1bb10b39846d836b0f3bf6776ddc36aed7c73498ca7e/protobuf-3.8.0-cp36-cp36m-manylinux1_x86_64.whl (1.2MB)
         |████████████████████████████████| 1.2MB 47.3MB/s
    Requirement already satisfied: scipy in /usr/local/lib/python3.6/dist-packages (from cirq) (1.4.1)
    Requirement already satisfied: numpy~=1.16 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.17.5)
    Collecting sympy==1.4
      Downloading https://files.pythonhosted.org/packages/21/21/f4105795ca7f35c541d82c5b06be684dd2f5cb4f508fb487cd7aea4de776/sympy-1.4-py2.py3-none-any.whl (5.3MB)
         |████████████████████████████████| 5.3MB 35.4MB/s
    Collecting networkx==2.3
      Downloading https://files.pythonhosted.org/packages/85/08/f20aef11d4c343b557e5de6b9548761811eb16e438cee3d32b1c66c8566b/networkx-2.3.zip (1.7MB)
         |████████████████████████████████| 1.8MB 29.4MB/s
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.6/dist-packages (from cirq) (3.6.6)
    Requirement already satisfied: sortedcontainers~=2.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.1.0)
    Requirement already satisfied: pandas in /usr/local/lib/python3.6/dist-packages (from cirq) (0.25.3)
    Requirement already satisfied: requests~=2.18 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.21.0)
    Requirement already satisfied: matplotlib~=3.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (3.1.2)
    Requirement already satisfied: google-api-python-client~=1.6 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.7.11)
    Requirement already satisfied: six>=1.9 in /usr/local/lib/python3.6/dist-packages (from protobuf==3.8.0->cirq) (1.12.0)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.6/dist-packages (from protobuf==3.8.0->cirq) (45.1.0)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.6/dist-packages (from sympy==1.4->cirq) (1.1.0)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.6/dist-packages (from networkx==2.3->cirq) (4.4.1)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas->cirq) (2018.9)
    Requirement already satisfied: python-dateutil>=2.6.1 in /usr/local/lib/python3.6/dist-packages (from pandas->cirq) (2.6.1)
    Requirement already satisfied: urllib3<1.25,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (1.24.3)
    Requirement already satisfied: idna<2.9,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (2.8)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (2019.11.28)
    Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (3.0.4)
    Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (1.1.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (2.4.6)
    Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (0.10.0)
    Requirement already satisfied: google-auth>=1.4.1 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (1.4.2)
    Requirement already satisfied: google-auth-httplib2>=0.0.3 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (0.0.3)
    Requirement already satisfied: uritemplate<4dev,>=3.0.0 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (3.0.1)
    Requirement already satisfied: httplib2<1dev,>=0.9.2 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (0.11.3)
    Requirement already satisfied: rsa>=3.1.4 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (4.0)
    Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (0.2.8)
    Requirement already satisfied: cachetools>=2.0.0 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (4.0.0)
    Requirement already satisfied: pyasn1>=0.1.3 in /usr/local/lib/python3.6/dist-packages (from rsa>=3.1.4->google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (0.4.8)
    Building wheels for collected packages: networkx
      Building wheel for networkx (setup.py) ... done
      Created wheel for networkx: filename=networkx-2.3-py2.py3-none-any.whl size=1556408 sha256=3f0f6bb9e78ac09bf8f86ac6f7e20b27b4042418c81c816c176fbdad8884f444
      Stored in directory: /root/.cache/pip/wheels/de/63/64/3699be2a9d0ccdb37c7f16329acf3863fd76eda58c39c737af
    Successfully built networkx
    ERROR: albumentations 0.1.12 has requirement imgaug<0.2.7,>=0.2.5, but you'll have imgaug 0.2.9 which is incompatible.
    Installing collected packages: protobuf, sympy, networkx, cirq
      Found existing installation: protobuf 3.10.0
        Uninstalling protobuf-3.10.0:
          Successfully uninstalled protobuf-3.10.0
      Found existing installation: sympy 1.1.1
        Uninstalling sympy-1.1.1:
          Successfully uninstalled sympy-1.1.1
      Found existing installation: networkx 2.4
        Uninstalling networkx-2.4:
          Successfully uninstalled networkx-2.4
    Successfully installed cirq-0.7.0 networkx-2.3 protobuf-3.8.0 sympy-1.4

```
import cirq
import random
```

```
# Number of input qubits
qubitNo=8
inputQubit=[cirq.GridQubit(i,0) for i in range(qubitNo)]

# Output qubit
outputQubit=cirq.GridQubit(qubitNo,0)

# String S such that f(x)=x.s
factorString=[random.randint(0,1) for _ in range(qubitNo)]

```

```
# define Oracle U_f
def makeOracle(inputQubit, outputQubit, factorString):
  for qubit, bit in zip(inputQubit,factorString):
    if bit:
      yield cirq.CNOT(qubit, outputQubit)
```

```
# define the whole BV algorithm circuit
def makeBVcircuit(inputQubit, outputQubit, Oracle):
  BV=cirq.Circuit()
  BV.append([
  cirq.X(outputQubit),
  cirq.H(outputQubit),
  cirq.H.on_each(*inputQubit)
  ])

  BV.append(Oracle)

  BV.append([
  cirq.H.on_each(*inputQubit),
  cirq.measure(*inputQubit, key='result')
  ])

  return BV
```

```
#make one example of Oracle and BVcircuit
Oracle=makeOracle(inputQubit, outputQubit, factorString)

BVcircuit=makeBVcircuit(inputQubit, outputQubit, Oracle)
print('BVCircuit')
print(BVcircuit)

```

    BVCircuit
    (0, 0): ───H───H───────────────M('result')───
                                   │
    (1, 0): ───H───────@───H───────M─────────────
                       │           │
    (2, 0): ───H───H───┼───────────M─────────────
                       │           │
    (3, 0): ───H───H───┼───────────M─────────────
                       │           │
    (4, 0): ───H───H───┼───────────M─────────────
                       │           │
    (5, 0): ───H───────┼───@───H───M─────────────
                       │   │       │
    (6, 0): ───H───H───┼───┼───────M─────────────
                       │   │       │
    (7, 0): ───H───H───┼───┼───────M─────────────
                       │   │
    (8, 0): ───X───H───X───X─────────────────────

```
#measurement
simulator=cirq.Simulator()
result=simulator.run(BVcircuit)

#comparison measurement result with factorString
print(result)
print('factorString=',factorString)
```

    result=0, 1, 0, 0, 0, 1, 0, 0
    factorString= [0, 1, 0, 0, 0, 1, 0, 0]

```

```


# Select suitable MDL test: KVLCC2 **NO** speed

# Purpose
Find suitable roll decay tests from MDL, to reproduce in Motions later on.

# Methodology
Look at the results from the KLVCC2 ship.
* Load model tests from MDL
* Determine damping

# WIP - improvements
(WORK IN PROGRESS)
Use this section only if the notebook is not final.

Notable TODOs:
* todo 1
* todo 2
* todo 3

## Results
Describe and comment the most important results.

# Suggested next steps
State suggested next steps, based on results obtained in this notebook.

# Setup

```python
# %load imports.py
"""
These is the standard setup for the notebooks.
"""

%matplotlib inline
%load_ext autoreload
%autoreload 2

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

import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)
import numpy as np
import os
import matplotlib.pyplot as plt
from collections import OrderedDict
#plt.style.use('paper')

#import data
import copy
from mdldb.run import Run

from sklearn.pipeline import Pipeline
from rolldecayestimators.transformers import CutTransformer, LowpassFilterDerivatorTransformer, ScaleFactorTransformer, OffsetTransformer
from rolldecayestimators.direct_estimator_cubic import EstimatorQuadraticB, EstimatorCubic
from rolldecayestimators.ikeda_estimator import IkedaQuadraticEstimator
import rolldecayestimators.equations as equations
import rolldecayestimators.lambdas as lambdas
from rolldecayestimators.substitute_dynamic_symbols import lambdify
import rolldecayestimators.symbols as symbols
import sympy as sp

from sklearn.metrics import r2_score
from src.data import database
from mdldb import tables

```

    Duplicate key in file WindowsPath('C:/Users/maa/.matplotlib/stylelib/paper.mplstyle'), line 461 ('figure.figsize   : 5, 3   ## figure size in inches')
    Duplicate key in file WindowsPath('C:/Users/maa/.matplotlib/stylelib/paper.mplstyle'), line 462 ('figure.dpi       : 100        ## figure dots per inch')

```python
from mdldb.hdf5_file import Hdf5File
from rolldecayestimators.direct_estimator_cubic import EstimatorCubic,EstimatorQuadraticB
from sklearn.base import clone
from copy import deepcopy
import joblib
```

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

```python
sql = """
SELECT * from run
WHERE model_number='M5057-01-A' and test_type='roll decay' and project_number=40178362;
"""
df_rolldecays = pd.read_sql(sql=sql, con=db.engine, index_col='id')

df_rolldecays['ship_speed'].fillna(0, inplace=True)

```

```python
df_rolldecays
```

      project_number
      series_number
      run_number
      test_number
      model_number
      ship_name
      loading_condition_id
      ascii_name
      ship_speed
      comment
      file_path_ascii
      file_path_ascii_temp
      file_path_log
      file_path_hdf5
      date
      test_type
      facility
      angle1
      angle2
      Körfallstyp

      id

      21337
      40178362
      1
      94
      1
      M5057-01-A
      M5057-01-A
      166
      94.0
      0.0
      Roll decay, 0 kn
      NaN
      None
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      2018-04-03
      roll decay
      MDL
      None
      None
      None

      21338
      40178362
      1
      95
      1
      M5057-01-A
      M5057-01-A
      166
      95.0
      0.0
      Roll decay, 0 kn
      NaN
      None
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      2018-04-03
      roll decay
      MDL
      None
      None
      None

      21339
      40178362
      1
      96
      1
      M5057-01-A
      M5057-01-A
      166
      96.0
      0.0
      Roll decay, 0 kn
      NaN
      None
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      2018-11-28
      roll decay
      MDL
      None
      None
      None

      21340
      40178362
      1
      97
      1
      M5057-01-A
      M5057-01-A
      166
      97.0
      15.5
      Roll decay, 15.5 kn
      NaN
      None
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      \\sspa.local\gbg\LABmeasuredataMDL\40178362\00...
      2018-04-04
      roll decay
      MDL
      None
      None
      None

```python
df_rolldecays[['date','ship_speed']]
```

      date
      ship_speed

      id

      21337
      2018-04-03
      0.0

      21338
      2018-04-03
      0.0

      21339
      2018-11-28
      0.0

      21340
      2018-04-04
      15.5

```python
df_rolldecays=df_rolldecays.loc[[21337,21338]].copy()
```

```python
fig,axes=plt.subplots(nrows=len(df_rolldecays))

for (id, row),ax in zip(df_rolldecays.iterrows(),axes):

    run = db.session.query(Run).get(int(id))
    run = database.load_run(run, save_as_example=False, prefer_hdf5=True)

    run.df.plot(y='phi', ax=ax)
    ax.set_ylabel(f"roll [{run.units['phi']}]")
    ax.grid(True)
    ax.set_ylim([-0.2,0.2])

plt.tight_layout()
```

```python
fig,axes=plt.subplots(ncols=len(df_rolldecays))

for (id, row),ax in zip(df_rolldecays.iterrows(),axes):

    run = db.session.query(Run).get(int(id))
    run = database.load_run(run, save_as_example=False, prefer_hdf5=True)

    run.track_plot(ax)

plt.tight_layout()
```

## Analyze the damping

```python
#run = db.session.query(Run).get(21338)
#run = db.session.query(Run).get(21339)
run = db.session.query(Run).get(21337)

#run = db.session.query(Run).get(21340)

run = database.load_run(run, save_as_example=False, prefer_hdf5=True)
```

    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units

```python
lowpass_filter = LowpassFilterDerivatorTransformer(cutoff=1, minimum_score=0.999)
cutter = CutTransformer(phi_max=np.deg2rad(15), phi_min=np.deg2rad(3.5), phi1d_start_tolerance=0.015)
offset_transformer = OffsetTransformer()
```

```python
estimator_cubic = EstimatorCubic(fit_method='derivation')
estimator_quadratic_b = EstimatorQuadraticB(fit_method='derivation', maxfev=1000)
```

```python
steps = [
    ('filter',lowpass_filter),
    ('cutter', cutter),
#    ('offset_transformer',offset_transformer),
    ('estimator', estimator_cubic)
]

pipline_cubic = Pipeline(steps=steps)
```

```python
steps = [
    ('filter',lowpass_filter),
    ('cutter', cutter),
#    ('offset_transformer',offset_transformer),
    ('estimator', estimator_quadratic_b)
]

pipline_quadratic_b = Pipeline(steps=steps)
```

```python
models_cubic = OrderedDict()
scores = pd.DataFrame()

for (id, row),ax in zip(df_rolldecays.iterrows(),axes):

    run = db.session.query(Run).get(int(id))
    run = database.load_run(run, save_as_example=False, prefer_hdf5=True)

    pipline_cubic.fit(X=run.df[['phi']])
    scores.loc['models_cubic',id] = pipline_cubic.score(X=run.df[['phi']])

    models_cubic[id]=deepcopy(pipline_cubic)
```

    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units

```python
fig,ax=plt.subplots()

for id, model in models_cubic.items():

    estimator = model['estimator']
    estimator.plot_damping(ax=ax)

fig,ax=plt.subplots()
for id, model in models_cubic.items():

    estimator = model['estimator']
    estimator.plot_omega0(ax=ax, include_model_test=True)
```

## Quadratic

```python
models_quadratic = OrderedDict()

for (id, row),ax in zip(df_rolldecays.iterrows(),axes):

    run = db.session.query(Run).get(int(id))
    run = database.load_run(run, save_as_example=False, prefer_hdf5=True)

    pipline_quadratic_b.fit(X=run.df[['phi']])
    scores.loc['pipline_quadratic_b',id] = pipline_quadratic_b.score(X=run.df[['phi']])

    models_quadratic[id]=deepcopy(pipline_quadratic_b)
```

    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units
    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units

```python
fig,ax=plt.subplots()

colors = ['r','g','b']
markers = ['o','*','+']

for id, model in models_quadratic.items():

    estimator = model['estimator']
    estimator.plot_damping(ax=ax)

fig,ax=plt.subplots()
for id, model in models_quadratic.items():

    estimator = model['estimator']
    estimator.plot_omega0(ax=ax, include_model_test=True)
```

## Integration

### Quadratic

```python
estimator_quadratic_b_int = EstimatorQuadraticB(fit_method='integration', maxfev=100000, p0=estimator_quadratic_b.parameters)

steps = [
    ('filter',lowpass_filter),
    ('cutter', cutter),
#    ('offset_transformer',offset_transformer),
    ('estimator', estimator_quadratic_b_int)
]

pipline_quadratic_b_int = Pipeline(steps=steps)
```

```python
models_quadratic_b_int = OrderedDict()

for (id, row),ax in zip(df_rolldecays.iterrows(),axes):

    run = db.session.query(Run).get(int(id))
    run = database.load_run(run, save_as_example=False, prefer_hdf5=True)

    pipline_quadratic_b_int.fit(X=run.df[['phi']])
    scores.loc['pipline_quadratic_b_int',id] = pipline_quadratic_b_int.score(X=run.df[['phi']])

    models_quadratic_b_int[id]=deepcopy(pipline_quadratic_b_int)
```

    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units
    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units

```python
fig,ax=plt.subplots()

colors = ['r','g','b']
markers = ['o','*','+']

for id, model in models_quadratic_b_int.items():

    estimator = model['estimator']
    estimator.plot_damping(ax=ax)

fig,ax=plt.subplots()
for id, model in models_quadratic.items():

    estimator = model['estimator']
    estimator.plot_omega0(ax=ax, include_model_test=True)
```

### Cubic

```python
estimator_cubic_int = EstimatorCubic(fit_method='integration', maxfev=100000, p0=estimator_cubic.parameters)

steps = [
    ('filter',lowpass_filter),
    ('cutter', cutter),
#    ('offset_transformer',offset_transformer),
    ('estimator', estimator_cubic_int)
]

pipline_cubic_int = Pipeline(steps=steps)
```

```python
models_cubic_int = OrderedDict()

for (id, row),ax in zip(df_rolldecays.iterrows(),axes):

    run = db.session.query(Run).get(int(id))
    run = database.load_run(run, save_as_example=False, prefer_hdf5=True)

    pipline_cubic_int.fit(X=run.df[['phi']])
    scores.loc['models_cubic_int',id] = pipline_cubic_int.score(X=run.df[['phi']])

    models_cubic_int[id]=deepcopy(pipline_cubic_int)
```

    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units
    c:\dev\evaluation\signal_lab\mdl_to_evaluation.py:106: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
      df_.units = units

```python
fig,ax=plt.subplots()

colors = ['r','g','b']
markers = ['o','*','+']

for id, model in models_cubic_int.items():

    estimator = model['estimator']
    estimator.plot_damping(ax=ax)

fig,ax=plt.subplots()
for id, model in models_cubic_int.items():

    estimator = model['estimator']
    estimator.plot_omega0(ax=ax, include_model_test=True)
```

```python
scores
```

      21337
      21338

      models_cubic
      0.999130
      0.998342

      pipline_quadratic_b
      0.996430
      0.992402

      pipline_quadratic_b_int
      0.996755
      0.993559

      models_cubic_int
      0.999980
      0.999968

```python
model = models_cubic_int[21338]
```

```python
joblib.dump(model, '../models/KVLCC2_0_speed.pkl')
```

    ['../models/KVLCC2_0_speed.pkl']

```python
for name, model in models_cubic_int.items():

    joblib.dump(model, '../models/KVLCC2_%i.pkl' % name)

```

```python
scale_factor = run.model.scale_factor

meta_data = {
    'Volume':run.loading_condition.Volume/(scale_factor**3),
    'GM':run.loading_condition.gm/scale_factor,
    'rho':1000,
    'g':9.81,
}

estimator = model['estimator']
result_for_database = estimator.result_for_database(meta_data=meta_data)
result_for_database
```

```python
result_for_database['omega0']
```

```python
result_for_database['omega0']/np.sqrt(run.model.scale_factor)
```

```python
phi_a = np.deg2rad(10)

B_e = lambdas.B_e_lambda_cubic(B_1=result_for_database['B_1'],
                         B_2=result_for_database['B_2'],
                         B_3=result_for_database['B_3'],
                         omega0=result_for_database['omega0'],
                         phi_a = phi_a)
```

```python
g=9.81
rho=1000
Volume = run.loading_condition.Volume/(scale_factor**3)
beam = run.ship.beam/scale_factor
lambdas.B_hat_lambda(B=B_e, Disp=Volume, beam=beam,g=g, rho=rho)
```

## $A_{44}$

```python
result_for_database['A_44']
```

```python
mass = run.loading_condition.Volume*rho
mass*run.loading_condition.KXX**2
```

```python
np.sqrt(result_for_database['A_44']/mass)
```

```python
result_for_database['A_44']/(mass*run.loading_condition.KXX**2)
```

```python
run.loading_condition.Volume/(run.ship.lpp*run.ship.beam*run.loading_condition.TA)
```

```python
run.ship.lpp
```

```python
run.ship.beam
```

```python
run.loading_condition.TA
```

```python
equations.A44
```

```python
omega0 = result_for_database['omega0']
scale_factor = run.model.scale_factor
GM = run.loading_condition.gm/scale_factor
m = run.loading_condition.Volume/(scale_factor**3)*rho

A_44 = lambdas.A44_lambda(GM=GM, g=g, m=m, omega0=omega0)
A_44
```

```python
kxx = run.loading_condition.KXX/scale_factor #???
I_44 = m*kxx**2
I_44
```

```python
A_44/I_44
```

```python

```

```python

```

```python

```


# Real Time Diagrammatics in QmeQ - Tutorial

This notebook contains a few examples highlighting the capabilities and usage of the RTD approach in QmeQ. The theory behind the approach is described [elsewhere](https://github.com/M-Josefsson/RTD-docs/blob/master/RTD.pdf).

## Installation

This notebook assumes that the latest version of QmeQ is installed. If not, it can be installed using

`
pip install qmeq
`

or

`pip install --upgrade qmeq`

if an older version of qmeq is already installed.

## The RTD approach

The RTD approach provides the functionality to solve the transport problem for diagonal density matrices using the Real Time Diagrammatic technique. It is a second order  approach ($\propto\Gamma^2$, fourth order in $t$) and it can be seen as the second order extension of the Pauli approach. As such, it cannot properly handle degenerate (or almost degenerate $\Delta E\lesssim \Gamma$) states coupled to the same lead since such conditions tend to lead to off-diagonal terms in the density matrix. However, recall that different spins couple to different leads in QmeQ, so spin-degeneracy is not a problem.

Being a second order approach means it takes into account processes such as co-tunneling, pair tunneling and energy re-normalization. It is however still perturbative in $\Gamma$ (formally in $t$) and is valid when $\hbar\Gamma\ll k_B T$. Choosing a too large $\Gamma$ can leads to unphysical artifacts, but one can generally make sure that a feature is not an artifact by checking that its magnitude scales with at most $\Gamma^2$, instead of $\Gamma^3$.

The approach is called RTD (i.e. `kerntype='pyRTD'` or `kerntype='RTD'`). It can be used in the same way as the first-order approaches, i.e. no additional parameters need to be specified. However, only `itype=1` is supported in the RTD approach, and the provided bandwidth should therefore always be a lot larger than all other energies in the problem. A larger bandwidth increases the accuracy of the results.

Furthermore, the RTD approach differs from the first order approaches in a few more ways. It does not support a matrix free method for solving the master equations, `mfreeq=True`, or the usage of symmetries `symmetry=True`. In addition, the only supported indexing type is `indexing=charge`.

We set $\hbar=e=k_b=1$ throughout this notebook.

## Auxiliary functions and imports

Before introducing the examples we'll define some functions used for calculating currents and for plotting.

```python
def current(system, V, Vg, T, dT, leads=[0]):
    """Calculates current for single, and double level systems. The variable
    leads defines which leads the current is evaluated in."""
    if system.nleads==2:
        system.change( mulst={0:V/2, 1:-V/2} )
        system.change( tlst={0:T+dT, 1:T} )
    elif system.nleads==4:
        system.change( mulst={0:V/2, 1:-V/2, 2:V/2, 3:-V/2} )
        system.change( tlst={0:T+dT, 1:T, 2:T+dT, 3:T} )
    else:
        raise ValueError('Current only defined for 2 or 4 leads.')

    if system.nsingle == 1:
        system.change( hsingle={(0,0):-vg} )
    elif system.nsingle == 2:
        system.change( hsingle={(0,0):-vg, (1,1):-vg} )
    else:
        raise ValueError('Current only defined for systems with nsingle = 1 or 2 .')

    system.solve()
    I, Q = 0, 0
    for l in leads:
        I += system.current[l]
        Q += system.heat_current[l]
    return I,Q

def generate_plot(data_x, data_y, label, color, ylabel, xlabel='Vg'):
    """Plots data_x[i] vs data_y[i]. Sets color and labels based on
    the input parameters."""

    xlabels = {'Vg' : r'$V_g\ [\hbar\Gamma/e]$', 'Vg_U' : r'$V_g\ [U/e]$'}
    ylabels = {'I' : r'$I\ [e\Gamma]$', 'Q' : r'$Q\ [\hbar\Gamma^2]$',
               'Vth': r'$V_{th}\ [\hbar\Gamma/e]$', 'P': r'$P\ [\hbar\Gamma^2]$',
              'n':r'$\eta\ [\eta_C]$', 'absI':r'$|I|\ [e\Gamma]$'}

    for i in range(len(data_y)):
        p1 = plt.plot(data_x, data_y[i], color[i], label=label[i])
    plt.xlabel(xlabels[xlabel], size=14)
    plt.ylabel(ylabels[ylabel], size=14)
    plt.legend()
```

```python
#Imports
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import scipy.optimize as optimize
import qmeq
```

## Minimal example: single level

As a first example let's look at a single level (no spin) connected to two leads. The Hamiltonian for this case reads:

\begin{equation}
    H = H_R + H_T + H_{QD} \\
    H_R = \sum_{k,r=l,r}\omega_{k, r}c^\dagger_{k,r}c_{k, r}\\
    H_T = \sum_{k,r} t_{k, r}d^\dagger c_{k, r} + h.c. . \\
    H_{QD} = \varepsilon d^\dagger d,
\end{equation}

where $c$ denotes fermion field operators acting on the lead subspace and $d$ the QD counterparts.

We'll use both `kerntype='RTD'` and `kerntype='Pauli'` to get a comparison between the two. Non-equilibrium conditions are included using either a voltage bias or a thermal biases. First we setup our `qmeq.Builder` objects:

```python
nsingle, nleads = 1, 2
hsingle = {(0,0):0}

gamma = 1
t = np.sqrt(gamma/2/np.pi)
tleads = {(0,0):t, (1,0):t}
T, V = 10*gamma, 0

system_RTD = qmeq.Builder(nsingle=nsingle, nleads=nleads, tleads=tleads, kerntype='pyRTD', itype=1, dband=1e6)
system_Pauli= qmeq.Builder(nsingle=nsingle, nleads=nleads, tleads=tleads, kerntype='Pauli', itype=1, dband=1e6)
```

Next we sweep the gate voltage $V_g$ (level energy)  at a fixed bias voltage $V=10\Gamma=T \ (\Delta T=0)$ and plot the results.

```python
Vg = np.linspace(-100, 100, 200)
I_RTD, I_Pauli = np.zeros(len(Vg)), np.zeros(len(Vg))
Q_RTD, Q_Pauli = np.zeros(len(Vg)), np.zeros(len(Vg))
V = 10

for i, vg in enumerate(Vg):
    I_RTD[i], Q_RTD[i] = current(system_RTD, V, vg, T, 0)
    I_Pauli[i], Q_Pauli[i] = current(system_Pauli, V, vg, T, 0)

#Plotting
plt.figure(figsize=(11, 3.5))
f1 = plt.subplot(121)
generate_plot(Vg, [I_RTD, I_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'I', 'Vg')
plt.subplot(122)
generate_plot(Vg, [Q_RTD, Q_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'Q', 'Vg')
plt.show()
```

Here one can see that the second order processes make the results qualitatively different, with generally lower currents.

Switching to a thermal bias $\Delta T = T/2$ $(V=0)$ instead of a voltage bias we get:

```python
dT = T/2

for i, vg in enumerate(Vg):
    I_RTD[i], Q_RTD[i] = current(system_RTD, 0, vg, T, dT)
    I_Pauli[i], Q_Pauli[i] = current(system_Pauli, 0, vg, T, dT)

#Plotting
plt.figure(figsize=(11, 3.5))
f1 = plt.subplot(121)
generate_plot(Vg, [I_RTD, I_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'I', 'Vg')
plt.subplot(122)
generate_plot(Vg, [Q_RTD, Q_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'Q', 'Vg')
plt.show()
```

Second order processes allow electron transport at a finite, but small, energy window around the energy level, in contrast to first order processes where the energy of the transported electrons is always exactly equal to that of the QD state. This finite energy window leads to a finite heat current when $\mu_1=\mu_2=V_g$, as can be seen for the black line at $V_g=0$ in the right-hand side figure, something that is not possible in first order.

Although the effects of the second order corrections might seem small in these plots, they can have large consequences in certain situations, as will be seen later.

## Anderson model: co-tunneling

Let's turn our attention to the Anderson model:

\begin{equation}
    H = H_R + H_T + H_{QD} \\
    H_R = \sum_{k,\sigma,r=l,r}\omega_{k,\sigma, r}c^\dagger_{k,\sigma,r}c_{k,\sigma, r}\\
    H_T = \sum_{k,\sigma,r} t_{k,\sigma, r}d^\dagger_{\sigma}c_{k,\sigma, r} + h.c. . \\
    H_{QD} = \sum_{\sigma=\uparrow\downarrow}\varepsilon_\sigma n_\sigma + Un_\uparrow n_\downarrow
\end{equation}

where $n_\sigma = d^\dagger_\sigma d_\sigma$. When the QD is subjected to an external magnetic field the single particle states will be Seeman-split $\Delta \varepsilon = \varepsilon_\uparrow - \varepsilon_\downarrow>0$. In that case one expects to see familiar co-tunneling lines inside the Coulomb diamond when performing measurements on QDs. Both second order approaches in QmeQ are able to capture this feature, as we will see now. However, due to the  numerical complexity of the 2nd order von Neumann approach we only demonstrate this for the RTD approach.

First we build the system.

```python
def Anderson_model(kerntype, T, U, Ez):
    """Sets up an Anderson model with lead temperature
    T, interaction strength U and Seeman split Ez."""
    nsingle, nleads = 2, 4
    gamma = 1
    t = np.sqrt(gamma/2/np.pi)
    T = T
    U = U
    Ez = Ez #Seeman split

    tlst = {0:T, 1:T, 2:T, 3:T}
    tleads = {(0,0):t, (1,0):t, (2,1):t, (3,1):t}
    coulomb = {(0,1,1,0):U}
    mulst = {0:0, 1:0, 2:0, 3:0}
    hsingle = {(0,0):0, (1,1):0}

    system = qmeq.Builder(nsingle, hsingle, coulomb,
                          nleads, tleads, mulst, tlst, 5e5,
                          kerntype=kerntype, itype=1)
    return system

#Set up the model
T, U, Ez = 10, 500, 100
system = Anderson_model('RTD', T, U, Ez)
```

Next we solve for the differential conductance $G=\frac{dI}{dV}$ for both $V$ and $V_g$ to be able to plot a stability diagram.

```python
%%time
Vg = np.linspace(-U/2, U*3/2, 100)
V = np.linspace(-U*3/2, U*3/2, 100)
G = np.zeros((len(V), len(Vg)))
dV = 1e-3

for i, vg in enumerate(Vg):
    system.change(hsingle={(0,0):-vg -Ez/2,(1,1):-vg+Ez/2})
    for j, v in enumerate(V):
        system.change(mulst={0:v/2+dV, 1:-v/2-dV, 2:v/2+dV, 3:-v/2-dV})
        system.solve()
        I1 = system.current[0] + system.current[2]

        system.change(mulst={0:v/2-dV, 1:-v/2+dV, 2:v/2-dV, 3:-v/2+dV})
        system.solve()
        G[j,i] = (I1 - system.current[0] - system.current[2])/(2*dV)
```

    Wall time: 25.7 s

When plotting the result we see large areas of finite (but small) conductance away from the zero-bias line $V=0$ in the Coulomb diamond. This is an effect of co-tunneling, which is properly captured by the RTD approach.

```python
plt.figure(figsize=(7,5))
plt.pcolormesh(Vg/U, V/U, G, norm=matplotlib.colors.LogNorm() )
c = plt.colorbar(label=r'$G \ [e^2/{\hbar}]$')
plt.xlabel(r'$V_g\ [U]$', size=14)
plt.ylabel(r'$V\ [U]$', size=14)
plt.show()
```

## A larger example: the quantum dot heat engine

In order to see when it can be absolutely necessary to include second order effects we can keep studying the Anderson model. However, now we'll use the QD as a heat engine, converting thermal energy into electrical energy using the Seebeck effect. When using the QD as a heat engine it's connected in series to an external (resistive) load, instead of a voltage source, yielding the circuit diagram:

The red and blue blocks next to the QD symbolize leads at different temperatures. Due to current conservation in the circuit the same current needs to flow through both components. In addition, Krichhoff's voltage law states that the size of the voltages need to be the same over both components, but with opposite polarity. Thus, determining the operating conditions for the QD requires solving (iteratively)

\begin{equation}
    I_{QD}(V_{QD}) + \frac{V_{QD}}{R}=0,
\end{equation}

for $V_{QD}$. In the limit $R\rightarrow\infty$ the equation can be used to define the thermovoltage $V_{th}=V_{QD}\Big |_{I=0}$.

All electrical power generated by the QD will be dissipated in the load and it is evaluated as $P=IV_{QD}$. We're also interested in the thermodynamic efficiency of device

\begin{equation}
    \eta=\frac{P}{\dot{Q}_H},
\end{equation}

with $\dot{Q}_H$ denoting the heat current entering the QD from the hot side.

Before calculating the power and efficiency of the device we need to introduce functions for calculating the voltage over the QD in the presence of a load, and during open circuit conditions.

```python
def func_root(v, vg, T, dT, system, R):
    I, Q = current(system, v, vg, T, dT, leads=[0,2])
    if R != np.inf:
        I += v/R
    return I

def voltage_R(vg, T, dT, system, R):
    result = optimize.root_scalar(func_root, args=(vg, T, dT, system, R), method='brentq', bracket=[-200, 200])
    return result.root
```

It's well-known that sequential (first order) tunneling processes often don't give accurate predictions of $V_{th}$ (i.e. $\lim_{R\rightarrow \infty}V_{QD}$) due to a tight coupling between particle and energy flow. Including also second order processes breaks this tight coupling, which can lead to drastic changes of the thermovoltage, even for seemingly small $\Gamma$s.

To see this we calculate $V_{th}$ using both `kerntype='RTD'` and `kerntype='Pauli'` for an Anderson model where $T=\Delta T = 10\hbar\Gamma$ and $U = 20T$.

```python
system_RTD = Anderson_model('RTD', T, U, 0)
system_Pauli = Anderson_model('Pauli', T, U, 0)

dT = T
Vg = np.linspace(-U/2, U*3/2, 200)
Vth_RTD, Vth_Pauli = np.zeros(len(Vg)), np.zeros(len(Vg))
for i, vg in enumerate(Vg):
    Vth_RTD[i] = voltage_R(vg, T, dT, system_RTD, np.inf)
    Vth_Pauli[i] = voltage_R(vg, T, dT, system_Pauli, np.inf)

generate_plot(Vg, [Vth_RTD, Vth_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'Vth', 'Vg_U')
```

Indeed, we see that including the second order processes drastically change the lineshape of $V_{th}(V_g)$.

Next, we change the load to a finite value $R=5\ M\Omega$ such that the device can generate power. We focus on the generated power and absorbed heat current as a function of $V_g$:

```python
R = 243*5 #5 MOhm i units of hbar/e^2
I_Pauli, I_RTD = np.zeros(len(Vg)), np.zeros(len(Vg))
P_Pauli, P_RTD = np.zeros(len(Vg)), np.zeros(len(Vg))
Q_Pauli, Q_RTD = np.zeros(len(Vg)), np.zeros(len(Vg))

for i, vg in enumerate(Vg):
    v_Pauli = voltage_R(vg, T, dT, system_Pauli, R)
    I_Pauli[i], Q_Pauli[i] = current(system_Pauli, v_Pauli, vg, T, dT, leads=[0,2])
    P_Pauli[i] = -v_Pauli*I_Pauli[i]

    v_RTD = voltage_R(vg, T, dT, system_RTD, R)
    I_RTD[i], Q_RTD[i] = current(system_RTD, v_RTD, vg, T, dT, leads=[0,2])
    P_RTD[i] = -v_RTD*I_RTD[i]

#Plotting
plt.figure(figsize=(11, 3.5))
plt.subplot(121)
generate_plot(Vg/U, [P_RTD, P_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'P', 'Vg_U')
plt.subplot(122)
generate_plot(Vg/U, [Q_RTD, Q_Pauli], ['RTD', 'Pauli'], ['k', 'r--'], 'Q', 'Vg_U')
```

The differences between the two approaches are quite substantial, especially for the heat current, and they lead to even larger differences when calculating the efficiency:

```python
n_Pauli = P_Pauli/Q_Pauli
n_RTD = P_RTD/Q_RTD
nc = dT/(T+dT)

generate_plot(Vg/U, [n_RTD/nc, n_Pauli/nc], ['RTD', 'Pauli'], ['k', 'r--'], 'n', 'Vg_U')
```

Here we see that ignoring the second order processes leads to very large errors. The differences stem from the fact the QD can be seen as a perfect energy filter in the Pauli case (energy of transported electrons = energy of the QD state), which is no longer true when using the RTD approach. This has a large impact on $\eta$.

(The Pauli result actually gives $\eta=0$ for $V_g=0$ and $V_g=U$, but the resolution used here is too small to capture that feature.)

## Off-diagonal corrections

The RTD approach sets up Master equations for diagonal density matrices. However, when one derives these equations by performing a systematic expansion of the Liouville-von Neumann equation up to second order in $\Gamma$ one needs to include a correction term that stems from off-diagonal elements in first order. See [this article](https://journals.aps.org/prb/abstract/10.1103/PhysRevB.78.235424) for an explanation.

These correction terms are included by default in QmeQ, but can be turned off by the user. This is done by setting the variable `off_diag_corrections` of the `qmeq.Builder` object to `False` before calling `solve()`, e.g.

```Python
system = qmeq.Builder(..., kerntype='RTD')
system.off_diag_corrections = False
system.solve()
```

However, turning off the corrections generally does not lead to any noticeable performance gains, although it reduces memory consumption a bit.

These corrections can be important as long as first order off-diagonal terms are not identically zero (which they can be due to selection rules, e.g. in the Anderson model). They are e.g. important when more than one QD state connect to the same lead, even when the energy difference between the states is large. To illustrate this we can study a model with two states, both connected to the same two leads.

\begin{equation}
    H = H_R + H_T + H_{QD} \\
    H_R = \sum_{k,r=l,r}\omega_{k,r}c^\dagger_{k,r}c_{k, r}\\
    H_T = \sum_{k,r,i} t_{k, r, i}d^\dagger_{i}c_{k,r} + h.c. . \\
    H_{QD} = \sum_{i=1,2}\varepsilon_i d^\dagger_id_i
\end{equation}

We set up the systems using parameters where the corrections become important (in this case $T=10\Gamma$, $V=3T,\ \Delta\varepsilon = 2T,\ U = 10^4T$):

```python
nsingle, nleads = 2, 2
gamma, T, V, dE, U = 1, 10, 30, 20, 1e5

coulomb = {(0,1,1,0):U}
hsingle = {(0,0):0, (1,1):dE}

t = np.sqrt(gamma/2/np.pi)
tleads = {(0,0):t, (1,0):t, (0,1):t, (1,1):t}
tlst = {0:T, 1:T}
mulst = {0:-V/2, 1:V/2}

system = qmeq.Builder(nsingle=nsingle, hsingle=hsingle, nleads=nleads, tleads=tleads, mulst=mulst,
                      coulomb=coulomb, tlst=tlst, kerntype='RTD', itype=1, dband=1e6)
```

Sweeping $V_g$ and plotting the current with and without the corrections shows the importance of these effects.

```python
Vg = np.linspace(-0.2*U, 1.2*U , 300)
I1, I2 = np.zeros(len(Vg)), np.zeros(len(Vg))

for i, vg in enumerate(Vg):
    system.change(hsingle={(0,0):-vg, (1,1):dE-vg})
    system.solve()
    I1[i] = system.current[0]

    system.off_diag_corrections = False
    system.solve()
    I2[i] = system.current[0]
    system.off_diag_corrections = True

#Plotting
plt.figure(figsize=(13,4))
plt.subplot(121)
generate_plot(Vg/U, [abs(I1), abs(I2)], ['w/ corr', 'w/o corr'], ['r', 'b'], 'absI', 'Vg_U')
plt.semilogy()

plt.subplot(122)
plt.semilogy(Vg/U, abs((I2-I1)/I1), 'k')
plt.xlabel(r'$V_g\ [U/e]$', size=14)
plt.ylabel('relative error ' + r'$\left | (I-I_{w/o})\ / \ I \right|$', size=14)
plt.show()
```

It's obvious that it is absolutely crucial to include the correction terms for these parameters. The relative strength of the corrections will of course depend on the model and its parameters, but it's generally recommended to keep the corrections turned on.

## Complex $t$s

The RTD approach can in general handle complex $t$s, with the exception that the energy current cannot be evaluated when the product of $t$s lead to complex matrix elements of the energy kernels. However, if one, for example, rotates all $t$s with a global complex phase one will not change the resulting currents. We exemplify this using the Anderson model

```python
def rotate_tleads(g, theta):
    t = np.sqrt(g/2/np.pi)
    t2 = t*(np.cos(theta)+ 1j*np.sin(theta))
    tleads = {(0,0):t2, (1,0):t2, (2,1):t2, (3,1):t2}
    return tleads

T, dT = 10, 10
U = 400
N = 100

#Set up system and rotate tleads
system = Anderson_model('RTD', T, U, 0)
system2 = Anderson_model('RTD', T, U, 0)
theta = np.random.rand()*2*np.pi
tleads = rotate_tleads(1, theta)
system2.change(tleads = tleads)

#Sweep Vg and calculate current
Vg = np.linspace(-150, 550, N)
I, Q = np.zeros(N), np.zeros(N)
I2, Q2 = np.zeros(N), np.zeros(N)
for i, vg in enumerate(Vg):
    I[i], Q[i] = current(system, 0, vg, T, dT, leads=[0,2])
    I2[i], Q2[i] = current(system2, 0, vg, T, dT, leads=[0,2])

#Plotting
generate_plot(Vg/U, [I, I2], [r'$\theta=0$', r'$\theta\ne 0$'], ['k', 'r--'], 'I', 'Vg_U')
plt.title(r'$\theta$ = ' + str(round(theta, 2)) )
plt.show()
```

## Comparison with 2nd order von Neumann (2vN)

The `RTD` approach and the `2vN` approach are based on different assumptions, and they can thus model different physical processes. Both are second order approaches, but whereas `RTD` is second order in $\Gamma$, `2vN` is second order in the number of electron/hole excitations. The currents calculated using the two approaches are thus expected to differ sometimes, but will give identical results other times.

One example where they give identical results are for the an Anderson model where $U=1000\Gamma$, $T = 10\Gamma$. Below are some pre-computed results comparing the charge currents for the two approaches when sweeping either $V_g$ with a thermal bias $(\Delta T=\frac{1}{2}T, V=0)$ or sweeping $V$ with a voltage bias $(\ \Delta T = 0, V_g = 100\Gamma)$

## RTD with ManyBodyBuilder

In the RTD approach the energy current out from a lead is evaluated using three Kernels:

\begin{equation}
\langle J_r \rangle = \underset{D}{\text{Tr}} L_D W_r \rho_D - \frac{1}{2}\underset{D}{\text{Tr}}W_{E,1}^r\rho_D + \frac{1}{2}\underset{D}{\text{Tr}}W_{E,2}^r\rho_D
\end{equation}

The kernels in the two right-most terms depend on both the usual *many body* tunneling amplitudes in `Tba` as well as the pure *single particle* tunneling amplitudes defined in `tleads`. Therefore, using the RTD approach together with`qmeq.ManyBodyBuilder` can lead to incorrect evaluation of the energy- and heat currents as no single particle tunneling amplitudes are defined, and the last two terms are simply ignored. This is no problem in models where each single particle state only connects to a *single* lead, since the kernels contain terms proportional to $t_{l,r}t_{l,r'}$ where $l$ denotes a single particle state and $r,\ r'$ two *different* leads.

If that is not the case also single particle tunneling amplitudes have to be added when using the `ManyBodyBuilder`. We exemplify this by looking at a model with two single particle states that are hybridized. We include two leads and both states couple to both leads.

\begin{equation}
    H = H_R + H_T + H_{QD} \\
    H_R = \sum_{k,r=l,r}\omega_{k,r}c^\dagger_{k,r}c_{k, r}\\
    H_T = \sum_{k,r,i} t_{k, r, i}d^\dagger_{i}c_{k,r} + h.c. . \\
    H_{QD} = \sum_{i=1,2}\varepsilon_i d^\dagger_id_i + \sum_{i\ne j}\Omega_{ij}d^\dagger_i d_j
\end{equation}

We first build our model using `qmeq.Builder` to be able obtain many-body tunnel matrix elements and a diagonalized QD Hamiltonian:

```python
#lead parameters
nleads = 2
mulst = [0, 0]
tlst = [20, 10]
t = 1/np.sqrt(2*np.pi)
tleads_ref = {(0, 0): t, (0,1):t/10, (1, 1): t, (1,0):t/10}

#QD parameters
nsingle = 2
Omega, U = 5, 500
coulomb = {(0,1,1,0):U}
hsingle = {(0,0):0, (1,1):0, (0,1):Omega}

#Setup reference system and soolve once
system_ref = qmeq.Builder(nsingle=nsingle, nleads=nleads, hsingle=hsingle, mulst=mulst, coulomb=coulomb,
                      tlst=tlst, tleads=tleads_ref, dband=5e5, kerntype='RTD', itype=1)
system_ref.solve()
print('Many-body energies: ', system_ref.Ea, '\n')
print('Many-body tunneling:\n ', system_ref.Tba)
```

    Many-body energies:  [  0.  -5.   5. 500.]

    Many-body tunneling:
      [[[ 0.        +0.j  0.25388531+0.j  0.31030427+0.j  0.        +0.j]
      [ 0.25388531-0.j  0.        +0.j  0.        +0.j -0.31030427+0.j]
      [ 0.31030427-0.j  0.        +0.j  0.        +0.j  0.25388531+0.j]
      [ 0.        +0.j -0.31030427-0.j  0.25388531-0.j  0.        +0.j]]

     [[ 0.        +0.j -0.25388531+0.j  0.31030427+0.j  0.        +0.j]
      [-0.25388531-0.j  0.        +0.j  0.        +0.j -0.31030427+0.j]
      [ 0.31030427-0.j  0.        +0.j  0.        +0.j -0.25388531+0.j]
      [ 0.        +0.j -0.31030427-0.j -0.25388531-0.j  0.        +0.j]]]

For convenience we define a function for looping over $V_g$ and calculating the energy current for the two methods of building the system:

```python
def loop_energy_current(sys, MB=False):
    """Loops Vg and calculates E. If MB=True it is assumed that
    the ManyBodyBuilder was used."""
    I = []
    for vg in Vg:
        if MB is True:
            sys.Ea = np.array([0, -vg-Omega, -vg+Omega, U-2*vg])
            sys.si.states_changed = True
            sys.solve(qdq=False, rotateq=False)
        else:
            sys.change(hsingle = {(0,0):-vg, (1,1):-vg, (0,1):Omega})
            sys.solve()
        I.append(sys.energy_current[0])
    return I

Vg = np.linspace(-200,U + 200, 100)
```

Next we see what happens if we use `qmeq.ManyBodyBuilder()` without specifying single particle tunneling. We plot the absolute value of the relative error with respect to the reference $\varepsilon= |(I_{MB}-I_{ref})/I_{ref}|$.

*Note that when using `qmeq.BuilderManyBody` we need to create the system using `pyRTD` and then change the kernel to `RTD`. This is due to a bug in the current version of the `RTD` approach. This will be fixed in later versions.*

```python
#Setup MB system
MB_charge = [0,1,1,2] #Charge of many body states
system1 = qmeq.BuilderManyBody(system_ref.qd.Ea, MB_charge, system_ref.Tba, mulst, tlst, 5e5,
                               kerntype='RTD', itype=1)
system1.kerntype='RTD'

#Calculate reference and many body energy currents
I_ref = loop_energy_current(system_ref)
I1 = loop_energy_current(system1, MB = True)

#Plotting
plt.semilogy(Vg/U, abs((np.array(I_ref) - np.array(I1))/np.array(I_ref)))
plt.ylabel('| relative error |', size=14)
plt.xlabel(r'$V_g\ [U]$', size=14)
plt.show()
```

Here it is obvious that we make mistakes when calculating the energy current. The magnitude of the error will of course depend on the model and its parameters.

We'll therefore add single particle tunneling amplitudes to the ManyBodyBuilder system. This requires specifying `ManyBodyBuilder.nsingle` and `ManyBodyBuilder.tleads_array` where `tleads_array` is an `nleads*nsingle` array.

When specifying `tleads_array` we need to choose a single particle basis. We'll exemplify this by using both the same basis as for the reference system (see above), and the bonding/anti-bonding (molecule) basis. For the former we have already defined the $t$s in `tleads`. For the latter we need to rotate `tleads` to the new basis.

```python
#Define the two systems
system2 = qmeq.BuilderManyBody(system_ref.qd.Ea, MB_charge, system_ref.Tba, mulst, tlst, 5e5,
                               kerntype='pyRTD', itype=1)
system2.kerntype = 'RTD'

system3 = qmeq.BuilderManyBody(system_ref.qd.Ea, MB_charge, system_ref.Tba, mulst, tlst, 5e5,
                               kerntype='pyRTD', itype=1)
system3.kerntype = 'RTD'

#Rotate tleads for bonding/anti-bonding basis
f1 = 0.6363961030854391
f2 = 0.7778174593127221
tleads_bond = {(0, 0): t*f1, (0,1):t*f2, (1, 1): t*f2, (1,0):-t*f1}

#Translate tleads to arrays
tleads_array2 = np.zeros((2,2), dtype=complex)
tleads_array3 = np.zeros((2,2), dtype=complex)
for i in range(2):
    for j in range(2):
        tleads_array2[i,j] = tleads_ref[(i,j)]
        tleads_array3[i,j] = tleads_bond[(i,j)]

#Set single particle tunneling and calculate energy currents
system2.nsingle = 2
system3.nsingle = 2
system2.tleads_array = tleads_array2
system3.tleads_array = tleads_array3
I2 = loop_energy_current(system2, MB = True)
I3 = loop_energy_current(system3, MB = True)

#Plotting
plt.semilogy(Vg/U, abs((np.array(I_ref) - np.array(I2))/np.array(I_ref)), 'k')
plt.semilogy(Vg/U, abs((np.array(I_ref) - np.array(I3))/np.array(I_ref)),'r--')
plt.ylabel('| relative error |', size=14)
plt.xlabel(r'$V_g\ [U]$', size=14)
plt.show()
```

Here we can see that both basis choices give (approximately) the same error, which is many orders of magnitude smaller than for the case without single particle tunneling.