# New Angles from Right Range: Optimizing Car Sensor Positioning with D-Wave Hybrid Quantum Computers

### BMW Quantum Computing Challenge -- Round 2

#### Valter Uotila, [Unified Database Management Systems](https://www2.helsinki.fi/en/researchgroups/unified-database-management-systems-udbms/people#section-63562)
#### Sardana Ivanova, [Discovery Research Group](https://www2.helsinki.fi/en/researchgroups/computational-creativity-and-data-mining/people#section-102417)

#### Department of Computer Science, University of Helsinki

## Introduction to the implementation

We have been learning, testing and developing some basic quantum algorithms using D-waves own API but we are not yet very experienced with details related to these quantum computers.

The structure of the code is simple: first we import the data, then we create the binary variables as described in the proposal. After that we construct the three objective functions. Finally, we send the total objective functions to D-wave's quantum computer which solves it. The final result is printed.

## Initializing parameters

### Importing D-wave packages

In order to run the code here, you need to be able to successfully access D-wave's quantum cloud computing resources. You can see more info at https://cloud.dwavesys.com/. The reason is that, unfortunately, Amazon Bracket does not yet support D-wave's hybrid solvers.

```python
%%time

import dimod

from dwave.system import LeapHybridSampler

import json
import csv

from itertools import combinations
import pprint
import os
import math

import warnings
warnings.filterwarnings("ignore")

import numpy as np
from matplotlib import pyplot
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits import mplot3d
from sympy import *

notebook_path = os.path.abspath("main_3D_connecting_Dwave_Leap.ipynb")
```

    Wall time: 2.51 s

### Global parameters

The parameters `enviroment_x`, `enviroment_y`, `enviroment_z` describe the dimensions of the environment. The environment is the space where the car is located in. Anything outside the environment is supposed not to be accessible by sensors. We took the values for these parameters from the data set `criticallity_grid` which BMW provided.

```python
%%time

car_sample_accuracy = 500

angle_accuracy = 60

variables = dict()

all_variables = list()

criticallity_grid_abs_path = os.path.join(os.path.dirname(notebook_path), "sensor_position_data/criticallity_grid_0_5.csv")

xs, ys, zs = list(), list(), list()

with open(criticallity_grid_abs_path, newline='') as csvfile:
    records = iter(csv.reader(csvfile, delimiter=','))
    next(records)
    for row in records:
        xs.append(int(float(row[0])*100))
        ys.append(int(float(row[1])*100))
        zs.append(int(float(row[2])*100))

print("Corner points of the environment: ", max(xs), min(xs), max(ys), min(ys), max(zs), min(zs))

environment_x = abs(max(xs)) + abs(min(xs))
environment_y = abs(max(ys)) + abs(min(ys))
environment_z = abs(max(zs)) + abs(min(zs))

print("Environment x-coordinate: ", environment_x)
print("Environment y-coordinate: ", environment_y)
print("Environment z-coordinate: ", environment_z)
```

    Corner points of the environment:  11600 -7500 5100 -5100 650 -50
    Environment x-coordinate:  19100
    Environment y-coordinate:  10200
    Environment z-coordinate:  700
    Wall time: 597 ms

### Importing sensors

We created various sensors to demonstrate the code. It is difficult to say if these sensors have realistic values because we didn't have sensor examples in BMW data.

```python
%%time

abs_sensors_file_path = os.path.join(os.path.dirname(notebook_path), "3d_example_data/3D_sensors_set_1.json")

sensor_types = {0: 'lidar', 1: 'radar', 2: 'camera', 3: 'ultrasound'}

f = open(abs_sensors_file_path)

sensor_root = json.load(f)
sensors = sensor_root["sensors"]

def get_sensor_price(sensor_id):
    for sensor in sensors:
        if sensor['id'] == sensor_id:
            return sensor['price']

print(json.dumps(sensors, indent=4, sort_keys=True))
```

    [
        {
            "id": 1,
            "price": 100,
            "type": 1,
            "view": {
                "angle": {
                    "horizontal": 10,
                    "vertical": 50
                },
                "range": 2000
            }
        },
        {
            "id": 2,
            "price": 200,
            "type": 2,
            "view": {
                "angle": {
                    "horizontal": 10,
                    "vertical": 50
                },
                "range": 5000
            }
        },
        {
            "id": 3,
            "price": 100,
            "type": 3,
            "view": {
                "angle": {
                    "horizontal": 10,
                    "vertical": 50
                },
                "range": 7000
            }
        },
        {
            "id": 4,
            "price": 10,
            "type": 1,
            "view": {
                "angle": {
                    "horizontal": 10,
                    "vertical": 50
                },
                "range": 10000
            }
        },
        {
            "id": 5,
            "price": 100,
            "type": 2,
            "view": {
                "angle": {
                    "horizontal": 10,
                    "vertical": 50
                },
                "range": 10000
            }
        }
    ]
    Wall time: 2 ms

### Initializing allowed sensor positions on car

Here we utilize the data BMW provided. Compared to the first-round two-dimensional version, the car model in this second-round version is based on the allowed sensor positions data. In order to create variables, we need to sample some points from the surfaces. To sample points, we need some easier format for the surfaces than vector representation. Thus we calculate plane equations for the surfaces. The equation allows us to sample points when we are creating the variables.

```python
def unit_vector(vector):
    return vector / np.linalg.norm(vector)

def test_vectors(v1, v2, a, b, c, cp):

    if not np.allclose([1,1,0,0], [v1.dot(v1), v2.dot(v2), v1.dot(v2), v1.dot(v2)]):
        print("Error in dot products!")
        print(v1.dot(v2))
        print(v1.dot(v2))

    if not np.allclose([1,0,0], [cp.dot(cp), cp.dot(v1), cp.dot(v2)]):
        print("Error in normal vector dot products!")
        print(cp.dot(v1))
        print(cp.dot(v2))

def plot_region(a, b, c, d):
    x = np.linspace(-1,1,10)
    y = np.linspace(-1,1,10)

    X,Y = np.meshgrid(x,y)
    if c != 0:
        Z = (d - a*X - b*Y) / c

        fig = plt.figure()
        ax = fig.gca(projection='3d')

        surf = ax.plot_surface(X, Y, Z)

def scatter_plot(points, save = False):
    fig = pyplot.figure()
    ax = Axes3D(fig)

    x_vals = [c[0] for c in points]
    y_vals = [c[1] for c in points]
    z_vals = [c[2] for c in points]

    #ax.scatter3D(x_vals, y_vals, z_vals, c=z_vals);

    ax.scatter(x_vals, y_vals, z_vals)
    #ax.set_xlabel('X Label')
    #ax.set_ylabel('Y Label')
    #ax.set_zlabel('Z Label')
    pyplot.show()

def plane_equation(c1, c2, c3):
    p1 = np.array([c1[0], c1[1], c1[2]])
    p2 = np.array([c2[0], c2[1], c2[2]])
    p3 = np.array([c3[0], c3[1], c3[2]])

    # These two vectors are in the plane
    v1 = unit_vector(p3 - p1)
    v2 = unit_vector(p2 - p1)

    # We modify the second vector so that it is orthogonal with the vector v1.
    # This is important so that we get 90 degree angle between the vectors and we
    # can use trigonometric functions.
    v2 -= v1.dot(v2)*v1

    # Cross product is a vector normal to the plane
    # spanned by vectors v1 and v2
    cp = unit_vector(np.cross(v1, v2))
    cp = np.array([round(x, 7) for x in cp])
    a, b, c = cp

    # Testing vectors
    # test_vectors(v1, v2, a, b, c, cp)

    # This evaluates a * x3 + b * y3 + c * z3 which equals d
    d = np.dot(cp, p3)

    # print('Equation is {0}x + {1}y + {2}z = {3}'.format(a, b, c, d))
    # plot_region(a, b, c, d)

    return { 'span1': v1, 'span2': v2, 'normal_vector': cp, 'x': a, 'y': b, 'z': c, 'd': d }
```

In the following, we calculate the planes for the different surfaces of the car. Besides, we characterize each plane with its plane equation (or line).

```python
%%time

abs_position_file_path = os.path.join(os.path.dirname(notebook_path), "sensor_position_data/allowed_sensor_positions.csv")

allowed_sensor_positions = dict()

with open(abs_position_file_path, newline='') as csvfile:
    records = iter(csv.DictReader(csvfile, delimiter=';'))
    next(records)
    for row in records:
        c1 = (int(row["x1"]), int(row["y1"]), int(row["z1"]))
        c2 = (int(row["x2"]), int(row["y2"]), int(row["z2"]))
        c3 = (int(row["x3"]), int(row["y3"]), int(row["z3"]))
        c4 = (int(row["x4"]), int(row["y4"]), int(row["z4"]))

        region = row["Region"].lower()

        allowed_sensor_positions[region] = dict()
        allowed_sensor_positions[region]["corners"] = [c1, c2, c3, c4]
        allowed_sensors = [x.lower() for x in row["Allowed Sensors"].split(", ")]
        allowed_sensor_positions[region]["allowed_sensors"] = allowed_sensors

        equation = plane_equation(c1, c2, c3)
        allowed_sensor_positions[region]["equation"] = equation

        intervals = dict()

        if equation["x"] == 0:
            if c1[0] < c2[0]:
                intervals["x"] = range(c1[0], c2[0], car_sample_accuracy)
            elif c1[0] > c2[0]:
                intervals["x"] = range(c2[0], c1[0], car_sample_accuracy)
            elif c1[0] < c3[0]:
                intervals["x"] = range(c1[0], c3[0], car_sample_accuracy)
            elif c1[0] > c3[0]:
                intervals["x"] = range(c3[0], c1[0], car_sample_accuracy)

        if equation["y"] == 0:
            if c1[1] < c2[1]:
                intervals["y"] = range(c1[1], c2[1], car_sample_accuracy)
            elif c1[1] > c2[1]:
                intervals["y"] = range(c2[1], c1[1], car_sample_accuracy)
            elif c1[1] < c3[1]:
                intervals["y"] = range(c1[1], c3[1], car_sample_accuracy)
            elif c1[1] > c3[1]:
                intervals["y"] = range(c3[1], c1[1], car_sample_accuracy)

        if equation["z"] == 0:
            if c1[2] < c2[2]:
                intervals["z"] = range(c1[2], c2[2], car_sample_accuracy)
            elif c1[2] > c2[2]:
                intervals["z"] = range(c2[2], c1[2], car_sample_accuracy)
            elif c1[2] < c3[2]:
                intervals["z"] = range(c1[2], c3[2], car_sample_accuracy)
            elif c1[2] > c3[2]:
                intervals["z"] = range(c3[2], c1[2], car_sample_accuracy)

        allowed_sensor_positions[region]["fixed_intervals"] = intervals

#for_printing = allowed_sensor_positions[elem]
#pp = pprint.PrettyPrinter(width=41, compact=True)
for elem in allowed_sensor_positions:
    print(elem, allowed_sensor_positions[elem])
    print()

```

    bonnet {'corners': [(-1400, 1000, 1200), (-1400, -1000, 1200), (0, -1000, 1000), (0, 1000, 1000)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.57154761, -0.81649658, -0.08164966]), 'span2': array([-0.46666667, -0.33333333,  0.06666667]), 'normal_vector': array([-0.1414214,  0.       , -0.9899495]), 'x': -0.1414214, 'y': 0.0, 'z': -0.9899495, 'd': -989.9495000000001}, 'fixed_intervals': {'y': range(-1000, 1000, 500)}}

    side mirror left {'corners': [(-1400, 1150, 1320), (-1400, 950, 1320), (-1400, 950, 1200), (-1400, 1150, 1200)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.        , -0.85749293, -0.51449576]), 'span2': array([ 0.        , -0.26470588,  0.44117647]), 'normal_vector': array([-1., -0.,  0.]), 'x': -1.0, 'y': -0.0, 'z': 0.0, 'd': 1400.0}, 'fixed_intervals': {'y': range(950, 1150, 500), 'z': range(1200, 1320, 500)}}

    side mirror right {'corners': [(-1400, -950, 1320), (-1400, -1150, 1320), (-1400, -1150, 1200), (-1400, -950, 1200)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.        , -0.85749293, -0.51449576]), 'span2': array([ 0.        , -0.26470588,  0.44117647]), 'normal_vector': array([-1., -0.,  0.]), 'x': -1.0, 'y': -0.0, 'z': 0.0, 'd': 1400.0}, 'fixed_intervals': {'y': range(-1150, -950, 500), 'z': range(1200, 1320, 500)}}

    camera behind windshield {'corners': [(-2000, 700, 1600), (-2000, -700, 1600), (-1900, -700, 1500), (-1900, 700, 1500)], 'allowed_sensors': ['camera'], 'equation': {'span1': array([ 0.07106691, -0.99493668, -0.07106691]), 'span2': array([-0.07070707, -0.01010101,  0.07070707]), 'normal_vector': array([-0.7071068,  0.       , -0.7071068]), 'x': -0.7071068, 'y': 0.0, 'z': -0.7071068, 'd': 282.8427200000001}, 'fixed_intervals': {'y': range(-700, 700, 500)}}

    front roof {'corners': [(-2200, 550, 1745), (-2200, -550, 1745), (-2000, -650, 1600), (-2000, 650, 1600)], 'allowed_sensors': ['lidar', 'camera', 'ultrasound', 'radar'], 'equation': {'span1': array([ 0.16324355, -0.9794613 , -0.11835157]), 'span2': array([-0.15989074, -0.04065555,  0.11592079]), 'normal_vector': array([-0.5869676, -0.       , -0.8096104]), 'x': -0.5869676, 'y': -0.0, 'z': -0.8096104, 'd': -121.44143999999983}, 'fixed_intervals': {'y': range(-550, 550, 500)}}

    back {'corners': [(-4922, 1000, 1000), (-4922, -1000, 1000), (-4922, -1000, 500), (-4922, 1000, 500)], 'allowed_sensors': ['lidar', 'camera', 'ultrasound', 'radar'], 'equation': {'span1': array([ 0.        , -0.9701425 , -0.24253563]), 'span2': array([ 0.        , -0.05882353,  0.23529412]), 'normal_vector': array([-1., -0.,  0.]), 'x': -1.0, 'y': -0.0, 'z': 0.0, 'd': 4922.0}, 'fixed_intervals': {'y': range(-1000, 1000, 500), 'z': range(500, 1000, 500)}}

    trunk {'corners': [(-4922, 350, 1300), (-4922, -350, 1300), (-4922, -350, 1000), (-4922, 350, 1000)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.        , -0.91914503, -0.3939193 ]), 'span2': array([ 0.        , -0.15517241,  0.36206897]), 'normal_vector': array([-1., -0.,  0.]), 'x': -1.0, 'y': -0.0, 'z': 0.0, 'd': 4922.0}, 'fixed_intervals': {'y': range(-350, 350, 500), 'z': range(1000, 1300, 500)}}

    side left {'corners': [(-4922, 1000, 1000), (0, 1000, 1000), (0, 1000, 500), (-4922, 1000, 500)], 'allowed_sensors': ['lidar', 'camera', 'ultrasound', 'radar'], 'equation': {'span1': array([ 0.99487987,  0.        , -0.10106459]), 'span2': array([0.01021405, 0.        , 0.10054713]), 'normal_vector': array([ 0., -1.,  0.]), 'x': 0.0, 'y': -1.0, 'z': 0.0, 'd': -1000.0}, 'fixed_intervals': {'x': range(-4922, 0, 500), 'z': range(500, 1000, 500)}}

    pillar left 1 {'corners': [(-2820, 650, 1600), (-2520, 650, 1600), (-2520, 1000, 1200), (-2820, 1000, 1200)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.49153915,  0.57346234, -0.65538554]), 'span2': array([ 0.75838926, -0.28187919,  0.32214765]), 'normal_vector': array([-0.       , -0.7525767, -0.6585046]), 'x': -0.0, 'y': -0.7525767, 'z': -0.6585046, 'd': -1542.78222}, 'fixed_intervals': {'x': range(-2820, -2520, 500)}}

    pillar left 2 {'corners': [(-3620, 650, 1600), (-3320, 650, 1600), (-3320, 1000, 1200), (-3620, 1000, 1200)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.49153915,  0.57346234, -0.65538554]), 'span2': array([ 0.75838926, -0.28187919,  0.32214765]), 'normal_vector': array([-0.       , -0.7525767, -0.6585046]), 'x': -0.0, 'y': -0.7525767, 'z': -0.6585046, 'd': -1542.78222}, 'fixed_intervals': {'x': range(-3620, -3320, 500)}}

    roof left {'corners': [(-4400, 550, 1745), (-2200, 550, 1745), (-2000, 650, 1600), (-4400, 650, 1600)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.99731769,  0.0415549 , -0.06025461]), 'span2': array([ 0.00535743, -0.04144344,  0.06009299]), 'normal_vector': array([ 0.       , -0.8232128, -0.567733 ]), 'x': 0.0, 'y': -0.8232128, 'z': -0.567733, 'd': -1443.46112}, 'fixed_intervals': {'x': range(-4400, -2200, 500)}}

    side right {'corners': [(-4922, -1000, 1000), (0, -1000, 1000), (0, -1000, 500), (-4922, -1000, 500)], 'allowed_sensors': ['lidar', 'camera', 'ultrasound', 'radar'], 'equation': {'span1': array([ 0.99487987,  0.        , -0.10106459]), 'span2': array([0.01021405, 0.        , 0.10054713]), 'normal_vector': array([ 0., -1.,  0.]), 'x': 0.0, 'y': -1.0, 'z': 0.0, 'd': 1000.0}, 'fixed_intervals': {'x': range(-4922, 0, 500), 'z': range(500, 1000, 500)}}

    pillar right 1 {'corners': [(-2820, -650, 1600), (-2520, -650, 1600), (-2520, -1000, 1200), (-2820, -1000, 1200)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.49153915, -0.57346234, -0.65538554]), 'span2': array([0.75838926, 0.28187919, 0.32214765]), 'normal_vector': array([ 0.       , -0.7525767,  0.6585046]), 'x': 0.0, 'y': -0.7525767, 'z': 0.6585046, 'd': 1542.78222}, 'fixed_intervals': {'x': range(-2820, -2520, 500)}}

    pillar right 2 {'corners': [(-3620, -650, 1600), (-3320, -650, 1600), (-3320, -1000, 1200), (-3620, -1000, 1200)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.49153915, -0.57346234, -0.65538554]), 'span2': array([0.75838926, 0.28187919, 0.32214765]), 'normal_vector': array([ 0.       , -0.7525767,  0.6585046]), 'x': 0.0, 'y': -0.7525767, 'z': 0.6585046, 'd': 1542.78222}, 'fixed_intervals': {'x': range(-3620, -3320, 500)}}

    roof right {'corners': [(-4400, -550, 1745), (-2200, -550, 1745), (-2000, -650, 1600), (-4400, -650, 1600)], 'allowed_sensors': ['lidar', 'camera'], 'equation': {'span1': array([ 0.99731769, -0.0415549 , -0.06025461]), 'span2': array([0.00535743, 0.04144344, 0.06009299]), 'normal_vector': array([ 0.       , -0.8232128,  0.567733 ]), 'x': 0.0, 'y': -0.8232128, 'z': 0.567733, 'd': 1443.46112}, 'fixed_intervals': {'x': range(-4400, -2200, 500)}}

    Wall time: 40 ms

### Initializing variables

Compared to the first-round, variables are updated to be triples `(x, y, i)` where `x` and `y` are points in the 3D-space and `i` refers to the sensor's id. The point `x` belongs to some of the allowed sensor positions on the car's surface. For each `x`, points `y` are sampled from the environment so that the distance between `x` and `y` is range `R_i` for sensor `i`.

We are dealing with different cases depending on how the car surface is positioned in the space. A bit simplifying we can say that one of the varibles, say `x`, runs over an interval. The values for the two other variables, `y` and `z`, we get from the plane equation when we substitute the value `x`. Now these triples belong to the car's surface.

```python
def sample_from_car_surface(corners, equation, fixed_intervals, car_sample_accuracy):
    sample = list()

    # This is the simple case that the plane is parallel with some of the three axis.
    # Thus the parallel axis stays constant.
    # For example, side mirror, back, trunk and sides are parallel to one of the axis

    if len(fixed_intervals) == 2:
        if 'x' in fixed_intervals and 'y' in fixed_intervals:

            z = corners[0][2]

            for x in fixed_intervals['x']:
                for y in fixed_intervals['y']:
                    sample.append((x, y, z))

        elif 'x' in fixed_intervals and 'z' in fixed_intervals:

            y = corners[0][1]

            for x in fixed_intervals['x']:
                for z in fixed_intervals['z']:
                    sample.append((x, y, z))

        elif 'y' in fixed_intervals and 'z' in fixed_intervals:

            x = corners[0][0]

            for y in fixed_intervals['y']:
                for z in fixed_intervals['z']:
                    sample.append((x, y, z))

    elif len(fixed_intervals) == 1:

        if 'x' in fixed_intervals:
            y, z = symbols('y z')
            expr = equation['y']*y + equation['z']*z - equation['d']
            y_interval = None

            c1 = corners[0][1]
            c2 = corners[1][1]
            c3 = corners[2][1]

            if c1 < c2:
                y_interval = range(c1, c2, car_sample_accuracy)
            elif c1 > c2:
                y_interval = range(c2, c1, car_sample_accuracy)
            elif c1 < c3:
                y_interval = range(c1, c3, car_sample_accuracy)
            elif c1 > c3:
                y_interval = range(c3, c1, car_sample_accuracy)

            for x in fixed_intervals['x']:
                for y_var in y_interval:
                    y_expr = expr.subs(y, y_var)
                    z = math.floor(solve(y_expr)[0])
                    sample.append((x, y_var, z))
                    #print((x, y_var, z))
                x += car_sample_accuracy

        elif 'y' in fixed_intervals:
            x, z = symbols('x z')
            expr = equation['x']*x + equation['z']*z - equation['d']
            x_interval = None

            c1 = corners[0][0]
            c2 = corners[1][0]
            c3 = corners[2][0]

            if c1 < c2:
                x_interval = range(c1, c2, car_sample_accuracy)
            elif c1 > c2:
                x_interval = range(c2, c1, car_sample_accuracy)
            elif c1 < c3:
                x_interval = range(c1, c3, car_sample_accuracy)
            elif c1 > c3:
                x_interval = range(c3, c1, car_sample_accuracy)

            for y in fixed_intervals['y']:
                for x_var in x_interval:
                    x_expr = expr.subs(x, x_var)
                    z = math.floor(solve(x_expr)[0])
                    sample.append((x_var, y, z))
                    #print((x_var, y, z))
                y += car_sample_accuracy

        elif 'z' in fixed_intervals:

            x, y = symbols('x y')
            expr = equation['x']*x + equation['y']*y - equation['d']
            x_interval = None

            c1 = corners[0][0]
            c2 = corners[1][0]
            c3 = corners[2][0]

            if c1 < c2:
                x_interval = range(c1, c2, car_sample_accuracy)
            elif c1 > c2:
                x_interval = range(c2, c1, car_sample_accuracy)
            elif c1 < c3:
                x_interval = range(c1, c3, car_sample_accuracy)
            elif c1 > c3:
                x_interval = range(c3, c1, car_sample_accuracy)

            for z in fixed_intervals['z']:
                for x_var in x_interval:
                    x_expr = expr.subs(x, x_var)
                    y = math.floor(solve(x_expr)[0])
                    #print(y)
                    sample.append((x_var, y, z))
                    #print((x_var, y, z))
                z += car_sample_accuracy

    return sample

```

```python
# Works in terminal Python but not in the notebook version
def plot_sampled_points_from_car(all_variables):
    fig = pyplot.figure()
    ax = fig.add_subplot(111, projection='3d')
    xs, ys, zs = list(), list(), list()

    for var in all_variables:
        point = var[1]
        xs.append(point[0])
        ys.append(point[1])
        zs.append(point[2])

    print(xs)
    print()
    print(ys)
    print()
    print(zs)

    #ax.scatter(xs, ys, zs)

    #ax.set_xlabel('X Label')
    #ax.set_ylabel('Y Label')
    #ax.set_zlabel('Z Label')

    #pyplot.show()
```

After we have sampled points from the car's surface, we sample points from the environment. We fix a point `x` on the car's surface and then pick a point `y` from the environment so that the distance between `x` and `y` is the range `R_i`. We let the angle between the car's surface and the vector `x - y` run over different values which produces multiple possible point pairs `(x, y)`. In the optimal situation we could sample points with some sufficiently small accuracy factor.

```python
%%time

for sensor in sensors:
    variables[sensor["id"]] = dict()
    for pos_name in allowed_sensor_positions:
        pos = allowed_sensor_positions[pos_name]
        #print(sensor_types)
        if sensor_types[sensor["type"]] in pos["allowed_sensors"]:

            variables[sensor["id"]][pos_name] = list()
            srange = sensor["view"]["range"]
            corners = pos["corners"]
            equation = pos["equation"]
            fixed_intervals = pos["fixed_intervals"]
            normal_vector = -equation['normal_vector']
            spanning_vector_on_plane1 = equation['span1']
            spanning_vector_on_plane2 = equation['span2']

            car_sample = sample_from_car_surface(corners, equation, fixed_intervals, car_sample_accuracy)

            #print(pos_name)
            #print(car_sample)
            #scatter_plot(car_sample)

            for car_point in car_sample:
                for angle_on_plane in range(0, 360, angle_accuracy):
                    for angle_for_normal in range(0, 90, angle_accuracy):

                        rad_angle_on_plane = math.radians(angle_on_plane)
                        rad_angle_for_normal = math.radians(angle_for_normal)

                        #print("Car point ", car_point)
                        #print("Normal vector", normal_vector)
                        #print("Spanning vector", spanning_vector_on_plane1)
                        #print("Spanning vector", spanning_vector_on_plane2)

                        # We start moving from the fixed point on the car's surface
                        point_in_environment = np.array([float(x) for x in car_point])
                        car_point_vector = np.array(car_point)

                        #print(rad_angle_on_plane)
                        #print(math.cos(rad_angle_on_plane))

                        #print("#11 ", srange*math.cos(rad_angle_on_plane)*spanning_vector_on_plane1)
                        #print("#12 ", srange*math.sin(rad_angle_on_plane)*spanning_vector_on_plane2)
                        #print("#13 ", srange*math.sin(rad_angle_for_normal)*normal_vector)

                        # Move along the plane to the direction of the first spanning vector
                        point_in_environment += srange*math.cos(rad_angle_on_plane)*math.cos(rad_angle_for_normal)*spanning_vector_on_plane1

                        #print("#1 ", point_in_environment)

                        # Move along the plane to the direction of the second spanning vector
                        point_in_environment += srange*math.sin(rad_angle_on_plane)*math.cos(rad_angle_for_normal)*spanning_vector_on_plane2

                        #print("#2 ", point_in_environment)

                        # Move to the orthonogal direction to the plane i.e. "upwards"
                        point_in_environment += srange*math.sin(rad_angle_for_normal)*normal_vector

                        #print("#3 ", point_in_environment)

                        #env_points.append(point_in_environment)
                        #scatter_plot(env_points)
                        #print("Distance defined in the sensor data is " + str(srange) + " and the distance between the sampled points is ", np.linalg.norm(car_point_vector.dot(point_in_environment)))

                        point_in_environment = [math.floor(x) for x in point_in_environment]

                        # For bugging purposes:
                        #print("Angles are ", angle_on_plane, angle_for_normal)

                        b_variable = (car_point, tuple(point_in_environment), sensor["id"])
                        variables[sensor["id"]][pos_name].append(b_variable)
                        all_variables.append(b_variable)

print("Number of variables: ", len(all_variables))
```

    Number of variables:  2412
    Wall time: 3.79 s

## Constructing quadratic unconstrained binary optimization model

```python
vartype = dimod.BINARY
main_bqm = dimod.BinaryQuadraticModel({}, {}, 0.0, vartype)
```

The following functions enable us to append or initialize coefficients for the variables in the BQM. The distance function implements the ordinary Euclidean distance.

```python
def append_linear_safe(variable, value, linear_dict):
    if variable in linear_dict.keys():
        linear_dict[variable] = linear_dict[variable] + value
    else:
        linear_dict[variable] = value

def append_quadratic_safe(variable, value, quadratic_dict):
    if variable in quadratic_dict.keys():
        quadratic_dict[variable] = quadratic_dict[variable] + value
    else:
        quadratic_dict[variable] = value

def sensor_view_volume(sensor_id):
    for sensor in sensors:
        if sensor['id'] == sensor_id:
            # Following code calculates volume of view of sensor
            side1 = 2*sensor["view"]["range"]*float(math.tan(math.radians(sensor["view"]["angle"]["horizontal"])/2))
            side2 = 2*sensor["view"]["range"]*float(math.tan(math.radians(sensor["view"]["angle"]["vertical"])/2))
            return (1/3)*sensor["view"]["range"]*side1*side2

def print_current_qubo(number_of_linear_terms = 100, number_of_quadratic_terms = 100):
    i = 0
    for elem in main_bqm.linear:
        print(elem, main_bqm.linear[elem])
        i+=1
        if i > number_of_linear_terms:
            break

    i = 0
    for elem in main_bqm.quadratic:
        print(elem, main_bqm.quadratic[elem])
        i+=1
        if i > number_of_quadratic_terms:
            break
```

### Constraint 1: selecting sufficiently sensors to cover the environment

Every binary quadratic function which is part of the model contains four parameters: linear terms, quadratic terms, offset (constant) and variable type. Variable type is always BINARY since we are using QUBO. If we use Ising, we set variable type to be SPIN.

```python
%%time

# Encoding constraint H1

E = environment_x*environment_y*environment_z
print("Total volume of the environment: ", E)

A1 = 1/math.pow(10, 12)
linear_h1 = {}
quadratic_h1 = {}
offset_h1 = float(math.pow(E, 2))
#print(offset_h1)

# Linear terms
for sensor_id in variables:

    volume = sensor_view_volume(sensor_id)
    print("Volume of sensor " + str(sensor_id) + ": ", volume)
    coefficient = float(math.pow(volume, 2) - 2*E*volume)

    for surface in variables[sensor_id]:
        for var in variables[sensor_id][surface]:
            append_linear_safe(var, coefficient, linear_h1)

# Quadratic terms
quadratic_terms = combinations(all_variables, 2)

for pair in quadratic_terms:
    b1 = pair[0]
    b2 = pair[1]

    volume1 = sensor_view_volume(b1[2])
    volume2 = sensor_view_volume(b2[2])

    coefficient_quadratic = float(2*volume1*volume2)
    append_quadratic_safe((b1, b2), coefficient_quadratic, quadratic_h1)

bqm_h1 = dimod.BinaryQuadraticModel(linear_h1, quadratic_h1, offset_h1, vartype)
bqm_h1.scale(A1)
main_bqm.update(bqm_h1)
```

    Total volume of the environment:  136374000000
    Volume of sensor 1:  435164093.9080988
    Volume of sensor 2:  6799438967.314044
    Volume of sensor 3:  18657660526.309734
    Volume of sensor 4:  54395511738.51235
    Volume of sensor 5:  54395511738.51235
    Wall time: 1min 9s

The following code is for printing the linear and quadratic terms.

```python
#print_current_qubo(10, 10)
```

### Constraint 2: optimizing overlap of sensor views

One of the biggest problems in the current code is the following overlap function. It is just a rough estimate on how much two sensor views overlap.

```python
def sensor_inside_sensor(car_p1, env_p1, car_p2, env_p2):
    if car_p2.all() == env_p2.all():
        return True
    if car_p1.all() == car_p2.all():
        if (car_p2 - env_p2).all() != 0:
            line = (car_p1 - env_p1)/(car_p2 - env_p2)
            for i in range(len(line) - 1):
                if line[i] != line[i+1]:
                    return False
            else:
                return True
    return False

def overlap(b1, b2):
    car_p1 = np.array([int(x) for x in b1[0]])
    env_p1 = np.array([int(x) for x in b1[1]])
    id1 = int(b1[2])

    car_p2 = np.array([int(x) for x in b2[0]])
    env_p2 = np.array([int(x) for x in b2[1]])
    id2 = int(b2[2])

    if sensor_inside_sensor(car_p1, env_p1, car_p2, env_p2):
        return 1

    cone_axis1 = env_p1 - car_p1
    cone_axis2 = env_p2 - car_p2

    # Angle between the axis

    u_cone_axis1 = cone_axis1 / np.linalg.norm(cone_axis1)
    u_cone_axis2 = cone_axis2 / np.linalg.norm(cone_axis2)

    axis_angle = np.arccos(np.dot(u_cone_axis1, u_cone_axis2))

    #print(math.degrees(axis_angle))

    # Distance between the middle points of the vectors cone_axis1 and cone_axis2

    mid_cone_axis1 = (np.linalg.norm(cone_axis1)/2)*u_cone_axis1
    mid_cone_axis2 = (np.linalg.norm(cone_axis2)/2)*u_cone_axis2

    mid_point_dist = np.linalg.norm(mid_cone_axis1 - mid_cone_axis2)

    top_point_dist = np.linalg.norm(env_p1 - env_p2)

    #print(mid_point_dist)

    sensor1, sensor2 = None, None

    for sensor in sensors:

        if sensor['id'] == id1:
            sensor1 = sensor

        if sensor['id'] == id2:
            sensor2 = sensor

    # This part of the code is very heuristical and possibly works badly and ruins everything

    side1 = np.linalg.norm(cone_axis1)
    side2 = np.linalg.norm(cone_axis2)

    top_half1_dist1 = math.tan(math.radians(sensor1["view"]["angle"]["horizontal"])/2)*side1
    top_half1_dist2 = math.tan(math.radians(sensor1["view"]["angle"]["vertical"])/2)*side1

    top_half2_dist1 = math.tan(math.radians(sensor2["view"]["angle"]["horizontal"])/2)*side2
    top_half2_dist2 = math.tan(math.radians(sensor2["view"]["angle"]["vertical"])/2)*side2

    #print("Distances on top: ", top_half1_dist1, top_half1_dist2, top_half2_dist1, top_half2_dist2)

    mid_half1_dist1 = top_half1_dist1/2
    mid_half1_dist2 = top_half1_dist2/2

    mid_half2_dist1 = top_half2_dist1/2
    mid_half2_dist2 = top_half2_dist2/2

    #print("Distances in middle: ", mid_half1_dist1, mid_half1_dist2, mid_half2_dist1, mid_half2_dist2)

    difference_top1 = top_half1_dist1 + top_half2_dist1 - top_point_dist
    difference_top2 = top_half1_dist2 + top_half2_dist2 - top_point_dist

    difference_mid1 = mid_half1_dist1 + mid_half2_dist1 - mid_point_dist
    difference_mid2 = mid_half1_dist2 + mid_half2_dist2 - mid_point_dist

    #print("Top differences: ", difference_top1, difference_top2)
    #print("Middle differences: ", difference_mid1, difference_mid2)

    top_divisor1 = max([top_half1_dist1, top_half2_dist1])
    top_divisor2 = max([top_half1_dist2, top_half2_dist2])

    top_divisor = top_divisor1 + top_divisor2

    mid_divisor1 = max([mid_half1_dist1, mid_half2_dist1])
    mid_divisor2 = max([mid_half1_dist2, mid_half2_dist2])

    mid_divisor = mid_divisor1 + mid_divisor2

    top_sum = 0
    mid_sum = 0

    if difference_top1 > 0:
        top_sum += difference_top1
    if difference_top2 > 0:
        top_sum += difference_top2
    if difference_mid1 > 0:
        mid_sum += difference_mid1
    if difference_mid2 > 0:
        mid_sum += difference_mid2

    overlap_result = (top_sum + mid_sum)/(top_divisor + mid_divisor)

    #print("Final result: ", overlap_result)

    if overlap_result > 1:
        return 1/overlap_result

    return overlap_result
```

```python
%%time

# Encoding constraint H2

A2 = 1000
linear_h2 = {}
quadratic_h2 = {}
offset_h2 = 0

quadratic_terms = combinations(all_variables, 2)

for pair in quadratic_terms:

    b1 = pair[0]
    b2 = pair[1]

    coefficient_quadratic = overlap(b1, b2)

    append_quadratic_safe((b1, b2), coefficient_quadratic, quadratic_h2)

bqm_h2 = dimod.BinaryQuadraticModel(linear_h2, quadratic_h2, offset_h2, vartype)
bqm_h2.scale(A2)
main_bqm.update(bqm_h2)

```

    Wall time: 2min 3s

```python
#print_current_qubo(50, 50)
```

### Constraint 3: minimizing total price

```python
%%time

# Encoding constraint H3

A3 = 1
linear_h3 = {}
quadratic_h3 = {}
offset_h3 = 0

for variable in all_variables:
    sensor_id = variable[2]
    price = get_sensor_price(sensor_id)
    append_linear_safe(variable, price, linear_h3)

bqm_h3 = dimod.BinaryQuadraticModel(linear_h3, quadratic_h3, offset_h3, vartype)
bqm_h3.scale(A3)
main_bqm.update(bqm_h3)
```

    Wall time: 101 ms

```python
#print_current_qubo(50, 50)
```

### Solve QUBO

Unfortunately, LeapHybridSampler is not available in Amazon Bracket. That is why this code will not work in Bracket. On the other hand, we tried run the code using BracketDwaveSampler but the problem cannot be mapped to the circuits.

```python
%%time

main_bqm.normalize()

sampler = LeapHybridSampler()
sampleset = sampler.sample(main_bqm)
sample = sampleset.first.sample
print(sampleset)
print()
# energy = sampleset.first.energy
print("Possible sensor positions in the space (point on car, point in environment, sensor id):")
i = 0
for varname, value in sample.items():
    if value == 1:
        i+=1
        print(varname, value)
print(i)
```

      ... ((-400, 500, 1057.0), (6499, -696, 71), 5)   energy num_oc.
    0 ...                                          0 0.000001       1
    ['BINARY', 1 rows, 1 samples, 2412 variables]

    Possible sensor positions in the space (point on car, point in environment, sensor id):
    ((-3422, 1000, 500), (-2428, 2732, 398), 1) 1
    ((-2400, -650, 1600.0), (2586, 6271, -3618), 5) 1
    ((-422, -1000, 500), (4640, -1000, 865), 5) 1
    ((-400, 0, 1057.0), (-1513, 298, 5590), 2) 1
    ((-400, 0, 1057.0), (191, 3484, 972), 2) 1
    ((-400, 0, 1057.0), (2457, -4083, 648), 2) 1
    ((-400, 0, 1057.0), (3049, -598, 564), 2) 1
    7
    Wall time: 1min 40s


```python
import pandas as pd
import numpy as np
```

```python
data = pd.read_csv('../dataset/mushrooms.csv')
```

```python
data.head()
```

      class
      cap-shape
      cap-surface
      cap-color
      bruises
      odor
      gill-attachment
      gill-spacing
      gill-size
      gill-color
      ...
      stalk-surface-below-ring
      stalk-color-above-ring
      stalk-color-below-ring
      veil-type
      veil-color
      ring-number
      ring-type
      spore-print-color
      population
      habitat

      0
      p
      x
      s
      n
      t
      p
      f
      c
      n
      k
      ...
      s
      w
      w
      p
      w
      o
      p
      k
      s
      u

      1
      e
      x
      s
      y
      t
      a
      f
      c
      b
      k
      ...
      s
      w
      w
      p
      w
      o
      p
      n
      n
      g

      2
      e
      b
      s
      w
      t
      l
      f
      c
      b
      n
      ...
      s
      w
      w
      p
      w
      o
      p
      n
      n
      m

      3
      p
      x
      y
      w
      t
      p
      f
      c
      n
      n
      ...
      s
      w
      w
      p
      w
      o
      p
      k
      s
      u

      4
      e
      x
      s
      g
      f
      n
      f
      w
      b
      k
      ...
      s
      w
      w
      p
      w
      o
      e
      n
      a
      g

5 rows × 23 columns

```python
data['class'].unique()
```

    array(['p', 'e'], dtype=object)

```python
ind = list(data.index)
np.random.shuffle(ind)

train_len = int(data.shape[0]*0.75)
train_ind = ind[:train_len]
training_data = data.iloc[train_ind,:]
#training_data.head()

test_ind = ind[train_len:]
testing_data = data.iloc[test_ind,:]
#testing_data.head()

print('Training_data size -> {}'.format(training_data.shape))
print('Testing_data size -> {}'.format(testing_data.shape))

assert data.shape[0] ==  len(train_ind)+ len(test_ind), 'Not equal distribution'
```

    Training_data size -> (6093, 23)
    Testing_data size -> (2031, 23)

\begin{equation}
P(Cap-shape \cap Cap-surface \cap ......\cap habitat | Class = p)\\[10pt]
P(Cap-shape \cap Cap-surface \cap ......\cap habitat | Class = e)
\end{equation}

```python
class NB:
    def __init__(self, target, dataframe):
        self.df = dataframe
        # Target/Category Column
        self.c_n = target
        # Column Names
        self.cols = list(self.df.columns)
        self.cols.remove(self.c_n)

        self.store = {}
        self.likelihood_for_all_()

    def likelihood_cal(self, x, y, z):
        """
        x -> Column Name (String)
        y -> Column Value (String)
        z -> Class value (String)
        c_n -> Class Name (Target)

        Returns -> P(x = y | c_n = z)
        """
        df = self.df

        if x not in self.cols:
            raise KeyError("Feature(column) not present in the Training Dataset")

        res =  len(df[(df[x] == y) & (df[self.c_n] == z)]) /len(df[df[self.c_n] == z])

        if res == 0.0:
            return 1/(len(df[df[self.c_n] == z]) + len(df[x].unique()))

        return res

    def likelihood_for_all_(self):
        df = self.df

        dict1 = {}
        for x in self.cols:
            dict2 = {}
            for y in df[x].unique():
                dict3 = {}
                for z in df[self.c_n].unique():
                    #print('P({}="{}"|{}="{}") = {}'.format(x,y,self.c_n,z,self.likelihood_cal(x, y, z)))
                    dict3[z] = self.likelihood_cal(x, y, z)
                dict2[y] = dict3
            dict1[x] = dict2

        self.store = dict1

    def likelihood_expr(self, class_val, expr):
        val = 1

        for k,v in expr:
            try:
                store_val = self.store[k][v][class_val]
            except:
                store_val = self.likelihood_cal(k,v,class_val)

            val *= store_val

        return val

    def prior(self, class_val):
        df = self.df
        return len(df[df[self.c_n] == class_val])/df.shape[0]

    def predict(self, X):
        df = self.df

        if type(X) == pd.core.series.Series:
            values_list = [list(X.items())]

        elif type(X) == pd.core.frame.DataFrame:
            values_list = [list(y.items()) for x,y in X.iterrows()]

        else:
            raise TypeError('{} is not supported type'.format(type(X)))

        predictions_list = []
        for values in values_list:
            likelihood_priors = {}
            for class_val in df[self.c_n].unique():
                likelihood_priors[class_val] = self.prior(class_val)*self.likelihood_expr(class_val,values)
            #print(likelihood_priors)

            normalizing_prob = np.sum([x for x in likelihood_priors.values()])
            probabilities = [(y/normalizing_prob,x) for x,y in likelihood_priors.items()]
            #print(probabilities)

            if len(probabilities) == 2:
                # For 2 Class Predictions
                max_prob = max(probabilities)[1]
                predictions_list.append(max_prob)

            else:
                # For Mulit Class Predictions
                exp_1 = [np.exp(x) for x,y in probabilities]
                exp_2 = np.sum(exp_1)
                softmax = exp_1/exp_2
                #print(softmax)
                class_names = [y for x,y in probabilities]
                softmax_values = [(x,y) for x,y in zip(softmax,class_names)]
                #print(softmax_values)
                max_prob = max(softmax_values)[1]
                predictions_list.append(max_prob)

        return predictions_list

    def accuracy_score(self, X, Y):
        assert len(X) == len(Y), 'Given values are not equal in size'

        total_matching_values = [x == y for x,y in zip(X,Y)]
        return (np.sum(total_matching_values)/len(total_matching_values))*100

    def calculate_confusion_matrix(self, X, Y):
        df = self.df

        unique_class_values = df[self.c_n].unique()
        decimal_class_values = list(range(len(unique_class_values)))
        numerical = {x:y for x,y in zip(unique_class_values, decimal_class_values)}

        x = [numerical[x] for x in X]
        y = [numerical[y] for y in Y]

        n = len(decimal_class_values)
        confusion_matrix = np.zeros((n,n))

        for i,j in zip(x,y):
            if i == j:
                confusion_matrix[i][i] += 1
            elif i != j:
                confusion_matrix[i][j] += 1

        return confusion_matrix

    def precision_score(self, X, Y):
        assert len(X) == len(Y), 'Given values are not equal in size'

        confusion_matrix = self.calculate_confusion_matrix(X,Y)
        tp = confusion_matrix[0][0]
        fp = confusion_matrix[1][0]

        return tp / (tp+fp)

    def recall_score(self, X, Y):
        assert len(X) == len(Y), 'Given values are not equal in size'

        confusion_matrix = self.calculate_confusion_matrix(X,Y)
        tp = confusion_matrix[0][0]
        fn = confusion_matrix[0][1]

        return tp / (tp+fn)

```

```python
genx = NB(target='class',dataframe=training_data)
```

```python
testing_data.head()
```

      class
      cap-shape
      cap-surface
      cap-color
      bruises
      odor
      gill-attachment
      gill-spacing
      gill-size
      gill-color
      ...
      stalk-surface-below-ring
      stalk-color-above-ring
      stalk-color-below-ring
      veil-type
      veil-color
      ring-number
      ring-type
      spore-print-color
      population
      habitat

      2066
      e
      x
      y
      e
      t
      n
      f
      c
      b
      p
      ...
      s
      p
      p
      p
      w
      o
      p
      n
      v
      d

      7532
      e
      x
      s
      w
      f
      n
      f
      w
      b
      p
      ...
      k
      w
      w
      p
      w
      t
      p
      w
      n
      g

      680
      e
      x
      s
      w
      t
      l
      f
      w
      n
      n
      ...
      s
      w
      w
      p
      w
      o
      p
      n
      v
      d

      6787
      p
      x
      y
      e
      f
      s
      f
      c
      n
      b
      ...
      s
      w
      w
      p
      w
      o
      e
      w
      v
      l

      4383
      p
      x
      y
      y
      f
      f
      f
      c
      b
      h
      ...
      k
      n
      b
      p
      w
      o
      l
      h
      v
      p

5 rows × 23 columns

```python
y_test = list(testing_data.iloc[:,0])
y_pred = genx.predict(testing_data.iloc[:,1:])
#print(y_test)
#print(y_pred)
```

```python
print('Accuracy Score -> {} %'.format(round(genx.accuracy_score(y_test,y_pred),3)))
print('Precison Score -> {}'.format(round(genx.precision_score(y_test,y_pred),3)))
print('Recall Score -> {}'.format(round(genx.recall_score(y_test,y_pred),3)))
```

    Accuracy Score -> 95.372 %
    Precison Score -> 0.92
    Recall Score -> 0.995

```python

```

```python

```


#### Procesamiento Digital de Señales

# Trabajo Práctico Nº0
#### Nicolás Salvia

# Introducción
Jupyter Notebook es una herramienta para la confección de reportes técnicos, dado que permite la interacción en el mismo ambiente de:
1. un procesador de texto elemental (formato Markdown) que permite resaltar texto, en forma de *itálica* o **negrita** de manera muy legible (haciendo doble click en este texto podrás ver el código fuente estilo Markdown). Cuenta con estilos predefinidos:

# Título 1
## Título 2
### Título 3

y también la capacidad de incluir enlaces a otras páginas, como por ejemplo [esta página](https://medium.com/ibm-data-science-experience/markdown-for-jupyter-notebooks-cheatsheet-386c05aeebed) donde encontrarás más funcionalidades del lenguaje **Markdown**

2. capacidad para incluir lenguaje matemático estilo LaTex, tanto de forma presentada

\begin{equation}
T(z) = \frac{Y(z)}{X(z)} = \frac{ b_2 \, z^{-2} + b_1 \,  z^{-1} + b_0 }
{a_2 \, z^{-2} + a_1 \,  z^{-1} + a_0}
\end{equation}

como *inline* en el propio párrafo $y[k] = \frac{1}{a_0} \left( \sum_{m=0}^{M} b_m \; x[k-m] - \sum_{n=1}^{N} a_n \; y[k-n] \right) $

3. La posibilidad de incluir scripts en Python, como los que usaremos para las simulaciones en los TPs de la materia. En este caso usaremos el *testbench0.py* como ejemplo. Una vez que lo probamos y estamos seguros que funciona de forma esperada en *Spyder*, podemos incluir los resultados de la simulación de manera casi transparente. Solo tenemos que agregar una celda de código donde incluimos el código, y los resultados directamente quedan incluidos en este documento.

```python
# Módulos para Jupyter
import warnings
warnings.filterwarnings('ignore')

import numpy as np
import matplotlib as mpl
#%%  Inicialización de librerías
# Setup inline graphics: Esto lo hacemos para que el tamaño de la salida,
# sea un poco más adecuada al tamaño del documento
mpl.rcParams['figure.figsize'] = (10,10)

import matplotlib.pyplot as plt
import pdsmodulos as pds

#%% Esto tiene que ver con cuestiones de presentación de los gráficos,
# NO ES IMPORTANTE
fig_sz_x = 14
fig_sz_y = 13
fig_dpi = 80 # dpi

fig_font_family = 'Ubuntu'
fig_font_size = 16

plt.rcParams.update({'font.size':fig_font_size})
plt.rcParams.update({'font.family':fig_font_family})

##############################################
#%% A partir de aquí comienza lo IMPORTANTE #
#############################################

def my_testbench( sig_type ):

    # Datos generales de la simulación
    fs = 1000.0 # frecuencia de muestreo (Hz)
    N = 1000   # cantidad de muestras

    ts = 1/fs # tiempo de muestreo
    df = fs/N # resolución espectral

    # grilla de sampleo temporal
    tt = np.linspace(0, (N-1)*ts, N).flatten()

    # grilla de sampleo frecuencial
    ff = np.linspace(0, (N-1)*df, N).flatten()

    # Concatenación de matrices:
    # guardaremos las señales creadas al ir poblando la siguiente matriz vacía
    x = np.array([], dtype=np.float).reshape(N,0)
    ii = 0

    # estructuras de control de flujo
    if sig_type['tipo'] == 'senoidal':

        # calculo cada senoidal de acuerdo a sus parámetros
        for this_freq in sig_type['frecuencia']:
            # prestar atención que las tuplas dentro de los diccionarios también pueden direccionarse mediante "ii"
            aux = sig_type['amplitud'][ii] * np.sin( 2*np.pi*this_freq*tt + sig_type['fase'][ii] )
            # para concatenar horizontalmente es necesario cuidar que tengan iguales FILAS
            x = np.hstack([x, aux.reshape(N,1)] )
            ii += 1

    elif sig_type['tipo'] == 'ruido':

        # calculo cada señal de ruido incorrelado (blanco), Gausiano de acuerdo a sus parámetros
        # de varianza
        for this_var in sig_type['varianza']:
            aux = np.sqrt(this_var) * np.random.randn(N,1)
            # para concatenar horizontalmente es necesario cuidar que tengan iguales FILAS
            x = np.hstack([x, aux] )

        # Podemos agregar algún dato extra a la descripción de forma programática
        # {0:.3f} significa 0: primer argunmento de format
        # .3f formato flotante, con 3 decimales
        # $ ... $ indicamos que incluiremos sintaxis LaTex: $\hat{{\sigma}}^2$
        sig_props['descripcion'] = [ sig_props['descripcion'][ii] + ' - $\hat{{\sigma}}^2$ :{0:.3f}'.format( np.var(x[:,ii]))  for ii in range(0,len(sig_props['descripcion'])) ]

    else:

        print("Tipo de señal no implementado.")
        return

    #%% Presentación gráfica de los resultados

    plt.figure(1)
    line_hdls = plt.plot(tt, x)
    plt.title('Señal: ' + sig_type['tipo'] )
    plt.xlabel('tiempo [segundos]')
    plt.ylabel('Amplitud [V]')
    #    plt.grid(which='both', axis='both')

    # presentar una leyenda para cada tipo de señal
    axes_hdl = plt.gca()

    # este tipo de sintaxis es *MUY* de Python
    axes_hdl.legend(line_hdls, sig_type['descripcion'], loc='upper right'  )

    plt.show()

```

Dado que nuestro *testbench* ha sido desarrollado de manera funcional, llamando a la función *my_testbench()* con diferentes parámetros, podemos lograr funcionalidades diferentes, como mostramos a continuación primero con una senoidal:

```python
sig_props = { 'tipo': 'senoidal',
              'frecuencia': (1, 3.33, 6.667), # Uso de tuplas para las frecuencias
              'amplitud':   (1, 1,  1),
              'fase':       (0, 0,  0)
             }
# Como también puedo agregar un campo descripción de manera programática
# este tipo de sintaxis es *MUY* de Pyth0on
sig_props['descripcion'] = [ str(a_freq) + ' Hz' for a_freq in sig_props['frecuencia'] ]

# Invocamos a nuestro testbench exclusivamente:
my_testbench( sig_props )
```

Y ahora con una señal aleatoria, en este caso ruido blanco Gaussiano incorrelado de varianza $\sigma^2$:

```python
# Usar CTRL+1 para comentar o descomentar el bloque de abajo.
sig_props = { 'tipo': 'ruido',
              'varianza': (1, 1, 1) # Uso de tuplas para las frecuencias
             }
sig_props['descripcion'] = [ '$\sigma^2$ = ' + str(a_var) for a_var in sig_props['varianza'] ]

# Invocamos a nuestro testbench exclusivamente:
my_testbench( sig_props )

```

Como puede verse en la figura anterior, al samplear una distribución estadística de media nula y varianza $\sigma^2=1$, obtenemos realizaciones cuyo parámetro $\sigma^2$ estimado, es decir $\hat\sigma^2$, tienen una desviación respecto al verdadero valor (sesgo). Nos ocuparemos de estudiar el sesgo y la varianza de algunos estimadores cuando veamos **Estimación Espectral**.

# Una vez terminado ...
Una vez que hayas termiando con la confección del documento, podemos utilizar una ventaja muy importante de este tipo de documentos que es la posibilidad de compartirlos *online* mediante la [página de nbviewer](http://nbviewer.jupyter.org/). Para ello es necesario que tu notebook y todos los recursos asociados estén alojados en un repositorio de [Github](https://github.com/). Como ejemplo, podés ver este mismo documento disponible [online](http://nbviewer.jupyter.org/github/marianux/pdstestbench/blob/master/notebook0.ipynb).


# Problema de selección de portafolio con preferencias media-varianza

En la clase pasada hablamos acerca de:
- preferencias,
- funciones de utilidad,
- la actitud de los inversionistas de cara al riesgo,
- la aversión al riesgo, entre otros.

Todas ellas son piezas que necesitamos para responder la pregunta de ¿cómo un inversionista toma la decisión óptima de selección de portafolio?

En esta clase al fin estamos listos para ensamblar estos conceptos y escribir el problema de selección de portafolios.

En el camino aprenderemos acerca del concepto de **utilidad esperada**, que nos permite trabajar con incertidumbre en el modelado económico (una de las ideas más importantes en economía). Esta idea tiene más de 60 años, y básicamente dice que los individuos, cuando están de cara a incertidumbre, maximizan el valor esperado de su utilidad (solo cierto si somos homo economicus).

Además del concepto de utilidad esperada, aprenderemos acerca de **preferencias media-varianza**. Es decir, supondremos que los inversionistas toman decisiones basados en un tipo particular de preferencias.

Con lo anterior, estableceremos el problema de selección de portafolios.

**Objetivos:**

- ¿Qué es utilidad esperada?
- ¿Qué son las preferencias media-varianza?
- Funciones de utilidad media-varianza.
- Enunciado y solución del problema básico de selección de portafolio.

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Utilidad esperada
- Básicamente combina las probabilidades de los resultados con cómo los inversionistas se sienten con dichos resultados.
- En otras palabras, la utilidad esperada multiplica la probabilidad de suceso de un evento con la utilidad que genera dicho evento.

Recordemos que las *funciones de utilidad* permiten a los inversionistas expresar cómo se sienten con los resultados, especialmente en los malos ratos.

Entonces la *utilidad esperada* es una herramienta que nos permite cuantificar cómo nos sentimos en nuestros malos momentos económicos, capturando el riesgo con la probabilidad de ocurrencia de dichos malos momentos.

Dado este marco de trabajo, cualquier decisión se puede escribir como la maximización de la utilidad esperada:
\begin{align}
\max_{\theta} & \quad E[U(W)], \\
\end{align}
mediante la escogencia de cierta variable $\theta$ (gastos, planes de ahorro, compra de activos, planes de producción, etc.).

Para nuestros propósitos, la variable de decisión serán los pesos o ponderaciones del portafolio.

Adicionalmente, en el contexto de la decisión de distribución de la riqueza entre activos, el problema de maximización tendrá comúnmente las siguientes restricciones:
- universo de inversión,
- posición en los activos dados.

**Ejemplo.**

Supongamos que un inversionista debe determinar la composición óptima de su portafolio, que contiene activos y bonos. Supongamos que son los únicos instrumentos disponibles.

Sean:
- $w_s$: peso o ponderación de activos en el portafolio,
- $w_b$: peso o ponderación de bonos en el portafolio,
- $r_s$: rendimiento de los activos, y
- $r_b$: rendimiento de los bonos.

De manera que podemos escribir el problema de selección de portafolios como la maximización de la utilidad esperade de nuestra riqueza futura, la cual dependerá de nuestros rendimientos:

\begin{align}
\max_{w_s,w_b} &\quad E[U(W)]\\
\text{s. a.}   &\quad W=W_0(1+w_sr_s+w_br_b)\\
               &\quad w_s+w_b=1
\end{align}

Preguntas:
- ¿Qué significan las restricciones?
- Ya que tenemos planteado este problema básico, ¿qué haría falta para empezar a resolverlo?
___

## 2. Preferencias media-varianza

### 2.1. Utilidad media-varianza

Entonces, ¿qué funciones de utilidad deberíamos de usar en este problema de selección de portafolios?

- La respuesta es: **preferencias media-varianza**.
- Éstas serán representadas en términos de funciones de utilidad como: **utilidad media-varianza**.

Usamos la *utilidad media-varianza* en el problema de selección de portafolios dado que ésta decribe el "trade-off" entre riesgo y rendimiento que enfrentan los inversionistas. La *utilidad media-varianza* está dada por la siguiente expresión:

$$U=E[r_p]-\frac{1}{2}\gamma\sigma_p^2,$$

donde
- $E[r_p]$ es el rendimiento esperado del portafolio,
- $\sigma_p^2$ es la varianza del portafolio, y
- $\gamma$ es el coeficiente de aversión al riesgo.

#### Intuición acerca de la función de utilidad media-varianza:
- Sólo se preocupa por medias :) y varianzas :(.
- Incrementa con: rendimiento esperado del portafolio.
- Decrece con: varianza del portafolio.
- Malos tiempos: rendimientos son bajos y las volatilidades son altas.
- Conecta bastante bien con la teoría moderna de portafolios, la cual caracteriza los rendimientos con medias y varianzas únicamente.
- Criticada por su limitación: supone que los inversionistas sólo se preocupan por medias y varianzas.

### 2.2. Curvas de indiferencia

*¿Recuerdan las curvas de nivel que se ven en cálculo de varias variables?*
- Bien, acá nos servirán para representar la utilidad media-varianza gráficamente.
- En el contexto de utilidad media-varianza, las curvas de nivel se llaman **curvas de indiferencia**.

Dados ciertos niveles de utilidad $U_1>U_2>U_3$, las curvas de indiferencia relativas a estos niveles de utilidad, son los lugares geométricos en el espacio de rendimiento esperado vs. volatilidad representados por las siguientes expresiones

$$U_1=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_1,$$

$$U_2=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_2,$$

$$U_3=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_3.$$

**Gráficamente**

```python
# Importar numpy y pyplot
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Coeficiente de aversión al riesgo (entre 1 y 10 comúnmente)
g = 8
# Niveles de utilidad 3, 2, 1
U1, U2, U3 = 0.3, 0.2, 0.1
# Vector de volatilidades (sugerido 1%-60%)
sp = np.linspace(0.01, 0.60, 100)
# Curvas de indiferencia
Erp1 = U1 + 1 / 2 * g * sp**2
Erp2 = U2 + 1 / 2 * g * sp**2
Erp3 = U3 + 1 / 2 * g * sp**2
```

```python
# Gráfica
plt.plot(sp, Erp1, 'r', lw=2, label='U1')
plt.plot(sp, Erp2, 'b', lw=2, label='U2')
plt.plot(sp, Erp3, 'g', lw=2, label='U3')
plt.xlabel('Volatilidad ($\sigma$)')
plt.ylabel('Rendimiento esperado ($E[r]$)')
plt.legend(loc='best')
plt.grid()
```

Bueno, ¿y porqué se llaman curvas de indiferencia?, ¿qué representa una cuva de indiferencia?

- Porque sobre una misma curva el nivel de utilidad es el mismo (es indiferente).
- Son todas las combinaciones de riesgo y rendimiento que producen un mismo nivel de utilidad.

Volviendo al problema de selección de portafolios, queremos la utilidad más alta.
- ¿Cuál de las anteriores curvas de indiferencia corresponde a la utilidad más alta?
- Intuitivamente, ¿porqué?
- Curvas de indiferencia para niveles de utilidad más altos, estarán...

Notamos además que las anteriores curvas de indiferencia son *paralelas* una con otra. Claro, las dibujamos con el mismo coeficiente de aversión al riesgo.

¿Cómo cambian estas curvas para coeficientes de aversión al riesgo más altos?

```python
# Coeficientes de aversión al riesgo (entre 1 y 10 comúnmente)
g1, g2, g3 = 3, 5, 7
# Nivel de utilidad
U = 0.05
# Vector de volatilidades (sugerido 1%-60%)
sp = np.linspace(0.01, 0.6, 100)
# Curvas de indiferencia
Erp1 = U + 1 / 2 * g1 * sp**2
Erp2 = U + 1 / 2 * g2 * sp**2
Erp3 = U + 1 / 2 * g3 * sp**2
```

```python
# Gráfica
plt.plot(sp, Erp1, 'r', lw=2, label='g1')
plt.plot(sp, Erp2, 'b', lw=2, label='g2')
plt.plot(sp, Erp3, 'g', lw=2, label='g3')
plt.xlabel('Volatilidad ($\sigma$)')
plt.ylabel('Rendimiento esperado ($E[r]$)')
plt.legend(loc='best')
plt.grid()
```

¿Cómo interpretamos las anteriores gráficas?, ¿qué pasa con las personas más aversas al riesgo?
- Se puede ver de dos maneras: para un mismo nivel de rendimiento esperado, una persona más aversa al riesgo soporta un nivel menor de riesgo; equivalentemente, para un mismo nivel de riesgo, una persona más aversa al riesgo requerirá un nivel de rendimiento esperado más alto.

Con todo lo anterior, el problema de selección de portafolios se puede plantear como *"encontrar la curva de indeferencia más alta dado el conjunto de oportunidades de inversión y restricciones"*.

## 3. Problema de selección de portafolios: una ilustración

Ahora ilustraremos el problema de selección de portafolios con algunos datos.
- Por ahora solo queremos ilustrar gráficamente cómo se resuelve este problema. Trabajar en la intuición.
- En las siguientes dos clases nos enfocaremos en cómo resolverlo analíticamente.

Acá tenemos el rendimiento medio anual y la volatilidad para dos instrumentos usando datos de EU: instrumentos de deuda (bonos) y acciones. Supondremos que el inversionista solo puede invertir en estas dos clases de instrumentos.

```python
# Importamos pandas
import pandas as pd
```

```python
# Datos
data=pd.DataFrame(index=['Stocks','Bonds', 'CorrSB'], columns=['Mean', 'Std'])
data['Mean'] = np.array([0.119,0.0591,0.113])
data['Std'] = np.array([0.1915,0.0833,None])
data.round(4)
```

      Mean
      Std

      Stocks
      0.1190
      0.1915

      Bonds
      0.0591
      0.0833

      CorrSB
      0.1130
      None

Entonces, ¿cuál es la distribución de riqueza óptima?, o más bien, ¿cuál es la composición óptima del portafolio para un inversionista dado su nivel de aversión al riesgo?

**Primero.** Recordamos que, para dos activos, podemos trazar la frontera de mínima varianza tomando todas las posibles combinaciones de los dos activos.

De nuevo, sean:
- $w_s=w$: peso o ponderación de activos en el portafolio,
- $w_b=1-w$: peso o ponderación de bonos en el portafolio,
- $r_s$: rendimiento de los activos, y
- $r_b$: rendimiento de los bonos.

Entonces

$$E[r_p]=wE[r_{s}]+(1-w)E[r_b]$$

$$\sigma_p^2=w^2\sigma_{s}^2+(1-w)^2\sigma_b^2+2w(1-w)\rho_{s,b}\sigma_s\sigma_b$$

```python
# Vector de w variando entre 0 y 1 con n pasos
w = np.linspace(0, 1, 100)
# Rendimientos esperados individuales
Ers = data.loc['Stocks', 'Mean']
Erb = data.loc['Bonds', 'Mean']
# Volatilidades individuales
ss = data.loc['Stocks', 'Std']
sb = data.loc['Bonds', 'Std']
# Correlacion
rsb = data.loc['CorrSB', 'Mean']
```

```python
# Crear un DataFrame cuyas columnas sean rendimiento
# y volatilidad del portafolio para cada una de las w
# generadas
portafolios = pd.DataFrame(data={'w': w,
                                 'Media': w * Ers + (1 - w) * Erb,
                                 'Vol': ((w * ss)**2 + ((1 - w) * sb)**2 + 2 * w * (1 - w) * rsb * ss * sb)**0.5
                                }
                          )
portafolios
```

      w
      Media
      Vol

      0
      0.000000
      0.059100
      0.083300

      1
      0.010101
      0.059705
      0.082700

      2
      0.020202
      0.060310
      0.082144

      3
      0.030303
      0.060915
      0.081635

      4
      0.040404
      0.061520
      0.081174

      5
      0.050505
      0.062125
      0.080760

      6
      0.060606
      0.062730
      0.080394

      7
      0.070707
      0.063335
      0.080078

      8
      0.080808
      0.063940
      0.079812

      9
      0.090909
      0.064545
      0.079597

      10
      0.101010
      0.065151
      0.079432

      11
      0.111111
      0.065756
      0.079318

      12
      0.121212
      0.066361
      0.079256

      13
      0.131313
      0.066966
      0.079245

      14
      0.141414
      0.067571
      0.079286

      15
      0.151515
      0.068176
      0.079378

      16
      0.161616
      0.068781
      0.079521

      17
      0.171717
      0.069386
      0.079715

      18
      0.181818
      0.069991
      0.079960

      19
      0.191919
      0.070596
      0.080255

      20
      0.202020
      0.071201
      0.080600

      21
      0.212121
      0.071806
      0.080993

      22
      0.222222
      0.072411
      0.081435

      23
      0.232323
      0.073016
      0.081924

      24
      0.242424
      0.073621
      0.082460

      25
      0.252525
      0.074226
      0.083042

      26
      0.262626
      0.074831
      0.083668

      27
      0.272727
      0.075436
      0.084338

      28
      0.282828
      0.076041
      0.085051

      29
      0.292929
      0.076646
      0.085805

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

      70
      0.707071
      0.101454
      0.140272

      71
      0.717172
      0.102059
      0.141944

      72
      0.727273
      0.102664
      0.143625

      73
      0.737374
      0.103269
      0.145314

      74
      0.747475
      0.103874
      0.147012

      75
      0.757576
      0.104479
      0.148717

      76
      0.767677
      0.105084
      0.150431

      77
      0.777778
      0.105689
      0.152152

      78
      0.787879
      0.106294
      0.153880

      79
      0.797980
      0.106899
      0.155616

      80
      0.808081
      0.107504
      0.157358

      81
      0.818182
      0.108109
      0.159107

      82
      0.828283
      0.108714
      0.160862

      83
      0.838384
      0.109319
      0.162623

      84
      0.848485
      0.109924
      0.164390

      85
      0.858586
      0.110529
      0.166163

      86
      0.868687
      0.111134
      0.167942

      87
      0.878788
      0.111739
      0.169726

      88
      0.888889
      0.112344
      0.171515

      89
      0.898990
      0.112949
      0.173309

      90
      0.909091
      0.113555
      0.175108

      91
      0.919192
      0.114160
      0.176912

      92
      0.929293
      0.114765
      0.178721

      93
      0.939394
      0.115370
      0.180534

      94
      0.949495
      0.115975
      0.182352

      95
      0.959596
      0.116580
      0.184173

      96
      0.969697
      0.117185
      0.185999

      97
      0.979798
      0.117790
      0.187829

      98
      0.989899
      0.118395
      0.189663

      99
      1.000000
      0.119000
      0.191500

100 rows × 3 columns

```python
# Gráfica
plt.plot(portafolios['Vol'], portafolios['Media'], label='Port.')
plt.xlabel('Volatilidad ($\sigma$)')
plt.ylabel('Rendimiento esperado ($E[r]$)')
plt.legend(loc='best')
plt.grid()
```

**Segundo.** Graficamos en la misma ventana, curvas de indiferencia.

```python
# Niveles de utilidad
U1, U2, U3 = 0.04, 0.0474, 0.06
# Coeficiente de aversión al riesgo
g = 8
# Curvas de indiferencia
Erp1 = U1 + 1 / 2 * g * portafolios['Vol']**2
Erp2 = U2 + 1 / 2 * g * portafolios['Vol']**2
Erp3 = U3 + 1 / 2 * g * portafolios['Vol']**2
```

```python
# Gráfica
plt.plot(portafolios['Vol'], Erp1, 'r', lw=2, label='U1')
plt.plot(portafolios['Vol'], Erp2, 'b', lw=2, label='U2')
plt.plot(portafolios['Vol'], Erp3, 'g', lw=2, label='U3')
plt.plot(portafolios['Vol'], portafolios['Media'], label='Port.')
plt.xlabel('Volatilidad ($\sigma$)')
plt.ylabel('Rendimiento esperado ($E[r]$)')
plt.legend(loc='best')
plt.grid()
```

**Tercero.** La elección óptima está dada por la curva de indiferencia para el nivel de utilidad más alto que es tangente a la frontera media-varianza.
- Claramente, esta selección depende del coeficiente de aversión al riesgo.

```python
# Gráfica con zoom
# Gráfica
plt.plot(portafolios['Vol'], Erp1, 'r', lw=2, label='U1')
plt.plot(portafolios['Vol'], Erp2, 'b', lw=2, label='U2')
plt.plot(portafolios['Vol'], Erp3, 'g', lw=2, label='U3')
plt.plot(portafolios['Vol'], portafolios['Media'], label='Port.')
plt.xlabel('Volatilidad ($\sigma$)')
plt.ylabel('Rendimiento esperado ($E[r]$)')
plt.legend(loc='best')
plt.grid()
plt.axis([0.08, 0.10, 0.07, 0.09])
```

# Anuncios parroquiales

## 1. Quiz la siguiente clase.
## 2. Un par de artículos del WSJ y el NYT que discuten herramientas disponibles para la medición de su propia tolerancia al riesgo:
- [Artículo 1](https://www.nytimes.com/2016/02/13/your-money/as-stocks-fall-its-time-to-measure-your-risk-tolerance.html)
- [Artículo 2](https://www.wsj.com/articles/check-your-tolerance-for-investment-risk-now-before-markets-sag-1405619939)

Created with Jupyter by Esteban Jiménez Rodríguez.


# Part II.  Approximate Riemann Solvers

In Part II of this book we present a number of *approximate Riemann solvers*.  We have already seen that for many important hyperbolic systems it is possible to work out the exact Riemann solution for arbitrary left and right states.  However, for complicated nonlinear systems, such as the Euler equations (see [Euler.ipynb](Euler.ipynb)), this exact solution can only be determined by solving a nonlinear system of algebraic equations for the intermediate states and the waves that connect them.  This can be done to arbitrary precision, but only at some computational expense.  The cost of exactly solving a single Riemann problem may seem insignificant, but it can become prohibitively expensive when the Riemann solver is used as a building block in a finite volume method.  In this case a Riemann problem must be solved at every cell edge at every time step.

For example, if we consider a very coarse grid in one space dimension with only 100 cells and take 100 time steps, then 10,000 Riemann problems must be solved.  In solving practical problems in two or three space dimensions it is not unusual to require the solution of billions or trillions of Riemann problems. In this context it can be very important to develop efficient approximate Riemann solvers that quickly produce a sufficiently good approximation to the true Riemann solution.

The following points have helped to guide the development of approximate Riemann solvers:

 - If the solution is smooth over much of the domain, then the jump in states between neighboring cells will be very small (on the order of $\Delta x$, the cell size) for most of the Riemann problems encountered in the numerical solution. Even if the hyperbolic system being studied is nonlinear, for such data the equations can be approximated by a linearization and we have seen that linear Riemann problems can be solved more easily than nonlinear ones.  Rather than solving a nonlinear system of equations by some iterative method, one need only solve a linear system (provided the eigenvalues and eigenvectors of the Jacobian matrix are known analytically, as they often are for practical problems).  In many cases the solution of this linear system can also be worked out analytically and is easy to implement, so no linear algebra is required.

 - In spite of smoothness over much of the domain, in interesting problems there are often isolated discontinuities such as shock waves that are important to model accurately.  So some Riemann problems arising in a finite volume method may have large jumps between the left and right states.  Hence a robust approximate Riemann solver must also handle these cases without introducing too much error.

 - But even in the case of large jumps in the data, it may not be necessary or worthwhile to solve the Riemann problem exactly.  The information produced by the Riemann solver goes into a numerical method that updates the approximate solution in each grid cell and the exact  structure of the Riemann solution is lost in the process.

Each chapter in this part of the book illustrates some common approximate Riemann solvers in the context of one of the nonlinear systems studied in part 1.  We focus on two popular approaches to devising approximate Riemann solvers, though these are certainly not the only approaches: linearized solvers and two-wave solvers.

## Notation and structure of approximate solutions

We consider a single Riemann problem with left state $q_l$ and right state $q_r$.  Then the Riemann solution gives a resolution of the jump $\Delta q = (q_r - q_l)$ into a set of propagating waves.  In both of the approaches described below, the approximate Riemann solution consists entirely of traveling discontinuities.     Following (LeVeque, 2002), we refer to these traveling discontinuities as *waves*, and denote them by ${\cal W}^p \in {\mathbb R}^m$, where the index $p$ denotes the characteristic family and typically ranges from $1$ to $m$ for a system of $m$ equations, although in an approximate solver the number of waves may be smaller.  For each wave, the approximate solver must also give a wave speed $s^p \in{\mathbb R}$.

The net effect of each wave from the Riemann solution is given by the product of the wave and its speed.  In Godunov's method, the simplest finite volume method based on Riemann solvers, the rate of change of the cell average to the left of a given interface is given by
$${\cal A}^-\Delta Q = \sum_{p=1}^m (s^p)^- {\cal W}^p$$
and that to the right is given by
$${\cal A}^+\Delta Q = \sum_{p=1}^m (s^p)^+ {\cal W}^p$$
where $(x)^- = \min(x,0)$ and $(x)^+ = \max(x,0)$.  We refer to ${\cal A}^-\Delta Q, ~\apdq \in {\mathbb R}^m$ as *fluctuations*.

The purpose of an approximate Riemann solver, in this context, is to determine the waves ${\cal W}^p$ and speeds $s^p$; these can then be used to update the numerical solution via the fluctuations.

Note that ${\cal A}^-\Delta Q$, for example, should be read as a single symbol and denotes the m-vector defined above.  The notation is motivated by the case of a constant-coefficient linear hyperbolic system of equations with flux function $f(q) = Aq$ for which $A = R\Lambda R^{-1}$ with real eigenvalues $\lambda_p$.  In this case, one can define a splitting $A = A^- + A^+$ in which $A^+ = R\Lambda^+ R^{-1}$ and $A^- = R\Lambda^- R^{-1}$, where the diagonal matrix $\Lambda^-$ has diagonal elements $\min(\lambda_p, 0)$ while $\Lambda^+$ has diagonal elements $\max(\lambda_p, 0)$. In this case the wave speeds are $s^p = \lambda_p$ and the fluctuations reduce to
$${\cal A}^-\Delta Q = A^-(q_r - q_l), \qquad\text{and}\qquad \apdq = A^+(q_r - q_l).$$

A common, but different, viewpoint is that the Riemann solver must produce an approximation of the flux at $x=0$, and Godunov's method is defined by differencing the flux at the left edge and right edge of the grid cell in each time step.  For a conservation law $q_t + f(q)_x = 0$, we can define the interface flux based on the fluctuations via
$$F = f(q_l) + {\cal A}^-\Delta Q \qquad\text{or} \qquad F = f(q_r) - \apdq.$$
These two expressions are equal provided that
\begin{align}
\label{adqdf}
{\cal A}^-\Delta Q + \apdq = f(q_r) - f(q_l),
\end{align}
which is a natural requirement to impose on the fluctuations and is satisfied, for example, in the case of the linear problem described above.

An advantage of working with fluctuations, waves, and speeds rather than interface fluxes is that these quantities often make sense also for non-conservative hyperbolic systems, such as the variable coefficient linear problem $q_t + A(x)q_x = 0$.  A Riemann problem is defined by prescribing matrices $A_l$ and $A_r$ along with the initial data $q_l$ and $q_r$, for example by using the material properties in the grid cells to the left and right of the interface for acoustics through a heterogeneous material.  The waves are then naturally defined using the eigenvectors of $A_l$ corresponding to negative eigenvalues for the left-going waves, and using eigenvectors of $A_r$ corresponding to positive eigenvalues for the right-going waves.  See (LeVeque, 2002) for more details.  *[notebook for variable coefficient acoustics?]*

## Linearized Riemann solvers
Consider a nonlinear system $q_t + f(q)_x = 0$. If $q_l$ and $q_r$ are close to each other, as is often the case over smooth regions of a more general solution, then the nonlinear system can be approximated by a linear problem of the form $q_t + \hat A q_x = 0$.  The coefficient matrix $\hat A$ should be some approximation to  $f'(q_l) \approx f'(q_r)$ in the case where $\|q_l-q_r\|$ is small.  The idea of a general linearized Riemann solver is to define a matrix $\hat A(q_l, q_r)$ that has this property but also makes sense as an approximation in the case when $\|q_l-q_r\|$ is not small.  For many nonlinear systems there is a *Roe linearization*, a particular function that works works very well based on ideas introduced originally by Philip Roe (Roe, 1981).  For systems such as the shallow water equations or the Euler equations, there are closed-form expressions for the eigenvalues and eigenvectors of $\hat A$ and the solution of the linearized Riemann problem, leading to efficient solvers.

## Two-wave solvers
Since the Riemann solution impacts the overall numerical solution only based on how it modifies the two neighboring solution values, it seems reasonable to consider approximations in which only a single wave propagates in each direction.  The solution will have a single intermediate state $q_m$ such that $\wave_1 = q_m - q_l$ and $\wave_2 = q_r-q_m$.  There are apparently $m+2$ values to be determined: the middle state $q_m$ and the speeds $s_1, s_2$.  In order for the method to be conservative, it must satisfy (\ref{adqdf}), and hence the $m$ conditions
\begin{align}
f(q_r) - f(q_l) = s_1 \wave_1 + s_2 \wave_2.
\end{align}
This can be solved for the middle state to find
\begin{align}  \label{AS:middle_state}
q_m = \frac{f(q_r) - f(q_l) - s_2 q_r + s_1 q_l}{s_1 - s_2}.
\end{align}
It remains only to specify the wave speeds, and it is in this specification that the various two-wave solvers differ.  In the following sections we briefly discuss the choice of wave speed for a scalar problem; the choice for systems will be elaborated in subsequent chapters.

In addition to the names and references provided below, let us mention that this class of solvers is an ingredient in the so-called *central schemes*.  Due to the extreme simplicity of two-wave solvers, the resulting central schemes are often even referred to as being "Riemann-solver-free".

### Lax-Friedrichs (LF) and local-Lax-Friedrichs (LLF)

The simplest such solver is the *Lax-Friedrichs* method, in which it is assumed that both waves have the same speed:
$$-s_1 = s_2 = a,$$
where $a\ge 0$.  Then (\ref{AS:middle_state}) becomes
$$q_m = -\frac{f(q_r) - f(q_l)}{2a} + \frac{q_r + q_l}{2}.$$
In the original Lax-Friedrichs method, the wave speed $a$ is taken to be the same in every Riemann problem over the entire grid; in the local Lax Friedrichs method, a different speed $a$ may be chosen for each Riemann problem.

For stability reasons, the wave speed should be chosen at least as large as the fastest wave speed appearing in the true Riemann solution.  However, choosing a wave speed that is too large leads to diffusion.  For the LLF method (originally due to Rusanov), the wave speed is chosen as
$$a(q_r, q_l) = \max(|f'(q)|)$$
where the maximum is taken over all values of $q$ between $q_r$ and $q_l$.  This ensures stability, but may introduce substantial damping of slower waves.

### Harten-Lax-van Leer (HLL)

A less dissipative solver can be obtained by allowing the left- and right-going waves to have different speeds.
This approach was developed in (Harten, Lax, and van Leer).  The solution is then determined by (\ref{AS:middle_state}).  In the original HLL solver, it was suggested to again to use speeds that bound the possible speeds occurring in the true solution.  For a scalar problem, this translates to
\begin{align*}
s_1 & = \min(f'(q)) \\
s_2 & = \max(f'(q)),
\end{align*}
where again the minimum and maximum are taken over all values between $q_r$ and $q_l$.  Many refinements of this choice have been proposed in the context of systems of equations.  Again, these will be discussed in later chapters.


# Ocean model

```python
# Convenient jupyter setup
%load_ext autoreload
%autoreload 2
%config IPCompleter.greedy=True
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

```python
# set up plotting settings for dark mode.
from jupyterthemes import jtplot
jtplot.style(theme="grade3", context="notebook", ticks=True, grid=False)
from src.plot_utils import ps_defaults
ps_defaults(use_tex=False, dpi=150)
from IPython.display import Latex
```

$r_E\begin{bmatrix}u_E\\ v_E \end{bmatrix} + f \begin{bmatrix}- v_E\\ u_E \end{bmatrix} = \frac{1}{\rho H^{*}} \begin{bmatrix}- \tau^{x}\\ \tau^{y} \end{bmatrix}$

$\begin{bmatrix}u_{m;\;t}\\ v_{m;\; t} \end{bmatrix} + f \begin{bmatrix}- v_m\\ u_m \end{bmatrix} = -g \begin{bmatrix}- h_{m;\;x}\\ h_{m;\;y} \end{bmatrix}  - r_m \begin{bmatrix} u_m\\ v_m \end{bmatrix}$

$u_{m;\;x} + v_{m;\;y} = - h_{m;\;t} - r_m\cdot h_{m} $

$w_s = H_s\left(u_{s;\;x} + v_{s;\;y}\right)$

$T_{s;\;t} + \begin{bmatrix} u_s\\ v_s \\ w_s \end{bmatrix} \cdot \nabla T_s  + \gamma M(w_{s}) \frac{T_s - T_d}{H_s}  = - \frac{\bar{Q} + Q'}{\rho c_{pw} H_s}$

$\frac{1}{H^{*}}= \frac{1}{H_s} - \frac{1}{D_1} - \frac{1}{D_2}$

%%latex

\begin{align}

r_E\begin{bmatrix}u_E\\ v_E \end{bmatrix} + f \begin{bmatrix}- v_E\\ u_E \end{bmatrix} = \frac{1}{\rho H^{*}} \begin{bmatrix}- \tau^{x}\\ \tau^{y} \end{bmatrix}\\

\begin{bmatrix}u_{m;\;t}\\ v_{m;\; t} \end{bmatrix} + f \begin{bmatrix}- v_m\\ u_m \end{bmatrix} = -g \begin{bmatrix}- h_{m;\;x}\\ h_{m;\;y} \end{bmatrix}  - r_m \begin{bmatrix} u_m\\ v_m \end{bmatrix} \\

u_{m;\;x} + v_{m;\;y} = - h_{m;\;t} - r_m\cdot h_{m} \\

\end{align}

```python
from src.configs.load_config import load_config
from src.models.model_setup import ModelSetup
from src.constants import TEST_DIREC
from src.models.ocean import Ocean
```

```python
cfg = load_config()
setup = ModelSetup(str(TEST_DIREC))
```

    {'project': 'seager19', 'user': 'test_user', 'test': True, 'notes': 'test', 'run': True, 'animate': True, 'atmos': True, 'ocean': {'tcom_name': 'tcom', 'tios2cdf_name': 'tios2cdf', 'spin': False, 'diag': False, 'ingrid': False, 'run_through': False}, 'oc': {'nummode': 2, 'boundaries': 'Periodic', 'wind_file': 'tau-ECMWF-clim', 'dq_dtemp_file': 'dQdT-sample.nc', 'dq_df_file': 'dQdf-sample.nc', 'mask_file': 'om_mask.nc', 'time_test': '2 months', 'time_spin': '2 months', 'time_diag': '2 years', 'time_run2f': '58 years'}, 'coup': {'c_d': 0.00225, 'rho_air': 1.225}, 'atm': {'k_days': 10, 'e_frac': 2.0, 'prcp_land': 1, 'r': 0.8, 'h_q': 1800, 'wnsp_min': 4, 'rho_00': 0.3, 'pr_max': '20.0 / 3600 / 24', 'relative_humidity': 0.8, 'number_iterations': 50, 'height_tropopause': 15000.0, 'theta_00': 300, 'nbsq': 0.0003, 'radius_earth': 6370000.0, 'pi': 3.141592653589793, 'stefan_boltzman_const': 5.67e-08, 'sec_in_day': 86400, 'gravity': 9.8, 'temp_0_c': 273.15, 'omega_2': '2 * 2 * ${atm.pi} / ${atm.sec_in_day}', 'latent_heat_vap': 2500000.0, 'cp_air': 1000, 'b_coeff': '${atm.gravity} * ${atm.pi} / ${atm.nbsq} / ${atm.theta_00} / ${atm.height_tropopause}', 'eps_days': 0.75, 'eps': '1.0 / ${atm.eps_days} / ${atm.sec_in_day}', 'eps_u': '${atm.eps}', 'eps_v': '${atm.e_frac} * ${atm.eps}', 'newtonian_cooling_coeff_k1': '${atm.b_coeff} / ${atm.k_days} / ${atm.sec_in_day}', 'eps_p': '(${atm.pi} / ${atm.height_tropopause}) ** 2 / ${atm.nbsq} / ${atm.k_days} / ${atm.sec_in_day}', 'beta': '${atm.omega_2} / ${atm.radius_earth}', 'rho_air': 1.225, 'emmisivity': 0.97, 'p_s': 1000, 'es_0': 6.11, 'delta_temp': 1.0, 'f2': 0.05, 'a_cloud_const': 0.6, 'f1_bar': 0.39, 'u_bar': 5.0, 'temp_surface_bar': '${atm.temp_0_c} + 25', 'c_e': 0.00125, 'c_bar': 0.6, 'qlh_coeff': '${atm.rho_air} * ${atm.c_e} * ${atm.latent_heat_vap}', 'qlw_coeff': '${atm.emmisivity} * ${atm.stefan_boltzman_const}', 'nx': 180, 'ny': 60, 'y_north_lim': 60, 'y_south_lim': '-${atm.y_north_lim} / 1', 'dx': '360 / ${atm.nx}', 'dy': '(${atm.y_north_lim} - ${atm.y_south_lim}) / ${atm.ny}'}, 'name': 'test_run'}
    {'project': 'seager19', 'user': 'test_user', 'test': True, 'notes': 'test', 'run': True, 'animate': True, 'atmos': True, 'ocean': {'tcom_name': 'tcom', 'tios2cdf_name': 'tios2cdf', 'spin': False, 'diag': False, 'ingrid': False, 'run_through': False}, 'oc': {'nummode': 2, 'boundaries': 'Periodic', 'wind_file': 'tau-ECMWF-clim', 'dq_dtemp_file': 'dQdT-sample.nc', 'dq_df_file': 'dQdf-sample.nc', 'mask_file': 'om_mask.nc', 'time_test': '2 months', 'time_spin': '2 months', 'time_diag': '2 years', 'time_run2f': '58 years'}, 'coup': {'c_d': 0.00225, 'rho_air': 1.225}, 'atm': {'k_days': 10, 'e_frac': 2.0, 'prcp_land': 1, 'r': 0.8, 'h_q': 1800, 'wnsp_min': 4, 'rho_00': 0.3, 'pr_max': 0.0002314814814814815, 'relative_humidity': 0.8, 'number_iterations': 50, 'height_tropopause': 15000.0, 'theta_00': 300, 'nbsq': 0.0003, 'radius_earth': 6370000.0, 'pi': 3.141592653589793, 'stefan_boltzman_const': 5.67e-08, 'sec_in_day': 86400, 'gravity': 9.8, 'temp_0_c': 273.15, 'omega_2': 0.0001454441043328608, 'latent_heat_vap': 2500000.0, 'cp_air': 1000, 'b_coeff': 0.022805635559392575, 'eps_days': 0.75, 'eps': 1.54320987654321e-05, 'eps_u': '${atm.eps}', 'eps_v': 3.08641975308642e-05, 'newtonian_cooling_coeff_k1': 2.639541152707474e-08, 'eps_p': 1.6923189988150473e-10, 'beta': 2.2832669440009544e-11, 'rho_air': 1.225, 'emmisivity': 0.97, 'p_s': 1000, 'es_0': 6.11, 'delta_temp': 1.0, 'f2': 0.05, 'a_cloud_const': 0.6, 'f1_bar': 0.39, 'u_bar': 5.0, 'temp_surface_bar': '${atm.temp_0_c} + 25', 'c_e': 0.00125, 'c_bar': 0.6, 'qlh_coeff': 3828.125, 'qlw_coeff': 5.4998999999999995e-08, 'nx': 180, 'ny': 60, 'y_north_lim': 60, 'y_south_lim': -60.0, 'dx': 2.0, 'dy': 2.0}, 'name': 'test_run'}

```python
ocean = Ocean(cfg, setup)
ocean.compile_all()
if cfg.run:
   ocean.run_all()
if cfg.animate:
   ocean.animate_all()
```

    editing  /home/users/sithom/seager19/src/test/test_direc/ocean/RUN/om_spin
    editing  /home/users/sithom/seager19/src/test/test_direc/ocean/RUN/om_diag
    editing  /home/users/sithom/seager19/src/test/test_direc/ocean/RUN/om_run2f
    cd /home/users/sithom/seager19/src/test/test_direc/ocean/RUN
    ../SRC/tcom -i om_test 2.03007 s

    'run_all'  2.05800 s

    'animate_all'  0.00031 s

```python
import os
import xarray as xr
```

```python
xr_da = xr.open_dataset(os.path.join(setup.ocean_data_path, cfg.oc.dq_dtemp_file), decode_times=False)
print(xr_da)
```

    <xarray.Dataset>
    Dimensions:  (T: 12, X: 360, Y: 181, Z: 1)
    Coordinates:
      * Y        (Y) float32 -90.0 -89.0 -88.0 -87.0 -86.0 ... 87.0 88.0 89.0 90.0
      * Z        (Z) float32 1.0
      * T        (T) float32 0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5 11.5
      * X        (X) float32 0.0 1.0 2.0 3.0 4.0 ... 355.0 356.0 357.0 358.0 359.0
    Data variables:
        dQdT     (T, Z, Y, X) float32 ...

```python
xr_da = xr.open_dataset(os.path.join(setup.ocean_data_path, cfg.oc.dq_df_file), decode_times=False)
print(xr_da)
```

    <xarray.Dataset>
    Dimensions:  (T: 12, X: 360, Y: 181, Z: 1)
    Coordinates:
      * Y        (Y) float32 -90.0 -89.0 -88.0 -87.0 -86.0 ... 87.0 88.0 89.0 90.0
      * Z        (Z) float32 1.0
      * T        (T) float32 0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5 11.5
      * X        (X) float32 0.0 1.0 2.0 3.0 4.0 ... 355.0 356.0 357.0 358.0 359.0
    Data variables:
        dQdf     (T, Z, Y, X) float32 ...

```python
os.listdir(setup.ocean_data_path)
```

    ['dQdf-sample.nc',
     'dQdT-sample.nc',
     'om_mask.nc',
     'qflx-0.nc',
     'qflx.nc',
     'qflx-test.nc',
     'rzk.pro',
     'spline_ECMWF.txt',
     'sst-ECMWF-clim.nc',
     'tau-ECMWF-clim.x',
     'tau-ECMWF-clim.y',
     'tau-ECMWF.x',
     'tau-ECMWF.y']

```python
os.path.join(setup_old.atmos_data_path, "mask-360x180.nc")
```

    '/home/users/sithom/seager19/atmos/DATA/mask-360x180.nc'

```python
xr_da = xr.open_dataset(
    os.path.join(setup_old.atmos_data_path, "ts-ECMWF-clim.nc"),
    decode_times=False,
)
print(xr_da)
```

    <xarray.Dataset>
    Dimensions:  (X: 360, Y: 181)
    Coordinates:
      * Y        (Y) float32 -90.0 -89.0 -88.0 -87.0 -86.0 ... 87.0 88.0 89.0 90.0
      * X        (X) float32 0.0 1.0 2.0 3.0 4.0 ... 355.0 356.0 357.0 358.0 359.0
    Data variables:
        ts       (Y, X) float32 ...

```python
xr_da = xr.open_dataset(
    os.path.join(setup_old.atmos_data_path, "sfcWind-ECMWF-clim.nc"),
    decode_times=False,
)
print(xr_da)
```

    <xarray.Dataset>
    Dimensions:  (X: 360, Y: 181)
    Coordinates:
      * X        (X) float32 0.0 1.0 2.0 3.0 4.0 ... 355.0 356.0 357.0 358.0 359.0
      * Y        (Y) float32 -90.0 -89.0 -88.0 -87.0 -86.0 ... 87.0 88.0 89.0 90.0
    Data variables:
        sfcWind  (Y, X) float64 ...

```python
xr_da = xr.open_dataset(
    os.path.join(setup_old.ocean_output_path, "om_spin.nc"),
    decode_times=False,
)
print(xr_da.STRESS_TAUX)
xr_da.STRESS_TAUX.isel(T_01=200, L_01=0).plot(y="X_01", x="Y_01")
```

```python
xr_da.STRESS_TAUX.isel(T_01=200, L_01=0).plot(y="X_01", x="Y_01")
```

```python
xr_da = xr.open_dataset(
    os.path.join(setup_old.ocean_output_path, "om_diag.nc"),
    decode_times=False,
)
print(xr_da)
```

    <xarray.Dataset>
    Dimensions:       (GRID: 1, L_01: 1, L_02: 1, L_03: 1, T_01: 25, T_02: 25, T_03: 25, X_01: 360, X_02: 360, X_03: 360, Y_01: 161, Y_02: 161, Y_03: 161)
    Coordinates: (12/13)
      * GRID          (GRID) float32 0.0
      * X_01          (X_01) float32 0.0 1.0 2.0 3.0 4.0 ... 356.0 357.0 358.0 359.0
      * Y_01          (Y_01) float32 -80.0 -79.0 -78.0 -77.0 ... 77.0 78.0 79.0 80.0
      * L_01          (L_01) float32 1.0
      * T_01          (T_01) float32 -48.02 -47.08 -46.09 ... -26.08 -25.09 -24.08
      * X_02          (X_02) float32 0.0 1.0 2.0 3.0 4.0 ... 356.0 357.0 358.0 359.0
        ...            ...
      * L_02          (L_02) float32 0.0
      * T_02          (T_02) float32 -48.02 -47.08 -46.09 ... -26.08 -25.09 -24.08
      * X_03          (X_03) float32 0.0 1.0 2.0 3.0 4.0 ... 356.0 357.0 358.0 359.0
      * Y_03          (Y_03) float32 -80.0 -79.0 -78.0 -77.0 ... 77.0 78.0 79.0 80.0
      * L_03          (L_03) float32 1.0
      * T_03          (T_03) float32 -48.02 -47.08 -46.09 ... -26.08 -25.09 -24.08
    Data variables:
        SST_SST       (T_01, L_01, Y_01, X_01) float32 ...
        SST_QFLX      (T_01, L_01, Y_01, X_01) float32 ...
        DYN_PRES      (T_02, L_02, Y_02, X_02) float32 ...
        TDEEP_HMODEL  (T_03, L_03, Y_03, X_03) float32 ...
        TDEEP_TDEEP   (T_03, L_03, Y_03, X_03) float32 ...
    Attributes:
        model:          output/om_diag.data
        missing_value:  -987654.0

```python
xr_da = xr.open_dataset(
    os.path.join(os.path.join(setup_old.ocean_output_path, "om_run2f.nc")),
    decode_times=False,
)
print(xr_da)
```

    <xarray.Dataset>
    Dimensions:       (GRID: 1, L_01: 1, L_02: 1, L_03: 1, T_01: 697, T_02: 697, T_03: 697, X_01: 360, X_02: 360, X_03: 360, Y_01: 161, Y_02: 161, Y_03: 161)
    Coordinates: (12/13)
      * GRID          (GRID) float32 0.0
      * X_01          (X_01) float32 0.0 1.0 2.0 3.0 4.0 ... 356.0 357.0 358.0 359.0
      * Y_01          (Y_01) float32 -80.0 -79.0 -78.0 -77.0 ... 77.0 78.0 79.0 80.0
      * L_01          (L_01) float32 0.0
      * T_01          (T_01) float32 -24.02 -23.08 -22.09 ... 669.9 670.9 671.9
      * X_02          (X_02) float32 0.0 1.0 2.0 3.0 4.0 ... 356.0 357.0 358.0 359.0
        ...            ...
      * L_02          (L_02) float32 1.0
      * T_02          (T_02) float32 -24.02 -23.08 -22.09 ... 669.9 670.9 671.9
      * X_03          (X_03) float32 0.0 1.0 2.0 3.0 4.0 ... 356.0 357.0 358.0 359.0
      * Y_03          (Y_03) float32 -80.0 -79.0 -78.0 -77.0 ... 77.0 78.0 79.0 80.0
      * L_03          (L_03) float32 1.0
      * T_03          (T_03) float32 -24.02 -23.08 -22.09 ... 669.9 670.9 671.9
    Data variables:
        DYN_PRES      (T_01, L_01, Y_01, X_01) float32 ...
        TDEEP_HTHERM  (T_02, L_02, Y_02, X_02) float32 ...
        TDEEP_TDEEP   (T_02, L_02, Y_02, X_02) float32 ...
        TDEEP_HMODEL  (T_02, L_02, Y_02, X_02) float32 ...
        SST_SST       (T_03, L_03, Y_03, X_03) float32 ...
    Attributes:
        model:          output/om_run2f.data
        missing_value:  -987654.0

## Varying ocean model

```python
import os
import matplotlib.pyplot as plt
import pandas as pd
from src.constants import DATA_PATH
```

```python
pd_obj = pd.read_csv(os.path.join(DATA_PATH, "nummode.csv"))
counter = 0
nummode_l = list()
time_l = list()
for i in pd_obj:
    if "MIN" not in i and "MAX" not in i and "nummode" in i:
        print(i)
        print(10 - counter)
        counter += 1
        print(pd_obj[i].values[0])
        time_l.append(pd_obj[i].values[0])
        nummode_l.append(11-counter)
```

    nummode_10_uncoupled_test - ocean_run
    10
    5147.776633918984
    nummode_9_uncoupled_test - ocean_run
    9
    4525.170610727044
    nummode_8_uncoupled_test - ocean_run
    8
    3929.120938214997
    nummode_7_uncoupled_test - ocean_run
    7
    3444.572780891001
    nummode_6_uncoupled_test - ocean_run
    6
    3468.7198799189937
    nummode_5_uncoupled_test - ocean_run
    5
    3071.88644934402
    nummode_4_uncoupled_test - ocean_run
    4
    2641.4500270090066
    nummode_3_uncoupled_test - ocean_run
    3
    2098.2206076779985
    nummode_2_uncoupled_test - ocean_run
    2
    1640.786252616992
    nummode_1_uncoupled_test - ocean_run
    1
    1225.6663067650225

```python
plt.plot(nummode_l, time_l) #, "x", markersize=2, color="red", alpha=1)
plt.xlabel("Number of Ocean Model Modes")
plt.ylabel("Time taken to run om_run2f (s)")
```

```python

```