# Single-vertical wavenumber near-inertial wave model

### Model equations
The Kernel class solves the near-inertial wave equation

\begin{equation}\tag{1}
\phi_t + J(\psi,\phi) + \tfrac{\mathrm{i}}{2}\phi\triangle\psi - \tfrac{\mathrm{i}}{2}f_0\lambda^2 \triangle\phi=  \nu_w \triangle \phi -\nu_{4w} \triangle^2 \phi - \mu_w \phi  \,,
\end{equation}
where $\phi$ is the near-inertial velocity
\begin{equation}\tag{2}
u_w + \mathrm{i}v_w = \phi(x,y,t)\,\exp[\mathrm{i}(m\,z - f_0 t)]\,,
\end{equation}
and $\lambda \equiv \tfrac{N_0}{f_0 m}$ is an intrinsic horizontal scale.

The flow, $\psi$, is quasigeostrophic and its dynamics and inversion relationship varies with model subclass.

#### YBJModel
In this subclass, the flow is steady and its prescribed as an initial condition.

#### UnCoupledModel
In this subclass, the flow evolves through standard quasigeostrophic dynamics:

\begin{equation}\tag{3}
q_t  + J(\psi,q) =  \nu \triangle q -\nu_4 \triangle^2 q - \mu q  \,,
\end{equation}

where the vorticity, $q$, is related to the streamfunction via the inversion relation

\begin{equation}\tag{4}
q = \triangle \psi\, .
\end{equation}

#### CoupledModel
In this subclass, waves feedback into the geostrophic flow via the inversion relationship:

\begin{equation}\tag{5}
q = \triangle \psi +\, \tfrac{1}{2}\left[\triangle \tfrac{1}{2}|\phi|^2 + \mathrm{i}J(\phi^{\star},\phi)\right] \,,
\end{equation}
where the potential vorticity, $q$, is governed by (3).

```python

```


```
%%capture
!pip install --upgrade pip
!pip install cirq==0.7
```

# QAOA Experiment
In this experiment, you are going to implement a hardware compatible QAOA algorithm for determining the Max-Cut of the processor's hardware graph (where you assign random edge weights). You will need to:

1. Define a random set of weights over the hardware graph.
2. Construct a QAOA circuit using Cirq
3. Calculate the expectated value of the QAOA cost function.
4. Create an outer loop optimization to minimize your cost function.
5. Compare cuts found from QAOA with the optimal cut.

## 1. Defining a random set of weights over the hardware graph
In order to make the problem easily embedable on a quantum device you are going to look at the problem of Max-Cut on the same graph that the device's qubit connectivity defines, but with random valued edge weights.

```
import cirq
import sympy
import numpy as np
import matplotlib.pyplot as plt
working_device = cirq.google.Bristlecone
print(working_device)
```

Since a circuit covering the entire Bristlecone device cannot be easily simulated, a small subset of the device graph will be used instead.

```
import networkx as nx

# Identify working qubits from the device.
device_qubits = working_device.qubits
working_qubits = sorted(device_qubits)[:12]

# Populate a networkx graph with working_qubits as nodes.
working_graph = nx.Graph()
for qubit in working_qubits:
  working_graph.add_node(qubit)

# Pair up all neighbors with random weights in working_graph.
for qubit in working_qubits:
  for neighbor in working_device.neighbors_of(qubit):
    if neighbor in working_graph:
      # Generate a randomly weighted edge between them. Here the weighting
      # is a random 2 decimal floating point between 0 and 5.
      working_graph.add_edge(
          qubit, neighbor, weight=np.random.randint(0,500) / 100)

nx.draw_circular(working_graph, node_size=1000, with_labels=True)
plt.show()
```

## 2. Construct the QAOA circuit
Now that you have created a Max-Cut problem graph, it's time to generate the QAOA circuit following [Farhi et al.](https://arxiv.org/abs/1411.4028). For simplicity $p=1$ is chosen.

```
from cirq.contrib.svg import SVGCircuit

alpha = sympy.Symbol('alpha')
beta = sympy.Symbol('beta')

qaoa_circuit = cirq.Circuit(
    # Prepare uniform superposition on working_qubits == working_graph.nodes
    cirq.H.on_each(working_graph.nodes()),

    # Do ZZ operations between neighbors u,v in the graph. u is a qubit v is
    # its neighbor qubit and w is the weight between these qubits.
    (cirq.ZZ(u, v) ** (alpha * w['weight']) for (u, v, w) in working_graph.edges(data=True)),

    # Apply X operations along all nodes of the graph. Again working_graph's
    # nodes are the working_qubits. Not here we use a moment
    # which will force all of the gates into the same line.
    cirq.Moment(cirq.X(qubit) ** beta for qubit in working_graph.nodes()),

    # All relevant things can be computed in computational basis.
    (cirq.measure(qubit) for qubit in working_graph.nodes()),
)
SVGCircuit(qaoa_circuit)
```

## 3. Calculating the expected value of the QAOA cost function
Now that you have created your parameterized QAOA circuit, you are going to need a way to calculate the expectation values of the cost function. Since the cost function is the (weighted) sum of all $ZZ$ pairs in the graph you can compute all of these values simultaneously.

First you need a way to estimate the expectation value given the samples from each qubit.

```
def estimate_cost(graph, samples):
    """Estimate the cost function of the QAOA on the given graph using the
    provided computational basis bitstrings."""
    cost_value = 0.0

    # Loop over edge pairs and compute contribution.
    for u, v, w in graph.edges(data=True):
      u_samples = samples[str(u)]
      v_samples = samples[str(v)]

      # Determine if it was a +1 or -1 eigenvalue.
      u_signs = (-1)**u_samples
      v_signs = (-1)**v_samples
      term_signs = u_signs * v_signs

      # Add scaled term to total cost .
      term_val = np.mean(term_signs) * w['weight']
      cost_value += term_val

    return -cost_value
```

Now if you run `qaoa_circuit` with a certain set of values for your placeholders you can use `estimate_expectation` to calculate the expectation value of the cost function for the circuit:

```
alpha_value = np.pi / 4
beta_value = np.pi / 2
sim = cirq.Simulator()
sample_results = sim.sample(qaoa_circuit, params={alpha: alpha_value, beta: beta_value}, repetitions=20000)
print(f'Alpha={alpha_value} Beta={beta_value}')
print(f'Estimated cost: {estimate_cost(working_graph, sample_results)}')
```

## 4. Outer loop optimization
There are lots of different techniques to choose parameters for `qaoa_circuit`. When there are only two parameters you can keep things simple and sweep over incremental pairings using `np.linspace` and track the minimum value you've found along the way.

```
grid_size = 5
exp_values = np.empty((grid_size, grid_size))
par_values = np.empty((grid_size, grid_size, 2))
for i, alpha_value in enumerate(np.linspace(0, 2*np.pi, grid_size)):
  for j, beta_value in enumerate(np.linspace(0, 2*np.pi, grid_size)):
    samples = sim.sample(
        qaoa_circuit,
        params={alpha: alpha_value, beta: beta_value},
        repetitions=20000)
    exp_values[i][j] = estimate_cost(working_graph, samples)
    par_values[i][j] = alpha_value, beta_value
```

You can also examine the cost function values w.r.t $\alpha$ and $\beta$:

```
plt.title('Heamap of QAOA Cost Function Value')
plt.xlabel(r'$\alpha$')
plt.ylabel(r'$\beta$')
plt.imshow(exp_values)
```

## 5. Compare cuts
Since you are going to be comparing graph cuts in `working_graph` a helper function to draw them and print their the size would be useful:

```
def output_cut(S_partition):
  """Plot and output the graph cut information."""

  # Generate the colors.
  coloring = []
  for node in working_graph:
    if node in S_partition:
      coloring.append('blue')
    else:
      coloring.append('red')

  # Get the weights
  edges = working_graph.edges(data=True)
  weights = [w['weight'] for (u,v, w) in edges]

  nx.draw_circular(
      working_graph,
      node_color=coloring,
      node_size=1000,
      with_labels=True,
      width=weights)
  plt.show()
  size = nx.cut_size(working_graph, S_partition, weight='weight')
  print(f'Cut size:{size}')

# Test with the empty S and all nodes placed in T.
output_cut([])
```

To get cuts using the QAOA you will first need to extract the best control parameters you found during your sweep:

```
best_exp_index = np.unravel_index(np.argmax(exp_values), exp_values.shape)
best_parameters = par_values[best_exp_index]
print(f'Best control parameters:{best_parameters}')
```

Each bitstring can be seen as a candidate cut in your graph. The qubits that measured 0 correspond to that qubit being in one cut partition and a qubit that measured to 1 corresponds to that qubit being in the other cut partition. Now that you've found good parameters for your circuit, you can just sample some bistrings, iterate over them and pick the one that gives the best cut:

```
# Number of candidate cuts to sample.
num_cuts = 100
candidate_cuts = sim.sample(
    qaoa_circuit,
    params={alpha: best_parameters[0], beta: best_parameters[1]},
    repetitions=num_cuts)

# Variables to store best cut partitions and cut size.
best_qaoa_S_partition = set()
best_qaoa_T_partition = set()
best_qaoa_cut_size = -9999

# Analyze each candidate cut.
for i in range(num_cuts):
  candidate = candidate_cuts.iloc[i]
  one_qubits = set(candidate[candidate==1].index)
  S_partition = set()
  T_partition = set()
  for node in working_graph:
    if str(node) in one_qubits:
      # If a one was measured add node to S partition.
      S_partition.add(node)
    else:
      # Otherwise a zero was measured so add to T partition.
      T_partition.add(node)

  cut_size = nx.cut_size(
      working_graph, S_partition, T_partition, weight='weight')

  # If you found a better cut update best_qaoa_cut variables.
  if cut_size > best_qaoa_cut_size:
    best_qaoa_cut_size = cut_size
    best_qaoa_S_partition = S_partition
    best_qaoa_T_partition = T_partition
```

The QAOA is known to do just a little better better than random guessing for Max-Cut on 3-regular graphs at `p=1`. You can use very similar logic to the code above, but now instead of relying on the QAOA to decied your `S_partition` and `T_partition` you can just pick then randomly:

```
import random

best_random_S_partition = set()
best_random_T_partition = set()
best_random_cut_size = -9999

# Randomly build candidate sets.
for i in range(num_cuts):
  S_partition = set()
  T_partition = set()
  for node in working_graph:
    if random.random() > 0.5:
      # If we flip heads add to S.
      S_partition.add(node)
    else:
      # Otherwise add to T.
      T_partition.add(node)

  cut_size = nx.cut_size(
      working_graph, S_partition, T_partition, weight='weight')

  # If you found a better cut update best_random_cut variables.
  if cut_size > best_random_cut_size:
    best_random_cut_size = cut_size
    best_random_S_partition = S_partition
    best_random_T_partition = T_partition
```

```
print('-----QAOA-----')
output_cut(best_qaoa_S_partition)

print('-----RANDOM-----')
output_cut(best_random_S_partition)
```

```

```


```python
import os
import sys
from ef.config.config import Config
from ef.config.components import *
```

In this example, we compare an exact analytical trajectory of a charged particle moving in a uniform magnetic field with results of a numerical simulation.

A charged particle moves in uniform magnetic field in circular trajectory.
An exact expressions for the trajectory can be obtained by solving Newton's equation:

$$
m \frac{ d \textbf{v} }{ dt } = \frac{q}{c} [ \textbf{v} \ \textbf{H} ]
$$

We chose z-axis to be directed along the magnetic field.
Then the previous expression becomes:

\begin{align}
& m v_x' = \frac{q}{c} v_y H_z \\
& m v_y' = -\frac{q}{c} v_x H_z \\
& m v_z' = 0
\end{align}

Performing integration over time (see [Single Particle in Uniform Magnetic Field](https://github.com/epicf/ef/blob/dev/examples/single_particle_in_magnetic_field/Single%20Particle%20in%20Uniform%20Magnetic%20Field.ipynb) [SymPy](http://www.sympy.org/en/index.html) notebook), it is possible to obtain
particle's law of motion:

\begin{align}
&v_x = v_{x0} \cos( \Omega t ) + v_{y0} \sin( \Omega t )
\\
&v_y = -v_{x0} \sin( \Omega t ) + v_{y0} \cos( \Omega t )
\\
&v_z = v_{z0}
\end{align}

\begin{align}
&x = x_0 + \frac{1}{\Omega} \Big[ v_{x0} \sin( \Omega t ) - v_{y0} \cos( \Omega t ) + v_{y0} \Big]
\\
&y = y_0 + \frac{1}{\Omega} \Big[ v_{x0} \cos( \Omega t ) + v_{y0} \sin( \Omega t ) - v_{x0} \Big]
\\
&z = z_0 + v_{z0} t
\end{align}

The parameters of the [circular motion](https://en.wikipedia.org/wiki/Gyroradius) - cyclotron frequency $\Omega$ and Larmor radius $r$ - are

$$
\Omega = \frac{q H_z}{m c},
\qquad
r = \frac{m c v_{\perp}}{q H_z} = \frac{m c (v_x^2 + v_y^2)^{1/2}}{q H_z}
$$

Let's perform some estimates (see below).
Suppose there is an electron (`q = 4.8e-10 [cgs]`, `m = 9.1e-28 [g]`) moving
in the `1000 [Gs] = 1 * 10^-1 [T]` magnetic field. It's speed along the field (z-axis) corresponds
to 1 keV energy, and it's speed in perpendicular plane corresponds to energy 100 eV (initially distributed
evenly between x- and y-components).

```python
from math import *

m = 9.8e-28
q = 4.8e-10
print( "q = {:.3e} [cgs]".format( q ) )
print( "m = {:.3e} [g]".format( m ) )

ev_to_cgs = 1.60218e-12
E_along = 1000 * ev_to_cgs
v_along = sqrt( 2 * E_along / m )
E_perp = 100 * ev_to_cgs
v_perp = sqrt( 2 * E_perp/2 / m )
print( "E_along = {:.3e} [eV] = {:.3e} [erg]".format( E_along / ev_to_cgs, E_along ) )
print( "E_perp = {:.3e} [eV] = {:.3e} [erg]".format( E_perp / ev_to_cgs, E_perp ) )
print( "v_along = {:.3e} [cm/s]; p_along = {:.3e} [g * cm/s]".format( v_along, v_along * m ) )
print( "v_perp = {:.3e} [cm/s]; p_perp = {:.3e} [g * cm/s]".format( v_perp, v_perp * m ) )
```

    q = 4.800e-10 [cgs]
    m = 9.800e-28 [g]
    E_along = 1.000e+03 [eV] = 1.602e-09 [erg]
    E_perp = 1.000e+02 [eV] = 1.602e-10 [erg]
    v_along = 1.808e+09 [cm/s]; p_along = 1.772e-18 [g * cm/s]
    v_perp = 4.043e+08 [cm/s]; p_perp = 3.962e-19 [g * cm/s]

It's cyclotron frequency and Larmor radius are `Omega = 1.633e+10 [1/s], r = 3.502e-02 [cm] ~ 0.035 [mm]`.
Gyration period equals to `3.848e-10 [s]` and distance in the direction of the field covered
during single revolution is `6.959e-01 [cm] ~ 7 [mm]`

```python
H = 1000
speed_of_light = 3.0e10
cyclotron_fr = q * H / m / speed_of_light
cyclotron_period = 2.0 * pi / cyclotron_fr
single_period_distance_along_field = v_along * cyclotron_period
larmor_r = m * speed_of_light * sqrt(2 * E_perp / m) / q / H
print( "H = {:.3e} [Gs]".format( H ) )
print( "c = {:.3e} [cm/s]".format( speed_of_light ) )
print( "Omega = {:.3e} [1/s]".format( cyclotron_fr ) )
print( "Cyclotron period = {:.3e} [s]".format( cyclotron_period ) )
print( "Single period distance along field= {:.3e} [cm]".format( single_period_distance_along_field ) )
print( "Larmor_r = {:.3e} [cm]".format( larmor_r ) )
```

    H = 1.000e+03 [Gs]
    c = 3.000e+10 [cm/s]
    Omega = 1.633e+10 [1/s]
    Cyclotron period = 3.848e-10 [s]
    Single period distance along field= 6.959e-01 [cm]
    Larmor_r = 3.502e-02 [cm]

Now, to check this, in config file we define a source which
generates a single particle at startup. Config is mostly similar
to ['Singe Particle in Free Space'](https://github.com/epicf/ef/wiki/Single-Particle-In-Free-Space) example, except this time we need nonzero magnetic field:

```python
external_fields = [ExternalFieldUniformConf(electric_or_magnetic="magnetic", field = (0, 0, 1000))]

```

Total simulation time has been adjusted to approximately 10 full gyration cycles (more precisely, 3.0e-9/3.848e-10 = 7.8 revolutions ).

```python
ten_revolutions_time = 10 * cyclotron_period
print( "10 revolutions time = {:.3e} [s]".format( ten_revolutions_time ) )

sim_time = 3.0e-9
n_of_revolutions = sim_time / cyclotron_period
n_of_steps = 1000
dt = sim_time / n_of_steps
print( "simulation_time = {:.3e} [s]".format( sim_time ) )
print( "n_of_revolutions = {:.1f}".format( n_of_revolutions ) )
print( "number_of_time_steps = {:d}".format( n_of_steps ) )
print( "time_step_size = {:.3e} [s]".format( dt ) )
```

    10 revolutions time = 3.848e-09 [s]
    simulation_time = 3.000e-09 [s]
    n_of_revolutions = 7.8
    number_of_time_steps = 1000
    time_step_size = 3.000e-12 [s]

```python
time_grid = TimeGridConf(total = 3.0e-9, step = 3.0e-12, save_step = 3.0e-12 )
```

Distance covered is

```python
z_distance = 5
t = z_distance / v_along
print( "z_distance = {:f} [cm]".format( z_distance ) )
print( "t = {:.3e} [s]".format( t ) )

```

    z_distance = 5.000000 [cm]
    t = 2.765e-09 [s]

_construct remaining fields and run_

Processing of simulation results is also mostly similar to the [previous example](https://github.com/epicf/ef/wiki/Single-Particle-in-Free-Space).
The law of motion is more complicated this time, and to define it we need not only
particle mass and initial position and momentum, but also it's charge and magnitude of the magnetic field.
All of this can be extracted from the relevant sections of the output `*.h5` files.
(see functions `eval_an_trajectory_at_num_time_points`, `velocities` and `coords` at
[plot.py](https://github.com/epicf/ef/blob/dev/examples/single_particle_in_magnetic_field/plot.py))

_functions_

Trajectory plots:

_figures_

Apart from the trajectory, kinetic energy is also plotted.
In theory, particle should not gain or loss any energy by interacting with magnetic field.
However, [due to numerical errors](https://en.wikipedia.org/wiki/Energy_drift), this might not be the case in simulations. In this case, the energy is conserved.

_figures_

**Long Simulation Time**
[Boris](https://en.wikipedia.org/wiki/Particle-in-cell#The_particle_mover) integration scheme is [notable](http://aip.scitation.org/doi/abs/10.1063/1.4818428?journalCode=php) for energy conservation even for long simulation times.

_config and results_

**Time Step Larger Than Gyration Period**
In practice it is necessary to keep in mind that time step should not exceed gyration period.
Otherwise, this will lead to erroneous results.

_config and results_

```python

```


```python
import os
import numpy as np
import pandas as pd
import time
from sympy import isprime, primerange
import random
from itertools import permutations
import threading
```

```python
# Config here. These are low to run fast in a script...
idx_range = 7 # permutations is the factorial of this...
num_threads = 8
offset = 0
max_tries = 100

initial_path = pd.read_csv('submission.151557248.csv')
cities = pd.read_csv('../data/raw/cities.csv')

pnums = [i for i in primerange(0, 197770)]
```

```python
def score_path(path_df):
    path_df['step'] = np.sqrt((path_df.X - path_df.X.shift())**2 +
                              (path_df.Y - path_df.Y.shift())**2)
    path_df['step_adj'] = np.where((path_df.index) % 10 != 0,
                                   path_df.step,
                                   path_df.step +
                                   path_df.step*0.1*(~path_df.CityId.shift().isin(pnums)))
    return path_df.step_adj.sum()
```

```python
path_df = cities.reindex(initial_path['Path']).reset_index()
start_idx = path_df.loc[path_df['CityId'] == 0].index
a = path_df.iloc[start_idx].iloc[0]
b = path_df.iloc[start_idx].iloc[1]

path_df_best = path_df.copy()
best_score = score_path(path_df_best)
print("Try for a good shuffle")
for i in range(10):
    path_df= path_df_best.copy()
    #shufle
    path_df = path_df.sample(frac=1).reset_index(drop=True)
    # always start at 0
    curr_idx = path_df.loc[path_df['CityId'] == 0].index
    #copy the wrong ones into place
    path_df.iloc[curr_idx[0]] = path_df.iloc[start_idx[0]]
    path_df.iloc[curr_idx[1]] = path_df.iloc[start_idx[1]]
    #start and end to 0
    path_df.iloc[start_idx[0]] = a
    path_df.iloc[start_idx[1]] = b

    score = score_path(path_df)
    print(str(score).ljust(20) + " / " + str(best_score).ljust(20))
    if score < best_score:
        best_score = score
        path_df_best = path_df.copy()

path_df = path_df_best.copy()
curr_idx = path_df.loc[path_df['CityId'] == 0].index
print(curr_idx)
```

```python
path_df_best = path_df.copy()
best_score = score_path(path_df_best)
score = best_score
max_idx=path_df['Path'].shape[0]

print( "Initial best: " + str(best_score))
try:
    for ix in range(offset,min(max_tries,int(max_idx-(idx_range+1)))):
        t = time.time()
        path_df = path_df_best.copy() #start at best
        start_loc = random.randint(1,max_idx-(idx_range+1)-1)
        i = start_loc

        rand_idx = random.randint(1,max_idx-(idx_range+1)-1)
        # start indexes are for current best
        start_idxes = list(range(start_loc,start_loc+idx_range))
        dest_idxes = list(range(start_loc,start_loc+idx_range))

        #add a random in to get lucky?
        start_idxes.pop()
        dest_idxes.pop()
        start_idxes.append(rand_idx)
        dest_idxes.append(rand_idx)
        # generate all permutations
        pms = np.array(list(set(permutations(dest_idxes))))

        # setup threads
        tmp_scores = num_threads*[None]
        tmp_swaps = num_threads*[None]
        threads = []
        thread_input = num_threads*[None]
        thread_idx = 0
        batch = pms.shape[0] // (num_threads)

        # split permutations into batches per thread
        for start in range(0, pms.shape[0], batch):
            end = min(pms.shape[0]-1, start + batch)
            thread_input[thread_idx]  = pms[start:end, start:end]
            thread_idx += 1

        # worker iterates through permutations and gets the score
        def worker(data_idx):
            wpms = thread_input[data_idx].copy()
            wpath_df = path_df_best.copy()
            scores = []
            swaps=[]
            if wpms is not None:
                for dest_idx in wpms:
                    # skip the known solution
                    if list(start_idxes) == list(dest_idx):
                        continue
                    # swap locations
                    for i in range(len(dest_idx)):
                        wpath_df.iloc[dest_idx[i]] = path_df_best.iloc[start_idxes[i]]

                    # calculate the score
                    s = score_path(wpath_df)
                    if len(list(dest_idx)) == len(list(start_idxes)): # why would this happen?
                        scores.append(s)
                        swaps.append([list(start_idxes),list(dest_idx)])

            # collect the results
            tmp_scores[data_idx] = scores
            tmp_swaps[data_idx] = swaps

        #start threads
        for j in range(num_threads):
            th = threading.Thread(target=worker, args=(j,), daemon=True)
            th.start()
            threads.append(th)

        #join threads, wait for completion
        for th in threads:
            if th is not None:
                th.join()

        # put together results
        x = np.concatenate(tmp_scores)

        # remove empties from the end
        l = tmp_swaps.pop()
        while len(l) == 0:
            l = tmp_swaps.pop()
        tmp_swaps.append(l) #append back the non empty

        s = np.concatenate(tmp_swaps)
        lowest = np.min(x)
        # print results
        print(str(i).ljust(6) + " : " +str(ix).rjust(6)+" / " +  str(min([int(max_idx-(idx_range+1)),max_tries])).ljust(6) +
            " CITY: " + str(path_df.iloc[i]['Path']).ljust(10) +
            " BATCH_SIZE: "+str(len(pms)))
        print("BEST: " + str(lowest).ljust(19) + " / " + str(best_score).ljust(19) )
        print("PM:" + str(s[np.argmin(x)][1]) )
        print("T:" + str(time.time() - t))
        # save the best if there is a better one
        if lowest < best_score:
            print("NEW BEST: " + str(np.min(x)))
            start_idx = s[np.argmin(x)][0]
            dest_idx = s[np.argmin(x)][1]
            print(start_idx)
            print(dest_idx)
            path_df = path_df_best.copy()
            for i in range(len(dest_idx)):
                path_df.iloc[dest_idx[i]] = path_df_best.iloc[start_idx[i]]
            print(score_path(path_df))
            path_df_best = path_df.copy()
            best_score = lowest
except Exception as e:
    raise e
except: # keyboard exception, save the best result
    out_df = path_df_best[['Path']].astype(int)
    out_df.to_csv(str(best_score)+'santa2018.csv', index=False)
    print("\nSAVED: "+str(best_score)+'santa2018.csv')
```

```python
# get to the end? save the best
out_df = path_df_best[['Path']].astype(int)
out_df.to_csv(str(best_score)+'santa2018.csv', index=False)
print("\nSAVED: "+str(best_score)+'santa2018.csv')
```


# Definition of the network properties.

```
#Import modules.
import numpy as np
import sympy as sp
import sys
from sympy import symbols, diff
from numpy.linalg import multi_dot
import math
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
```

```
n = 16 #number of measuremens.
v = 5 #number of network nodes.
d = 3 #imperfections : position and orientation = 2+1 =3
m = 2*v-d #number of unknown parameters.
r = n - m #degrees of freedom.

#Set numpy's print options.
np.set_printoptions(suppress=True,threshold=np.inf,linewidth=300,precision=15)

print("The design matrix A has dimensions of {}x{} elements".format(n,m))
print("The weight matrix P has dimensions of {}x{} elements".format(n,n))

#We assume that:
# 0 = Α
# 1 = B = constant and a_12 = a_BC = constant
# 2 = C
# 3 = D
# 4 = E
```

    The design matrix A has dimensions of 16x7 elements
    The weight matrix P has dimensions of 16x16 elements

```
#Matrices of measurements and standard errors.
l = np.array([64.1902,50.8882,32.4675,60.7616,26.2679,43.1958,92.6467,29.5762,106.2276,116.6508,112.9705,64.1595,490.249,220.725,791.552,659.535]) #array which includes all measurements. (angles+lengths)
noa = 12 #number of angles
sigma_1 = np.array([25]*noa) #the standard error is in cc!
sigma_2 = np.array([0.012]*(l.shape[0]-noa)) #the standard error is in meters!
sigma = np.concatenate((sigma_1,sigma_2))
```

```
#Temporary coordinates of network nodes.
x = np.array([1586.537,2075.094,2222.679,1449.130,1688.320])
y = np.array([937.235,896.541,354.801,522.664,741.395])

#Temporary distance S12.
S12 = np.sqrt((x[2]-x[1])**2+(y[2]-y[1])**2)

#Matrix of unknown parameters Χ : xA,yΑ,xA,yΑ,xA,yΑ,SBC
X=np.array([1586.537,  937.235, 1449.13 ,  522.664, 1688.320, 741.395, S12])
X
```

    array([1586.537          ,  937.235          , 1449.13           ,  522.664          , 1688.32           ,  741.395          ,  561.4833566767585])

```
#Create the necessary variables.
b_jik, a_ik, a_ij = symbols("b_jik, a_ik, a_ij ")
y_i,y_j,y_k = symbols("y_i,y_j,y_k")
x_i,x_j,x_k = symbols("x_i,x_j,x_k")
S_ij,S_ik = symbols("S_ij,S_ik")
dx_i,dx_j,dx_k = symbols("dx_i,dx_j,dx_k")
dy_i,dy_j,dy_k = symbols("dy_i,dy_j,dy_k")

#Auxiliary indices.
jj = np.array([2,5,1,5,4,5,3,5,1,4,3,2,2,5,3,3])-1
ii = np.array([1,1,4,4,3,3,2,2,5,5,5,5,1,1,4,5])-1
kk = np.array([5,4,5,3,5,2,5,1,2,1,4,3])-1
```

```
#Linearized angle equations.
angle_eq = (((y_j-y_i)/S_ij**2 - (y_k-y_i)/S_ik**2)*dx_i + ((x_k-x_i)/S_ik**2 - (x_j-x_i)/S_ij**2)*dy_i
            - dx_j*(y_j-y_i)/S_ij**2 + dy_j*(x_j-x_i)/S_ij**2 + dx_k*(y_k-y_i)/S_ik**2 - dy_k*(x_k-x_i)/S_ik**2
           )
angle_eq
```

$\displaystyle dx_{i} \left(- \frac{- y_{i} + y_{k}}{S_{ik}^{2}} + \frac{- y_{i} + y_{j}}{S_{ij}^{2}}\right) + dy_{i} \left(\frac{- x_{i} + x_{k}}{S_{ik}^{2}} - \frac{- x_{i} + x_{j}}{S_{ij}^{2}}\right) + \frac{dx_{k} \left(- y_{i} + y_{k}\right)}{S_{ik}^{2}} - \frac{dy_{k} \left(- x_{i} + x_{k}\right)}{S_{ik}^{2}} - \frac{dx_{j} \left(- y_{i} + y_{j}\right)}{S_{ij}^{2}} + \frac{dy_{j} \left(- x_{i} + x_{j}\right)}{S_{ij}^{2}}$

```
#Linearized distance equations.
dist_eq = -(x_j-x_i)/S_ij*dx_i-dy_i*(y_j-y_i)/S_ij+(x_j-x_i)/S_ij*dx_j+dy_j*(y_j-y_i)/S_ij
dist_eq
```

$\displaystyle \frac{dx_{i} \left(x_{i} - x_{j}\right)}{S_{ij}} + \frac{dx_{j} \left(- x_{i} + x_{j}\right)}{S_{ij}} - \frac{dy_{i} \left(- y_{i} + y_{j}\right)}{S_{ij}} + \frac{dy_{j} \left(- y_{i} + y_{j}\right)}{S_{ij}}$

```
def finda(dx,dy,a):
  '''This function calculates the true value of an azimuth angle'''
  if dx>0:
    if dy>0:return 200*a/math.pi
    if dy==0:return 100
    if dy<0:return 200+200*a/math.pi
  if dx<0:
    if dy>0:return 400+200*a/math.pi
    if dy==0:return 300
    if dy<0:return 200+200*a/math.pi
  if dx==0:
    if dy>0:return 0
    if dy==0:return print("Division by 0!")
    if dy<0:return 200
```

# Creation of design matrix Α.

```
#Create auxiliary vector.
dx_0,dy_0,dx_1,dy_1,dx_2,dy_2,dx_3,dy_3,dx_4,dy_4,dS_12 = symbols('dx_0,dy_0,dx_1,dy_1,dx_2,dy_2,dx_3,dy_3,dx_4,dy_4,dS_12')

DX = [dx_0,dy_0,
      dx_3,dy_3,
      dx_4,dy_4,
      dS_12]
DX
```

    [dx_0, dy_0, dx_3, dy_3, dx_4, dy_4, dS_12]

```
#Construction of the 16 observation equations.
eqs = np.empty(shape=(n,), dtype=object)
for i in range(eqs.shape[0]):
  Sij = np.sqrt((x[jj[i]]-x[ii[i]])**2+(y[jj[i]]-y[ii[i]])**2)
  if i<=noa-1:
    Sik = np.sqrt((x[kk[i]]-x[ii[i]])**2+(y[kk[i]]-y[ii[i]])**2)
    eqs[i]=angle_eq.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(x_k,x[kk[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]]),(S_ij,Sij),(S_ik,Sik),
                        (dx_i,symbols('dx_{}'.format(ii[i]))),
                        (dx_j,symbols('dx_{}'.format(jj[i]))),
                        (dx_k,symbols('dx_{}'.format(kk[i]))),
                        (dy_i,symbols('dy_{}'.format(ii[i]))),
                        (dy_j,symbols('dy_{}'.format(jj[i]))),
                        (dy_k,symbols('dy_{}'.format(kk[i])))
                        ])*636620
  else:
    eqs[i] = dist_eq.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(S_ij,Sij),
                        (dx_i,symbols('dx_{}'.format(ii[i]))),
                        (dx_j,symbols('dx_{}'.format(jj[i]))),
                        (dy_i,symbols('dy_{}'.format(ii[i]))),
                        (dy_j,symbols('dy_{}'.format(jj[i]))),
                        ])
```

```
#Calculate the true value of the azimuth angle a_BC = a_12.
a = math.atan((x[2]-x[1])/(y[2]-y[1]))
a_12 = finda(x[2]-x[1],y[2]-y[1],a)
```

```
#Replace the variables with their true values.
for i,eq in enumerate(eqs):
  eqs[i]=eq.subs([(dx_1,x[1]),(dy_1,x[2])])
  eqs[i]=eq.subs([(dx_2,dS_12*math.sin(a_12*math.pi/200)),(dy_2,dS_12*math.cos(a_12*math.pi/200))])
```

```
#Differentiation of each equation with each one of the unknown parameters and creation of design matrix Α.
A = np.zeros(shape=(n,m))
for i in range(0,n):
  for j in range(0,m):
      A[i,j] = diff(eqs[i],DX[j])
print('Array A is:\n')
print(A)
```

    Array A is:

    [[ 2451.597762888792       36.094803413594725     0.                    0.                -2559.38751561248     -1330.1784084027          0.               ]
     [-1175.7729943068243   -1788.7689044312356   -1383.6145213056554     458.5904960285357    2559.38751561248      1330.1784084027          0.               ]
     [-1383.6145213056554     458.5904960285357      58.13369594764356    990.8691535747793    1325.4808253580118   -1449.4596496033148       0.               ]
     [    0.                    0.                 1496.0400983799211    -663.485640519568    -1325.4808253580118    1449.4596496033148     713.5056891859489  ]
     [    0.                    0.                 -170.5592730219094    -785.9740090837469     565.7853005956594     782.0412821745708    -107.67896863959092 ]
     [    0.                    0.                    0.                    0.                 -565.7853005956594    -782.0412821745708    -605.8267205463579  ]
     [    0.                    0.                    0.                    0.                 -568.7351106339293    1417.838382428985       -0.000000000000057]
     [  107.7897527236875    1294.0836049891052       0.                    0.                  568.7351106339293   -1417.838382428985        0.               ]
     [-2559.38751561248     -1330.1784084027          0.                    0.                 1990.6524049785507    2748.016790831685        0.               ]
     [ 2559.38751561248      1330.1784084027       1325.4808253580118   -1449.4596496033148   -3884.8683409704918     119.28124120061494      0.               ]
     [    0.                    0.                -1325.4808253580118    1449.4596496033148     759.6955247623524   -2231.500931777886     -605.8267205463579  ]
     [    0.                    0.                    0.                    0.                 1134.5204112295885    -635.7971002544142     605.8267205463579  ]
     [   -0.996548978941336     0.083006822436356     0.                    0.                    0.                    0.                    0.               ]
     [   -0.461160850595852     0.887316555620209     0.                    0.                    0.461160850595852    -0.887316555620209     0.               ]
     [    0.                    0.                   -0.977254925040277     0.212067940727783     0.                    0.                    0.46148091700447 ]
     [    0.                    0.                    0.                    0.                   -0.810198182548022     0.586156041507619     0.778504416025518]]

# Creation of matrix of the calculated values.

```
#Creation of matrix of the calculated values.
delta_l = np.zeros(shape=(n,))

for i in range(n):

  if i<=noa-1: #If its a angle equation.

    div1 = (x_k-x_i)/(y_k-y_i)
    div2 = (x_j-x_i)/(y_j-y_i)

    d1=float(div1.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(x_k,x[kk[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]])]))
    d2=float(div2.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]])]))

    aik_ = np.arctan(d1)
    aij_ = np.arctan(d2)

    #Azimuth angle calculation.
    deltaX = x_k-x_i
    deltaY = y_k-y_i
    deltaX = float(deltaX.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(x_k,x[kk[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]])]))
    deltaY = float(deltaY.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(x_k,x[kk[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]])]))
    aik=finda(deltaX,deltaY,aik_)

    deltaX = x_j-x_i
    deltaY = y_j-y_i
    deltaX = float(deltaX.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(x_k,x[kk[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]])]))
    deltaY = float(deltaY.subs([(x_i,x[ii[i]]),(x_j,x[jj[i]]),(x_k,x[kk[i]]),(y_i,y[ii[i]]),(y_j,y[jj[i]]),(y_k,y[kk[i]])]))
    aij=finda(deltaX,deltaY,aij_)

    delta_l[i]=(l[i]-aik+aij) #in cc
    while delta_l[i]>399: delta_l[i]-=400
    delta_l[i]=delta_l[i]*10000 #convertion from grad to cc.

  else: #If its a distance equation.

    Sij = np.sqrt((x[jj[i]]-x[ii[i]])**2+(y[jj[i]]-y[ii[i]])**2) #distance calculation.
    delta_l[i]=l[i]-Sij #in meters

delta_l
```

    array([-59.5602269456208  ,  -1.328985675570493,   0.54263166884283 ,   0.376205193788337,  -0.273873615697084,   0.715280497161075,   0.054504408808498,   0.47446446842514 ,  -0.91423752337505 ,  65.78635400671828 ,  -0.102331578091253,  19.23021509455225 ,   0.000140863132685,
             0.014590891140813,  -0.000930744369043,  -0.006099338775243])

# Creation of the weight matrix P.

```
#Define the a-priori standard error.
sigma_0 = 1

I = np.identity(n)
P = I*(sigma_0/sigma)**2

print('Array P is:\n')
print(P)
```

    Array P is:

    [[   0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.0016            0.                0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.             6944.444444444443    0.                0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.             6944.444444444443    0.                0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.             6944.444444444443    0.            ]
     [   0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.                0.             6944.444444444443]]

#System solution.

```
#Calculate the new array with the adjusted coordinate values of network nodes A,D,E and distance SBC.
delta_x=np.dot(np.linalg.inv(multi_dot([A.T,P,A])),multi_dot([A.T,P,delta_l]))
X_hat = X+delta_x
X_hat
```

    array([1586.537312058668 ,  937.2577022482286, 1449.1268707420757,  522.6529576436293, 1688.3252579564742,  741.4028311285244,  561.4760256350538])

```
#Points Β=1 και C=2.
x_1 = x[1]
y_1 = y[1]

#Calculate the final coordinates of the node C=2.

#With:
# x_2 = X_hat[-1]*math.sin(a_12*math.pi/200) + x[1]
# y_2 = X_hat[-1]*math.cos(a_12*math.pi/200) + y[1]

#Or:
x_2 = delta_x[-1]*math.sin(a_12*math.pi/200) + x[2]
y_2 = delta_x[-1]*math.cos(a_12*math.pi/200) + y[2]
```

```
#Create a new array with the adjusted coordinate values of all network nodes.
X_hat_extended = np.array([X_hat[0], X_hat[1], x_1,y_1,x_2,y_2, X_hat[2], X_hat[3],  X_hat[4], X_hat[5]])
X_hat_extended
```

    array([1586.537312058668 ,  937.2577022482286, 2075.094          ,  896.541          , 2222.6770730474427,  354.8080732613636, 1449.1268707420757,  522.6529576436293, 1688.3252579564742,  741.4028311285244])

#Calculation of the a-priori variance-covariance matrix.

```
#Define the a-priori standard error.
sigma_0 = 1

#Calculate the a-priori variance-covariance matrix.
V_x_hat = (sigma_0**2)*np.linalg.inv(multi_dot([A.T,P,A]))

V_x_hat
```

    array([[ 0.000111855583289,  0.000081966479424,  0.000034464531617,  0.000123432666338,  0.0000683837617  ,  0.000078153508078, -0.00000352086878 ],
           [ 0.000081966479424,  0.00026984660057 , -0.000019089145558,  0.000228408655411,  0.00004345516026 ,  0.000180927905431, -0.000089221479819],
           [ 0.000034464531617, -0.000019089145558,  0.000131260202433,  0.000100403170793,  0.000033844659902,  0.000000134504035,  0.00003871346756 ],
           [ 0.000123432666338,  0.000228408655411,  0.000100403170793,  0.000500814070989,  0.000031831032341,  0.000213233229347, -0.000077729952066],
           [ 0.0000683837617  ,  0.00004345516026 ,  0.000033844659902,  0.000031831032341,  0.000070642781435,  0.000031238568966,  0.000022568997358],
           [ 0.000078153508078,  0.000180927905431,  0.000000134504035,  0.000213233229347,  0.000031238568966,  0.000160533659507, -0.000081706315352],
           [-0.00000352086878 , -0.000089221479819,  0.00003871346756 , -0.000077729952066,  0.000022568997358, -0.000081706315352,  0.000191435396471]])

#Computation of the a posteriori standard error.

```
#Calculation of the a-posteriori standard error.
u = np.dot(A,delta_x)-delta_l
sigma_0_hat = np.sqrt(multi_dot([u.T,P,u])/(n-m))
sigma_0_hat
```

    0.9781132765786059

```
#Create the necessary auxiliary variables and vectors.
x_0,y_0,x_1,y_1,x_2,y_2,x_3,y_3,x_4,y_4,S_12 = symbols('x_0,y_0,x_1,y_1,x_2,y_2,x_3,y_3,x_4,y_4,S_12')
DX_1 = np.array([x_0,y_0,x_3,y_3,x_4,y_4,S_12*math.sin(a_12*math.pi/200),S_12*math.cos(a_12*math.pi/200),x_1,y_1])
DX_2 = np.array([x_0,y_0,x_3,y_3,x_4,y_4,S_12])

#Calculation of the necessary Jacobian matrix for the propagation of uncertainty between the two vectors DX_1, DX_2.
J = np.zeros(shape=(2*v,V_x_hat.shape[0]))
for i in range(0,2*v):
  for j in range(V_x_hat.shape[0]):
      J[i,j] = diff(DX_1[i],DX_2[j])
print('Array J is:\n')
print(J)
```

    Array J is:

    [[ 1.                 0.                 0.                 0.                 0.                 0.                 0.               ]
     [ 0.                 1.                 0.                 0.                 0.                 0.                 0.               ]
     [ 0.                 0.                 1.                 0.                 0.                 0.                 0.               ]
     [ 0.                 0.                 0.                 1.                 0.                 0.                 0.               ]
     [ 0.                 0.                 0.                 0.                 1.                 0.                 0.               ]
     [ 0.                 0.                 0.                 0.                 0.                 1.                 0.               ]
     [ 0.                 0.                 0.                 0.                 0.                 0.                 0.262848396564252]
     [ 0.                 0.                 0.                 0.                 0.                 0.                -0.964837147099759]
     [ 0.                 0.                 0.                 0.                 0.                 0.                 0.               ]
     [ 0.                 0.                 0.                 0.                 0.                 0.                 0.               ]]

```
#Calculation of the a-priori variance-covariance matrix of the coordinates of all network nodes.
V_x_hat_extended = multi_dot([J,V_x_hat,J.T])
V_x_hat_extended_df = pd.DataFrame(V_x_hat_extended,index=["0","00","3","33","4","44","2","22","1","11"], columns=["0","00","3","33","4","44","2","22","1","11"])
V_x_hat_extended_df=V_x_hat_extended_df.sort_index()
V_x_hat_extended_df=V_x_hat_extended_df.reindex(sorted(V_x_hat_extended_df.columns), axis=1)
V_x_hat_extended = np.array(V_x_hat_extended_df)
V_x_hat_extended
```

    array([[ 0.000111855583289,  0.000081966479424,  0.               ,  0.               , -0.000000925454713,  0.000003397064989,  0.000034464531617,  0.000123432666338,  0.0000683837617  ,  0.000078153508078],
           [ 0.000081966479424,  0.00026984660057 ,  0.               ,  0.               , -0.000023451722909,  0.000086084198048, -0.000019089145558,  0.000228408655411,  0.00004345516026 ,  0.000180927905431],
           [ 0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ],
           [ 0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ,  0.               ],
           [-0.000000925454713, -0.000023451722909,  0.               ,  0.               ,  0.000013226133628, -0.000048549145451,  0.000010175772873, -0.000020431193266,  0.000005932224768, -0.00002147637398 ],
           [ 0.000003397064989,  0.000086084198048,  0.               ,  0.               , -0.000048549145451,  0.000178209262843, -0.000037352191594,  0.000074996745196, -0.000021775407024,  0.000078833288205],
           [ 0.000034464531617, -0.000019089145558,  0.               ,  0.               ,  0.000010175772873, -0.000037352191594,  0.000131260202433,  0.000100403170793,  0.000033844659902,  0.000000134504035],
           [ 0.000123432666338,  0.000228408655411,  0.               ,  0.               , -0.000020431193266,  0.000074996745196,  0.000100403170793,  0.000500814070989,  0.000031831032341,  0.000213233229347],
           [ 0.0000683837617  ,  0.00004345516026 ,  0.               ,  0.               ,  0.000005932224768, -0.000021775407024,  0.000033844659902,  0.000031831032341,  0.000070642781435,  0.000031238568966],
           [ 0.000078153508078,  0.000180927905431,  0.               ,  0.               , -0.00002147637398 ,  0.000078833288205,  0.000000134504035,  0.000213233229347,  0.000031238568966,  0.000160533659507]])

#Graphical representation of the error ellipses.

```
#Final coordinates of network nodes.
x_hat = np.zeros(shape=(v,))
y_hat = np.zeros(shape=(v,))

j=0
for i in range(0,v):
  x_hat[i] = X_hat_extended[j]
  y_hat[i] = X_hat_extended[j+1]
  if j%2==0:j+=2

#Network edges.
lines_x = np.concatenate((x_hat,np.array([x_hat[0],x_hat[4],x_hat[1],x_hat[4],x_hat[2],x_hat[4],x_hat[3],x_hat[4],x_hat[0],x_hat[3]])))
lines_y = np.concatenate((y_hat,np.array([y_hat[0],y_hat[4],y_hat[1],y_hat[4],y_hat[2],y_hat[4],y_hat[3],y_hat[4],y_hat[0],y_hat[3]])))
X_hat_extended
```

    array([1586.537312058668 ,  937.2577022482286, 2075.094          ,  896.541          , 2222.6770730474427,  354.8080732613636, 1449.1268707420757,  522.6529576436293, 1688.3252579564742,  741.4028311285244])

```
def auxfunc(V_xy):

  '''This function takes as argument the variance-covariance submatrix of the corresponding
  network node or edge, and as output it returns the absolute or relative ellipse properties.'''

  width, height, angle =0,0,0

  if (V_xy[0,1]+V_xy[0,0]-V_xy[1,1])!=0:

    #Define equations for the calculation of the semi-major axis, the semi-minor axis and the orientation of the error ellipse.
    sigma_x_sq,sigma_y_sq,sigma_xy = symbols('sigma_x_sq,sigma_y_sq,sigma_xy')
    sigma_max_squared = ((sigma_x_sq+sigma_y_sq)+((sigma_x_sq-sigma_y_sq)**2+4*sigma_xy**2)**0.5)/2
    sigma_min_squared = ((sigma_x_sq+sigma_y_sq)-((sigma_x_sq-sigma_y_sq)**2+4*sigma_xy**2)**0.5)/2
    tan_2theta = 2*sigma_xy/(sigma_x_sq-sigma_y_sq)

    #Calculate the length of the semi-major and the semi-minor ellipse axes.
    sigma_max_squared = sigma_max_squared.subs([(sigma_x_sq,V_xy[0,0]),(sigma_y_sq,V_xy[1,1]),(sigma_xy,V_xy[0,1])])
    sigma_min_squared = sigma_min_squared.subs([(sigma_x_sq,V_xy[0,0]),(sigma_y_sq,V_xy[1,1]),(sigma_xy,V_xy[0,1])])
    sigma_u=sigma_max_squared**0.5
    sigma_v=sigma_min_squared**0.5
    width = 2*sigma_u
    height = 2*sigma_v

    #Calculate the orientation of the error ellipse.
    tan_2theta = tan_2theta.subs([(sigma_x_sq,V_xy[0,0]),(sigma_y_sq,V_xy[1,1]),(sigma_xy,V_xy[0,1])])
    theta = math.atan(tan_2theta)*180/(2*math.pi)

    #Extract the variances and the covariances from the input matrix.
    sigma_x=V_xy[0,0]**0.5
    sigma_y=V_xy[1,1]**0.5
    sigma_xy=V_xy[0,1]

    #Angle investigation.
    if sigma_x>sigma_y:
      if sigma_xy>0:angle=theta
      if sigma_xy<0:angle=theta+180
    if sigma_x<sigma_y:
      if sigma_xy>0:angle=theta+90
      if sigma_xy<0:angle=theta+90
    if sigma_x==sigma_y:
      if sigma_xy>0:angle=45
      if sigma_xy<0:angle=135

  return width, height, angle
```

```
def ellipse_args(netnodecode1, netnodecode2=9999, V_x_hat=V_x_hat_extended):

  '''This function takes as arguments the specific network node for the calculation of the absolute error ellipse properties, or
  the two network nodes of the corresponding network edge in wich we want to calculate the relative error ellipse arguments.
  Aditionally this function takes as argument the variance-covariance matrix of the coordinates of all network nodes. As output,
  it extracts the absolute or relative ellipse properties.'''

  netnodecode=netnodecode1
  if netnodecode < 5:

    if netnodecode2 == 9999: #If we want to calculate the absolute error ellipse.

      #Extract the variance-covariance submatrix of the given network node.
      V_xy = V_x_hat[2*netnodecode:2*netnodecode+2,2*netnodecode:2*netnodecode+2]
      width, height, angle = auxfunc(V_xy)
      return width, height, angle

    elif netnodecode2 < 5: #If we want to calculate the relative error ellipse.

      Jrij = np.array([[-1,0,1,0],[0,-1,0,1]])
      Vrig = np.ones(shape=(4,4))

      #Extract the variance-covariance submatrix of the given network edge.
      V_xy1 = V_x_hat[2*netnodecode1:2*netnodecode1+2,2*netnodecode1:2*netnodecode1+2]
      V_xy2 = V_x_hat[2*netnodecode2:2*netnodecode2+2,2*netnodecode2:2*netnodecode2+2]
      V1 = V_x_hat[2*netnodecode1:2*netnodecode1+2,2*netnodecode2:2*netnodecode2+2]
      V2 = V_x_hat[2*netnodecode2:2*netnodecode2+2,2*netnodecode1:2*netnodecode1+2]
      Vrig=np.asarray(np.bmat([[V_xy1, V1], [V2, V_xy2]]))

      VDrij = multi_dot([Jrij,Vrig,Jrij.T])
      width, height, angle = auxfunc(VDrij)
      return width, height, angle

  else: return print("There is no network node with the given code name!")
```

```
#Graphical representation of the error ellipses.

fig=plt.figure(figsize=(15,10))
ax = fig.add_subplot(111, aspect='equal')

#Define plot options.
scalefactor = 1000
ld=1.5
ellcolor = 'red'
ellzorder = 4
pointzorder = 5

#Plot network edges.
plt.plot(lines_x, lines_y, linewidth=0.6, markersize=8, alpha=1)

#Plot network nodes.
plt.scatter(x_hat, y_hat, marker="+", zorder=pointzorder, s=70, c='black')

#Plot nodenames.
nodenames=["A","B","C","D","E"]
for i, txt in enumerate(nodenames):
    ax.annotate(txt, (x_hat[i], y_hat[i]), xytext=(x_hat[i]+5,y_hat[i]+5), zorder=pointzorder)

#Absolute error ellipses of all network nodes.
width, height, angle = ellipse_args(0)
ax.add_artist(Ellipse(xy=X_hat_extended[:2], width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))
width, height, angle = ellipse_args(1)
ax.add_artist(Ellipse(xy=X_hat_extended[2:4], width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))
width, height, angle = ellipse_args(2)
ax.add_artist(Ellipse(xy=X_hat_extended[4:6], width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))
width, height, angle = ellipse_args(3)
ax.add_artist(Ellipse(xy=X_hat_extended[6:8], width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))
width, height, angle = ellipse_args(4)
ax.add_artist(Ellipse(xy=X_hat_extended[8:], width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))

#Relative error ellipses between each station pair.

#Edge AC.
width, height, angle = ellipse_args(0,2)
xy=(X_hat_extended[:2]+X_hat_extended[4:6])/2
xx = np.array([x_hat[0],x_hat[2]])
yy = np.array([y_hat[0],y_hat[2]])
plt.plot(xx, yy, "k--", linewidth=2, markersize=4)
plt.plot((x_hat[0]+x_hat[2])/2,(y_hat[0]+y_hat[2])/2, marker=".", color='red', mec="black", markersize=12, zorder=pointzorder)
ax.add_artist(Ellipse(xy=xy, width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))

#Edge AB.
width, height, angle = ellipse_args(0,1)
xy=(X_hat_extended[:2]+X_hat_extended[2:4])/2
xx = np.array([x_hat[0],x_hat[1]])
yy = np.array([y_hat[0],y_hat[1]])
plt.plot(xx, yy, "k--", linewidth=2, markersize=4)
plt.plot((x_hat[1]+x_hat[0])/2,(y_hat[1]+y_hat[0])/2, marker=".", color='red', mec="black", markersize=12, zorder=pointzorder)
ax.add_artist(Ellipse(xy=xy, width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))

#Edge DB.
width, height, angle = ellipse_args(3,1)
xy=(X_hat_extended[6:8]+X_hat_extended[2:4])/2
xx = np.array([x_hat[3],x_hat[1]])
yy = np.array([y_hat[3],y_hat[1]])
plt.plot(xx, yy, "k--", linewidth=2, markersize=4)
plt.plot((x_hat[3]+x_hat[1])/2,(y_hat[3]+y_hat[1])/2, marker=".", color='red', mec="black", markersize=12, zorder=pointzorder)
ax.add_artist(Ellipse(xy=xy, width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))

#Edge EC.
width, height, angle = ellipse_args(4,2)
xy=(X_hat_extended[8:]+X_hat_extended[4:6])/2
xx = np.array([x_hat[4],x_hat[2]])
yy = np.array([y_hat[4],y_hat[2]])
plt.plot(xx, yy, "k--", linewidth=2, markersize=4)
plt.plot((x_hat[4]+x_hat[2])/2,(y_hat[4]+y_hat[2])/2, marker=".", color='red', mec="black", markersize=12, zorder=pointzorder)
ax.add_artist(Ellipse(xy=xy, width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))

#Edge BC.
width, height, angle = ellipse_args(1,2)
xy=(X_hat_extended[2:4]+X_hat_extended[4:6])/2
xx = np.array([x_hat[1],x_hat[2]])
yy = np.array([y_hat[1],y_hat[2]])
plt.plot(xx, yy, "k--", linewidth=2, markersize=4)
plt.plot((x_hat[1]+x_hat[2])/2,(y_hat[1]+y_hat[2])/2, marker=".", color='red', mec="black", markersize=12, zorder=pointzorder)
ax.add_artist(Ellipse(xy=xy, width=width*scalefactor, height=height*scalefactor, angle=angle, fill=None, alpha=1, linewidth=ld, color=ellcolor, zorder=ellzorder))

plt.title("Absolute and Relative error ellipses - Minimum Εxternal Constraints")
plt.xlabel("X (m)")
plt.ylabel("Y (m)")
plt.show();
```

```
#Save the figure.
formatype='png'
fig.savefig("/content/sample_data/Error_Ellipses_Minimum_Εxternal_Constraints."+formatype, dpi=300, facecolor='w', edgecolor='w',
        orientation='portrait', papertype=None, format=formatype,
        transparent=False, bbox_inches=None, pad_inches=0.1,
        metadata=None);
```


```c++
//  Copyright (c) 2020 Patrick Diehl
//
//  SPDX-License-Identifier: BSL-1.0
//  Distributed under the Boost Software License, Version 1.0. (See accompanying
//  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
```

# Exercise 1: Classical linear elasticity model

Let $\Omega = (0,1) \subset \mathbb R$ and $\overline{\Omega}$ be the closure of $\Omega$, i.e.\ $\Omega=[0,1]$. The continuum local problem consists in finding the displacement $(u\in\overline{\Omega})$ such that:
\begin{align}
\label{eq:1dlinearelasticity}
- E u''(x) = f_b(x), &\quad \forall x \in \Omega, \\
\label{eq:Dirichlet}
u(x) = 0, &\quad \text{at}\ x=0,\\
\label{eq:Neumann}
Eu'(x) = g, &\quad \text{at}\ x=1,
\end{align}
where $E$ is the constant modulus of elasticity of the bar, $f_b=f_b(x)$ is a scalar function describing the external body force density (per unit length), and $g \in \mathbb R$ is the traction force applied at end point $x=1$.

```c++
#include <blaze/Math.h>
#include <BlazeIterative.hpp>
#include <cmath>
#include <iostream>
#include <vector>
#include<run_hpx.cpp>
#include<hpx/include/lcos.hpp>
#include<hpx/include/parallel_for_loop.hpp>
```

## Helpfer function for Blaze

[Blaze](https://bitbucket.org/blaze-lib/blaze/src/master/) is an open-source, high-performance C++ math library for dense and sparse arithmetic. This linrary has a HPX backend and utilzies the parallel algorithms to accelarted the linear algebara functions. Blaze is not covered in this cours and for more details we refer to

* K. Iglberger, G. Hager, J. Treibig, and U. Rüde: High Performance Smart Expression Template Math Libraries . Proceedings of the 2nd International Workshop on New Algorithms and Programming Models for the Manycore Era (APMM 2012) at HPCS 2012
* K. Iglberger, G. Hager, J. Treibig, and U. Rüde: Expression Templates Revisited: A Performance Analysis of Current Methodologies (Download). SIAM Journal on Scientific Computing, 34(2): C42--C69, 2012

```c++
// Generates a Blaze dynamic matrix of size N times N and fills the matrix with zeros
blaze::DynamicMatrix<double> zeroMatrix(unsigned long N)
{
return blaze::DynamicMatrix<double>( N, N, 0 );
};
```

```c++
// Generates a Blaze dynamic vector of size N and fills the vector with zeros
blaze::DynamicVector<double> zeroVector(unsigned long N)
{
return blaze::DynamicVector<double>(N,0);
};
```

```c++
// Solves the matrix system A \times x = b and returns x
blaze::DynamicVector<double> solve(blaze::DynamicMatrix<double> A, blaze::DynamicVector<double> b )
{
     blaze::iterative::BiCGSTABTag tag;
     tag.do_log() = true;
     return solve(A,b,tag);
}
```

## Force function

As the external load, a linear $force_b$ function $force : \mathbb{R} \rightarrow \mathbb{R}$

 $$ force_b(x)  = \begin{cases} 1, if x == 1, \\
 0 , else\end{cases}, x = [0,1]$$

```c++
double force(double x){

    if ( x == 1 )
        return 1;

    return 0;
}
```

## Discretization

As the domain $\overline{\Omega}$ we consider the intervall $[0,1]$ and discretize the interval with $n$ elements and using the spacing $h=\frac{1}{n}$ such that $x={0,1\cdot h,2\cdot h,\ldots,n\cdot h}$.

```c++
size_t n = std::pow(2,2);
double h= 1./n;
n += 1;
```

    (unsigned long) 5

```c++
auto x = zeroVector(n);
for(size_t i = 0 ; i < n ; i++)
    x[i] = i * h;
```

```c++
// Print the discrete nodes
std::cout << x ;
```

    (           0 )
    (        0.25 )
    (         0.5 )
    (        0.75 )
    (           1 )

    (std::ostream &) @0x7fce872e3500

Task 1: Replace the for loop in Cell 11 with the hpx::for loop to fill the right-hand side $f$ in parrallel in Cell 12.

## Prepare the external load

```c++
// Get the force vector for the right-hand side
auto f = zeroVector(n);
```

```c++
/*
for(size_t i = 0 ; i < n  ; i++)
 {
  f[i] = force(x[i]);
 }
*/
```

```c++
run_hpx([](){

});
```

    (void) @0x7fce7831ce48

```c++
// Print the force vector
std::cout << f ;
```

    (           0 )
    (           0 )
    (           0 )
    (           0 )
    (           1 )

    (std::ostream &) @0x7fce872e3500

### Assemble the stiffness matrix using finite differences

1. Dirichlet boundary condition at $x=0$:
\begin{equation}
u_1 = 0.
\end{equation}
2. Finite difference schems for
In $\overline{\Omega}$:
$\forall i=2,\ldots,n-1$:
\begin{equation}
- E \frac{u_{i-1}-2u_i+u_{i+1}}{h^2} = f_b(x_k).
\end{equation}
3. Neumann boundary condition at $x=1$:
\begin{equation}
E \frac{u_{n-3}-4u_{n-2}+3u_n-1}{2h} = g.
\end{equation}

For simplicity we assume $E=1$.

Task 2: Use aysnchronous programming to assemble the stiffness matrix using hpx::async and hpx::future.

1. Finish the function assemble to fill the matrix from start to end in Cell 16
2. Generate a vector of hpx::futures<void> to collect all future for synchronization in Cell 17
3. Use hpx::async to execute the function assemble asynchronously in Cell 17
4. Use hpx::wait_all to syncrhonize the results in Cell 17

```c++
// Get the stiffness matrix filled with zeros
auto matrix = zeroMatrix(n);
```

```c++
/*
matrix(0,0) = 1;
for(size_t i = 1 ; i < n-1 ; i++){
    matrix(i,i-1) = -2;
    matrix(i,i) = 4;
    matrix(i,i+1) = -2;
    }

matrix(n-1,n-1) = 3*h;
matrix(n-1,n-2) = -4*h;
matrix(n-1,n-3) = h;

matrix *= 1./(2*h*h);
*/
```

```c++
// Assmeble the part of the stiffness matrix where the index goes from start to end
void assemble(blaze::DynamicMatrix<double>* matrix, size_t start, size_t end)
{

        #(*matrix)(i,i-1) = -2;
        #(*matrix)(i,i) = 4;
        #(*matrix)(i,i+1) = -2;

}
```

```c++

```

    (void) @0x7fce7831ce48

```c++
std::cout << matrix ;
```

    (            8            0            0            0            0 )
    (          -16           32          -16            0            0 )
    (            0          -16           32          -16            0 )
    (            0            0          -16           32          -16 )
    (            0            0            2           -8            6 )

    (std::ostream &) @0x7fce872e3500

```c++
// Solve the matrix system matrix times displacement - f
auto displacement = solve(matrix,f);
```

```c++
std::cout << displacement;
```

    (           0 )
    (        0.25 )
    (         0.5 )
    (        0.75 )
    (           1 )

    (std::ostream &) @0x7fce872e3500

# Doing python plots from C++

Doing plots in Python is quite convinent using [matplotlib](https://matplotlib.org/), however, plotting in C++ is a little bit more tricky since we need to write the data as a CSV file and use Python or Matplotlib to plot it. The notebooks have some magic implemented to plot C++ variables directly in Python's matplotlib.

Below we use %plot x y to plot a new line and we can repeat this command to add new lines to the same plot. Using %plotc x y will plot all previous added lines and clear the figure.

```c++
%data x displacement
```

    x shape is (5,)displacement shape is (5,)

```c++
%%plot
plt.xlabel("Position")
plt.ylabel("Displacement")
plt.plot(x,displacement,label="Simulation")
plt.plot(x,x,label="Exact solution")
plt.grid()
plt.legend()
```


# メモ

圏論の道案内

矢印でえがく数学の世界

西郷はやと甲矢人

を読む。

# 圏とは、射とは

圏 (category) とは、対象 (object) と 射 (arrow, morphism) とからなるある種のシステムだ。

定義 2.1

どんな射に対しても、域 (domain) と呼ばれる対象と余域 (codomain) と呼ばれる対象とがただ 1つ存在する。

射 $f$ の域が $A$、余域が $B$ であることを

$ \quad \quad B \overset{f}{\longleftarrow} A $

と書き、$f$ は $A$ から $B$ への射である、と言う。  また射 $f$ の域を $ \mathrm{dom}(f)$、余域を $\mathrm{cod}(f)$ と記す。

**射の合成 p.22**

$ g \circ f $

**定義 2.2**

射 $f,g$ について、$\mathrm{cod}(f) = \mathrm{dom}(g)$ であるなら、$f,g$ の合成 (composition) と呼ばれる $\mathrm{dom}(f)$ から $\mathrm{cod}(g)$ への射が一意に存在する。 これを $g \circ f$ と書く。

$ \quad \quad C \overset{g \circ f}{\longleftarrow} A $

$ \quad \quad C \overset{g}{\longleftarrow} B  \overset{f}{\longleftarrow} A$

対象と射の関係を図示したものを 図式 (diagram) と呼ぶ。

**定義 2.3**

図式内に現れる射やそれらの合成として得られる射について、域と余域がそれぞれ等しい射が互いに等しいとき、その図式は 可換 (commutative) である、と言う。

$$
\quad \quad B \overset{f}{\longleftarrow} A
$$

```python
from sympy import *
from sympy.categories import *
init_printing()
A = Object('A')
B= Object('B')
C=Object("C")
f = NamedMorphism(A,B,"f")
g = NamedMorphism(B,C,"g")
diagram=Diagram([f,g],{g*f: "unique"})
grid=DiagramGrid(diagram)
drawer=XypicDiagramDrawer()
display(drawer.draw(diagram, grid))
```

    '\\xymatrix{\nA \\ar[d]_{g\\circ f} \\ar[r]^{f} & B \\ar[ld]^{g} \\\\\nC & \n}\n'

$$
\begin{CD}
K(X) @>{ch}>> H(X;\mathbb Q);\\
@VVV @VVV \\
K(Y) @>{ch}>> H(Y;\mathbb Q);
\end{CD}
$$

$$
\require{AMScd}
\begin{CD}
    A @>{f}>> B \\
@V{g}VV    @VVV \\
    C   @>>>  D
\end{CD}
$$