# Milestone 1

## Introduction

Automatic differentiation is a tool for calculating derivatives using machine accuracy. It has several advantages over traditional methods of derivative calculations such as symbolic and finite differentiation. Automatic differentiation is useful for calculating complex derivatives where errors are more likely with classical methods. For instance , with finite differentiation, h values that are too small will lead to accuracy errors though floating point roundoff error, while h values that are too large will start making vastly inaccurate approximations.

Automatic differentiation is useful due to its practicality in real world applications that involve thousands of parameters in a complicated function, which would take a long runtime as well as strong possibility for error in calculating the derivatives individually.

Our package allows users to calculate derivatives of complex functions, some with many parameters, allowing machine precision.

## Background

Essentially automatic differentiation works by  breaking down a complicated function and performing a sequence of elementary arithmetic such as addition, subtraction, multiplication, and division as well as elementary functions like exp, log, sin, etc. These operations are then repeated by the chain rule and the derivatives of these sequences are calculated. There are two ways that automatic differentiation can be implemented - forward mode and reverse mode.

### 2.1 The Chain Rule

The chain rule makes up a fundamental component of auto differentiation. The basic idea is:
For univariate function, $$ F(x) = f(g(x))$$
 $$F^{\prime} = (f(g))^{\prime} = f^{\prime}(g(x))g^{\prime}(x)$$
For multivariate function, $$F(x) = f(g(x),h(x))$$
$$ \frac{\partial F}{\partial x}=\frac{\partial f}{\partial g} \frac{\partial g}{\partial x}+\frac{\partial f}{\partial h} \frac{\partial h}{\partial x}$$
For more generalized cases, if F is a combination of more sub-functions,  $$F(x) = f(g_{1}(x), g_{2}(x), …, g_{m}(x))$$
$$\frac{\partial F}{\partial x}=\sum_{i=1}^{m}\frac{\partial F}{\partial g_{i}} \frac{\partial g_{i}}{\partial x}$$

### 2.2 Forward Mode

The forward mode automatic differentiation is accomplished by firstly splitting the function process into one-by-one steps, each including only one basic operation. Then from the first node, the value and derivative will be calculated based on the values and derivatives of forward nodes. An example of computational graph and table for forward mode AD is shown as follows:

\begin{align}
  f\left(x,y\right) =\sin\left(xy\right)
\end{align}
We will be evaluating the function at $f(1, 0)$

Evaluation trace:

| Trace   | Elementary Function      | Current Value           | Elementary Function Derivative       | $\nabla_{x}$ Value  | $\nabla_{y}$ Value  |
| :---: | :-----------------: | :-----------: | :----------------------------: | :-----------------:  | :-----------------: |
| $x_{1}$ | $x_{1}$                  | $1$        | $\dot{x}_{1}$                        | $1$ | $0$ |
| $x_{2}$ | $x_{2}$                  | $0$        | $\dot{x}_{2}$                        | $0$ | $1$ |
| $x_{3}$ | $x_{1}x_{2}$                  | $0$        | $\dot{x}_{2}$                        | $0$ | $1$ |

### 2.3 Reverse Mode

The reverse mode automatic differentiation has a process similar to the forward mode auto differentiation, but has another reverse process. It does not apply the chain rule and only partial derivatives to a node are stored. First, for the forward process, the partial derivatives are stored for each node. For the reverse process, it starts with the differentiation to the last node, and then activations in the forward process are deployed in the differentiation differentiation step by step.

### 2.4 Forward Mode v.s. Reverse Mode

Two main aspects can be considered when choosing between Forward and Reverse mode auto differentiation.
* Memory Storage & Time of Computation

The forward mode needs memory storage for values and derivatives for each node, while the reverse mode only needs to store the activations of partial differentiation to each node. The forward mode do the computation at the same time as the variable evaluation, while the reverse mode do the calculation in the backward process.
* Input & Output Dimensionality

If the input dimension is much larger than output dimension, then reverse mode is more attractive. If the output dimension is much larger than the input dimension, the forward mode is much computational cheaper.

## How to use VorDiff

Our Automatic Differentiation package is called VorDiff. The two main objects you will interact with are `autodiff` and `operator`. In short, the user will first instantiate a variable (scalar or vector) as an `autodiff` object, and then feed those variables to operators specified in the `operator` object. The `operator` object allows users to build their own functions for auto-differentiation. Simple operations (addition, subtraction, multiplication, and division) may be used normally. More complex functions (e.g. sqrt, sin, cos) must use the operations defined in the `operator` class. Lastly, the user may retrieve the values and first derivatives from the objects defined above by using the `val` and `der` attributes of the `autodiff` object.

A short example is provided below:
```py
from VorDiff import autodiff
from VorDiff import operator

# Define variables
x = autodiff.scalar(val = 4)
v = autodiff.vector(vals = [4,2])

# Build functions
fx = 4*operator.sqrt(x) + x
fv = 4*operator.sqrt(v) + v

# Get values and derivates
print(fx.val, fx.der)
print(fv.val, fv.der)
```

## Software Organization

### Directory Structure
The package's directory will be structured as follows:
```
VorDiff
	__init__.py
    nodes
	    node.py
	    scalar.py
	    vector.py
	tests
	    test_nodes
		    test_node.py
		    test_scaler.py
		    test_vector.py
		test_autodiff.py
        test_operator.py
	docs
		...
	demo
	    ...
	autodiff.py
    operator.py
    README.md
    ...
```
### Modules
-   VorDiff: The VorDiff module contains the operator class to be directly used by users to evaluate functions and calculate their derivatives, a class node and subclasses scalar and vector to be used in autodiff class, and an autodiff class to perform automatic differentiation. This is the core of the package.

-   Test_Vordiff: The Test_Vordiff module contains the test suite for this project. TravisCI and CodeCov are used to test our operator classes, node classes, and auto-differentiator.

-   Demo: The Demo module contains python files demonstrating how to perform automatic differentiation with the implemented functions.

### Testing
In this project we will use TravisCI to perform continuous integration testing and CodeCov to check the code coverage of our test suite. The status us TravisCI and CodeCov can be found in README.md, in the top level of our package. Since the test suite is included in the project distribution, users can also install the project package and use pytest and pytest-cov to check the test results locally.

### Distribution:
Our open-source VorDiff package will be uploaded to PyPI by using twine because it uses a verified connection for secure authentication to PyPI over HTTPS. Users will be able to install our project package by using the convential `pip install VorDiff`.

## Implementation

### Node, Scalar, and Vector
The `Node` class represents a single node in the computational graph of a function. It outlines the operations necessary for the `scalar` and `vector` subclasses to implement.
```py
class Node():

    def __init__(self):

    def __add__(self, other):

    def __radd__(self, other):
   ...
   ```

### Operator
The operator class contains all mathematical operations that users can call to build their functions. Each function returns a `Node` object.
```py
from nodes import node

class Operator():

    def __init__(self):

    def sqrt(self, x):
        '''
        Perform operation
        '''
        return new_node

    def sin(self, x):
        '''
        Perform operation
        '''
        return new_node
```

The elementary function objects are analytically differentiable and some are defined in the numpy package. We will include the following elementary functions:

- Trigonometric functions: sin, cos, tan
- Inverse Trigonometric functions: arcsin, arccos, arctan
- Hyperbolic functions: sinh, cosh, tanh, arcsinh, arccosh, arctanh
- Exponents: exp, exp2, expm1
- Logarithms: log, log2, log10, log1p

The derivatives of these elementary function objects and the self-defined elementary function objects (such as power and sqrt) can be evaluated with the chain rule.

### AutoDiff
The `AutoDiff` class will allow the user to easily create variables and build auto-differentiable functions, without having to interface with the `Node` class. It will make use of the auto-differentiator much more intuitive for the user.
```py
import nodes.scalar
import nodes.vector

class AutoDiff():

    def __init__(self):

    def scalar(self, val: float = 0):
        return nodes.scalar(val)

    def vector(self, vals):
        return nodes.vector(vals)
```

### External dependencies:
We will use numpy package for mathematical computation (such as sin, log, exp), and pytest and pytest-cov for the test suite.

```python

```


# Linear Models

There's a few linear models out there that we felt were generally useful. This
document will highlight some of them.

## LOWESS

Lowess stands for LOcally WEighted regreSSion and has historically
been used for smoothing but you can also use it
for machine learning where you're interested in interpolating. Here's
a demo;

```python
import numpy as np
import matplotlib.pylab as plt
from sklego.linear_model import LowessRegression

n = 100
xs = np.linspace(0, np.pi, n)
ys = 1 + np.sin(xs) + np.cos(xs**2) + np.random.normal(0, 0.1, n)

mod = LowessRegression(sigma=0.1, span=0.5).fit(xs.reshape(-1, 1), ys)

xs_new = np.linspace(-1, np.pi + 1, n*2)
preds = mod.predict(xs_new.reshape(-1, 1))

plt.figure(figsize=(12, 4))
plt.scatter(xs, ys)
plt.plot(xs_new, preds, color='orange')
plt.title("Be careful with extrapolation here.");
```

The line does not look linear but that's because internally, during prediction,
many weighted linear regressions are happening. The gif below demonstrates how
the data is being weighted when we would make a prediction.

### Details on `sigma`.

We'll also show two different prediction outcomes depending on the hyperparameter
`sigma`.

You may be tempted now to think that a lower sigma always has a better fit, but you
need to be careful here. The data might have gaps and larger sigma values will be
able to properly regularize.

Note that this regression also works in higher dimensions but the main downside of
this approach is that it is really slow when making predictions.

If you really want to get advanced there's also a hyperparameter `span` but you'll
really need to know what you're doing. It was added for completeness but the authors
of this package have yet to find a proper usecase for it.

### Details on `span`.

The `span` parameter can be used to force that you'll only include a certain
  percentage of your dataset. Technically without a `span` you are still using
  all the data in your dataset, albeit with small weights if they are far away.

The effect of the `span` parameter on the weights can be seen below.

This will also effect the predictions.

You may need to squint your eyes a bit to see it, but lower spans cause more
  jiggles and less smooth curves.

## ProbWeightRegression

> Note, this is a somewhat experimental feature. We found this feature to be plausibly useful but we've not seen it cause a big "win" yet.

Let's say that you're interested in combining a few models for a regression task.
You could put them together in an ensemble. Say we've got predictions $y_1, y_2, y_3$, each of which come from respectable models, then you may want to combine these together in another linear regression. This way the new, hopefully best, prediction $y_*$ is defined via;

$$y^* = w_1 y_1 + w_2 y_2 + w_3 y_3$$

This can be a valid way of reweighing. But there's one issue:
technically the weights $w_1, w_2, w_3$ can sum to a number that isn't one. Since that's
numerically totally possible we need to be aware that we can end up in a strange situation.

The `ProbWeightRegression` adresses this. It assumes that every input it receives is
the output of a model and it will ensure that they are reweighed with a constraint in mind. For this usecase, it would optimise;

\begin{equation} \label{eq1}
\begin{split}
\text{min} & \sum_i (y_i - (w_1 y_{1,i} + w_2 y_{2,i} + w_3 y_{3,i}))^2 \\
\text{subject to} & \\
& \text{ } w_1 + w_2 + w_3 = 1 \\
& \text{ } w_1 \geq 0, w_2 \geq 0, w_3 \geq 0
\end{split}
\end{equation}

The final positivity constraint is optional in our model.

Here's an example usage.

```python
from sklearn.datasets import make_regression
import pandas as pd

X, y = make_regression(n_samples=1000, n_features=10, random_state=42)
df = pd.DataFrame(X)
```

We've turned the array into a dataframe so that we can apply the `ColumnSelector`.

```python
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.linear_model import LinearRegression
from sklearn.decomposition import PCA
from sklearn.model_selection import GridSearchCV

from sklego.meta import EstimatorTransformer
from sklego.linear_model import ProbWeightRegression
from sklego.preprocessing import ColumnSelector

pipe = Pipeline([
    ('models', FeatureUnion([
        ('path1', Pipeline([
            ('select1', ColumnSelector([0,1,2,3,4])),
            ('pca', PCA(n_components=3)),
            ('linear', EstimatorTransformer(LinearRegression()))
        ])),
        ('path2', Pipeline([
            ('select2', ColumnSelector([5,6,7,8,9])),
            ('pca', PCA(n_components=2)),
            ('linear', EstimatorTransformer(LinearRegression()))
        ]))
    ])),
    ('prob_weight', ProbWeightRegression())
])

grid = GridSearchCV(estimator=pipe, param_grid={}, cv=3).fit(df, y)
```

```python
from sklearn import set_config
set_config(display='diagram')
grid
```

<input class="sk-toggleable__control sk-hidden--visually" id="1f1cc5db-07d1-470d-aa5a-253d7d316602" type="checkbox" >GridSearchCV

GridSearchCV(cv=3,
             estimator=Pipeline(steps=[('models',
                                        FeatureUnion(transformer_list=[('path1',
                                                                        Pipeline(steps=[('select1',
                                                                                ColumnSelector(columns=[0,
                                                                                1,
                                                                                2,
                                                                                3,
                                                                                4])),
                                                                                ('pca',
                                                                                PCA(n_components=3)),
                                                                                ('linear',
                                                                                EstimatorTransformer(estimator=LinearRegression()))])),
                                                                       ('path2',
                                                                        Pipeline(steps=[('select2',
                                                                                ColumnSelector(columns=[5,
                                                                                6,
                                                                                7,
                                                                                8,
                                                                                9])),
                                                                                ('pca',
                                                                                PCA(n_components=2)),
                                                                                ('linear',
                                                                                EstimatorTransformer(estimator=LinearRegression()))]))])),
                                       ('prob_weight',
                                        ProbWeightRegression())]),
             param_grid={})

<input class="sk-toggleable__control sk-hidden--visually" id="722adf51-b4eb-4aea-acce-ff4fd0835955" type="checkbox" >models: FeatureUnion

FeatureUnion(transformer_list=[('path1',
                                Pipeline(steps=[('select1',
                                                 ColumnSelector(columns=[0, 1,
                                                                         2, 3,
                                                                         4])),
                                                ('pca', PCA(n_components=3)),
                                                ('linear',
                                                 EstimatorTransformer(estimator=LinearRegression()))])),
                               ('path2',
                                Pipeline(steps=[('select2',
                                                 ColumnSelector(columns=[5, 6,
                                                                         7, 8,
                                                                         9])),
                                                ('pca', PCA(n_components=2)),
                                                ('linear',
                                                 EstimatorTransformer(estimator=LinearRegression()))]))])

path1

<input class="sk-toggleable__control sk-hidden--visually" id="ee9ceae5-59b7-4c88-9fbf-9505349fa887" type="checkbox" >ColumnSelector

ColumnSelector(columns=[0, 1, 2, 3, 4])

<input class="sk-toggleable__control sk-hidden--visually" id="e0129048-6eb8-47bc-a8c0-ae9d96d63cd1" type="checkbox" >PCA

PCA(n_components=3)

<input class="sk-toggleable__control sk-hidden--visually" id="fe8293b5-d914-4a59-8c39-e2b4e18851c8" type="checkbox" >linear: EstimatorTransformer

EstimatorTransformer(estimator=LinearRegression())

<input class="sk-toggleable__control sk-hidden--visually" id="41c53c2a-d2c2-4241-b549-8f7653a0029f" type="checkbox" >LinearRegression

LinearRegression()

path2

<input class="sk-toggleable__control sk-hidden--visually" id="86b9c93e-12be-4fa6-865f-bb874ff99129" type="checkbox" >ColumnSelector

ColumnSelector(columns=[5, 6, 7, 8, 9])

<input class="sk-toggleable__control sk-hidden--visually" id="ec46eb32-4807-466f-a47b-dba193ff5315" type="checkbox" >PCA

PCA(n_components=2)

<input class="sk-toggleable__control sk-hidden--visually" id="5efe2360-3f3b-4c67-bc8f-e2619f06d229" type="checkbox" >linear: EstimatorTransformer

EstimatorTransformer(estimator=LinearRegression())

<input class="sk-toggleable__control sk-hidden--visually" id="6ceb127c-c870-4944-b6c3-5f47c0b5af1b" type="checkbox" >LinearRegression

LinearRegression()

<input class="sk-toggleable__control sk-hidden--visually" id="8d1aaabb-dda3-4fe0-99dc-c4b60597b332" type="checkbox" >ProbWeightRegression

ProbWeightRegression()

You can see that the `ProbWeightRegression` indeeds sums to one.

```python
grid.best_estimator_[1].coefs_
```

    array([0.03102466, 0.96897535])


# Recitation 10: Column generation and vehicle routing

Today we will spend some time thinking about and implementing column generation, in the particular context of vehicle routing problems. Given time, we will also think about heuristic ways to solve vehicle routing problems.

```julia
using Plots, Random, LinearAlgebra, Printf
```

## 1 - Vehicle routing problems

In an earlier recitation, we talked about the traveling salesman problem (TSP), where the goal is to find a minimum-length tour visiting a set of locations. The TSP has many "cousins", i.e., similar problems with additional complexity to represent practical considerations. One family of such problems are called vehicle routing problems (VRP), and seek to find the optimal set of routes for a *fleet* of vehicles to service a set of customers.

Some examples include:
- Open vehicle routing problem (OVRP): we are given a set of customers that must each be visited exactly once by some vehicle. All routes start from a customer location and must end at a single location, called the "depot". An example of an application of this setting is car-pooling.
- Capacitated vehicle routing problem (CVRP): in this setting, routes must begin and end at the depot, and each customer $i$ is now associated with a demand $d_i$, such that the total demand along any route cannot excced the vehicle capacity $Q$. This is a simplified version of the problem many delivery services face.

## 2 - Solving OVRP using column generation

Let's generate a problem instance, with $N$ locations uniformly sampled from a square (side length $L$)

```julia
using JuMP, Gurobi, LightGraphs

const GRB_ENV = Gurobi.Env()
```

```julia
N = 10
L = 100
```

We'll use the Euclidean metric for distances.

```julia
Random.seed!(1234)
locations = rand(N, 2) .* L
locations = vcat(locations, [L/2 L/2])
dist = [norm(locations[i, :] .- locations[j, :]) for i =1:N+1, j = 1:N+1];
```

Let's plot this example to get a sense of what it looks like!

```julia
plot(size=(500,500), xlim=(0, L), ylim=(0, L))
scatter!(locations[1:N, 1], locations[1:N, 2], label="Customers", legend=:topleft)
scatter!(locations[[N+1], 1], locations[[N+1], 2], label="Depot")
```

Now we need to formulate the problem.

Setup:
- Let $\mathcal{R}$ designate the set of ALL possible routes starting at some customer and ending at the depot.
- Let $c_r$ designate the length of route $r\in\mathcal{R}$.
- Let $\mathcal{I}_r\subseteq[N]$ designate the set of customers visited by route $r\in\mathcal{R}$.
- For any customer $i\in[N]$, we let $\mathcal{R}_i\subseteq\mathcal{R}$ designate the subset of routes that visit customer $i$.

Then we can formulate OVRP as the following set covering problem:

$$\begin{align}
\min\quad &\sum_{r\in\mathcal{R}}c_r y_r\\
\text{s.t.}\quad & \sum_{r\in\mathcal{R}_i}y_r \ge 1 & \forall i \in [N]\\
&y_r\in\{0,1\} & \forall t\in\mathcal{R}
\end{align}$$

Note that we allow customers to be visited multiple times here. However, given a feasible solution that exhibits this behavior, we can always delete the extra visit and lower the total cost, so we won't worry too much about this.

This formulation is so simple! But of course, the complexity is "hidden" in the exponential size of the set $\mathcal{R}$. In fact, we cannot hope to formulate this problem explicitly as written.

However, most routes in the set $\mathcal{R}$ are useless! For example, consider a random route:

```julia
route = vcat(shuffle(1:N)[1:4], N+1)
```

If we plot this, we see that no one in their right mind would recommend this route!

```julia
plot(size=(500,500), xlim=(0, L), ylim=(0, L))
scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
plot!(locations[route, 1], locations[route, 2], label="")
```

So clearly, we can get a good solution (maybe even an optimal one!) to the problem with a restricted set of routes.

Let's consider the linear relaxation of the problem, denoted by (P):

$$\begin{align}
\min\quad &\sum_{r\in\mathcal{R}}c_r y_r\\
\text{s.t.}\quad & \sum_{r\in\mathcal{R}_i}y_r \ge 1 & \forall i \in [N]\\
&y_r\ge 0 & \forall r\in\mathcal{R}
\end{align}$$

Notice that we've eliminated the upper bounds on $y_r$. It doesn't break the validity of the formulation, because from any solution with $y_r>1$, we can construct a feasible solution with $y_r'=1$ which lowers the total cost by $c_r(y_r-1)$.

Now we can write down the dual, denoted by (D):
$$\begin{align}
\max\quad & \sum_{i=1}^N p_i\\
\text{s.t.}\quad & \sum_{i\in\mathcal{I}_r}p_i \le c_r & \forall r\in\mathcal{R}\\
& p_i \ge 0&\forall i\in [N]
\end{align}$$

Assume we solve the main problem with a restricted set of routes  $\mathcal{R}^k\subset\mathcal{R}$, and we get a primal feasible solution $y^k$, and a dual solution $p^k$. The solution $y^k$ is optimal in the full primal if the corresponding dual solution $p^k$ is feasible in the full dual.

For dual feasibility, we require that $\sum_{i\in\mathcal{I}_r}p^k_i\le c_r$ for **every** route $r\in\mathcal{R}$. We can re-write this requirement as $c_r-\sum_{i\in\mathcal{I}_r}p^k_i\ge 0$. In other words, the **reduced cost** of each variable $y_r$ must be nonnegative.

So we propose the following column generation algorithm:
1. Solve (P) over a restricted set of routes $\mathcal{R}^k$, obtain a primal solution $y^k$ and a dual solution $p^k$
2. Find the route $r$ with the smallest reduced cost $\bar{c} = c_r-\sum_{i\in\mathcal{I}_r}p^k_i$
3. If $\bar{c}\ge 0$, terminate. Otherwise add the corresponding $r$ to $\mathcal{R}^k$ (obtaining $\mathcal{R}^{k+1}$) and iterate.

Let's start implementing this algorithm! First, we need an initial set of routes that will produce a feasible primal solution. An easy place to start is to just have each customer in its own route.

```julia
"Create initial set of routes and associated costs"
function initial_routes(distances::Matrix)
    n = size(distances, 1) - 1 # number of customers
    return [[i, n+1] for i in 1:n], distances[1:n, n+1]
end
```

Let's plot these initial routes!

```julia
plot(size=(500,500), xlim=(0, L), ylim=(0, L))
scatter!(locations[1:N, 1], locations[1:N, 2], label="", color="blue", legend=:topleft)
routes, costs = initial_routes(dist)
for route in routes
    plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
scatter!(locations[[N+1], 1], locations[[N+1], 2], label="", color="orange")
```

Second, we need a way to compute the smallest reduced cost, i.e.
$$(\min_{r\in\mathcal{R}}c_r - \sum_{i\in\mathcal{I}_r}p_i^k)$$

This is a kind of **shortest path problem**, where the distance between nodes $i$ and $j$ is the true distance $d_{ij}$, **minus** the dual variable associated with $i$. We can formulate it as follows:

$$\begin{align}
\min\quad & \sum_{i=1}^{N+1}\sum_{j=1}^{N+1}(d_{ij} - p_i^k)x_{ij}\\
\text{s.t.}\quad & \sum_{j=1}^{N+1}x_{ji} + z_i = \sum_{j=1}^{N+1}x_{ij}&\forall i\in[N]\\
& \sum_{j=1}^{N+1}x_{ij}\le 1 & \forall i \in [N]\\
& x_{ii}=0 & \forall i \in [N]\\
& \sum_{j=1}^{N+1}x_{j,N+1} = 1\\
& \sum_{j=1}^{N+1}x_{N+1,j} = 0\\
& \sum_{(i, j) \in C} x_{ij} \le |C| - 1 & \forall C \subseteq [N]\\
& x_{ij}\in\{0,1\}, z_i\in\{0,1\}\\
\end{align}$$

The first constraint is flow conservation, the second limits the flow through each customer to 1, the third removes self-edges, the fourth ensures the route visits the depot, and the fifth ensures the depot is terminal. The sixth constraint eliminates subtours (which by construction do not include the depot).

This subproblem is not that simple! In fact, the formulation sleuths among you will probably notice that if we change the right-hand side of the fourth constraint, we are actually formulating the full OVRP. However, this no longer holds for "harder" vehicle routing problems (e.g. CVRP).

We can implement this formulation below. Note that we need to include a callback to deal with the exponential number of subtour elimination constraints.

```julia
"""
    Given the induced graph as an adjacency list (i.e., next[i] is the next node to visit after node i),
        compute all subtours.
    Return them as a list of lists of nodes in the same component
"""
function find_subtours(next::Vector{Int})
    n = length(next)
    g = DiGraph(n)
    for i = 1:n
        if next[i] != 0
            add_edge!(g, i, next[i])
        end
    end
    components = strongly_connected_components(g)
    return sort(components, by=length)
end

"Find next column to add, returns route, cost, and whether or not the reduced cost is nonnegative"
function new_route(distances::Matrix, dual_variables::Vector)
    n = size(distances, 1) - 1 # number of customers
    @assert length(dual_variables) == n
    push!(dual_variables, 0) # depot
    model = Model(() -> Gurobi.Optimizer(GRB_ENV))
    set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
    @variable(model, x[i=1:(n+1), j=1:(n+1)], Bin)
    @variable(model, z[i=1:n], Bin)
    @constraint(model, flow_conservation[i = 1:n],
                sum(x[j, i] for j = 1:(n+1)) + z[i] == sum(x[i, j] for j = 1:(n+1)))
    @constraint(model, max_flow[i = 1:n],
                sum(x[i, j] for j = 1:(n+1)) <= 1)
    @constraint(model, no_self_flow[i = 1:(n+1)], x[i, i] == 0)
    @constraint(model, into_depot, sum(x[j, n+1] for j = 1:(n+1)) == 1)
    @constraint(model, out_of_depot, sum(x[n+1, j] for j = 1:(n+1)) == 0)
    @objective(model, Min, sum(x[i, j] * (distances[i, j] - dual_variables[i]) for i = 1:(n+1), j=1:(n+1)))

    "Define the callback function"
    function eliminate_subtours(cb_data)
        status = callback_node_status(cb_data, model)
        if status == MOI.CALLBACK_NODE_STATUS_INTEGER
            # get value of current solution
            next = zeros(Int, n + 1)
            # your code here

            subtours = find_subtours(next)
            # solve dual subproblems
            for subtour in subtours
                if length(subtour) == 1 || n + 1 in subtour
                    continue
                else
                    # add subtour elimination cut

                end
            end
        end
    end
    # set callback function and attach to model
    MOI.set(model, MOI.LazyConstraintCallback(), eliminate_subtours)
    optimize!(model)

    route = [argmax(value.(z))]
    while route[end] != n+1
        push!(route, argmax(value.(x[route[end], :])))
    end
    reduced_cost = sum(distances[route[i], route[i+1]] - dual_variables[route[i]] for i = 1:(length(route)-1))
    return route, sum(distances[route[i], route[i+1]] for i = 1:(length(route)-1)), reduced_cost
end
```

Whew! We implemented the subproblem, which was not easy. Now, the main problem should be a piece of cake!

```julia
"Solve OVRP relaxation using column generation"
function ovrp(distances::Matrix; T::Int = 10, verbose::Bool = false)
    n = size(distances, 1) - 1 # number of customers
    routes, costs = initial_routes(dist)
    t = 0
    while true
        t += 1
        model = Model(() -> Gurobi.Optimizer(GRB_ENV))
        set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
        #
        # define restricted main problem

        optimize!(model)
        p = [-JuMP.shadow_price(visit_customer[i]) for i=1:n]
        route, cost, reduced_cost = new_route(distances, p)
        verbose && @printf("Found column with reduced cost: %.2g\n", reduced_cost)
        if reduced_cost > -1e-6 || t > T
            return [routes[r] for r = eachindex(routes) if value(y[r]) >= 0.1],
                   sum(costs[r] for r = eachindex(routes) if value(y[r]) >= 0.1)
        end
        push!(routes, route); push!(costs, cost)
    end
end
```

Now, let's try to solve the problem, and look at the solution we obtain!

```julia
routes, total_cost = ovrp(dist, T=100, verbose = true)
println("Initial cost: ", sum(dist[1:N, N+1]))
println("Final cost: ", total_cost)
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

In a relatively small number of steps, we converge to this solution, which is optimal.

This is great news! Let's try a larger problem.

```julia
N = 20
L = 100
```

```julia
Random.seed!(1234)
locations = rand(N, 2) .* L
locations = vcat(locations, [L/2 L/2])
dist = [norm(locations[i, :] .- locations[j, :]) for i =1:N+1, j = 1:N+1];
plot(size=(500,500), xlim=(0, L), ylim=(0, L))
scatter!(locations[1:N, 1], locations[1:N, 2], label="Customers", legend=:topleft)
scatter!(locations[[N+1], 1], locations[[N+1], 2], label="Depot")
```

Now let's solve it using column generation!

```julia
routes, total_cost = ovrp(dist, T=2, verbose = true)
println("Initial cost: ", sum(dist[1:N, N+1]))
println("Final cost: ", total_cost)
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

It looks like we quickly pivot away from the bad initial solution, but then we get stuck on the single-route solution above, which is clearly suboptimal.

This is frustrating! What is the point of adding all these columns if we just end up picking the route we added at the very beginning?

When we add columns, we are also improving the **lower bound**. So even though we make no primal progress, we are always making dual progress!

Let's modify our OVRP solver to report upper and lower bounds at each iteration.

- **Upper bound:** this part is easy, we just report the objective of our current feasible solution.
- **Lower bound:** this part is trickier. Here, we need to use the fact that in the optimal solution, we will have no more than $N$ routes, so $\sum_{r\in\mathcal{R}}y^*_r \le N$. Then, given a feasible solution with objective $\hat{c}$ and the smallest reduced cost $\bar{c}$, the most improvement we can get on $\hat{c}$ is $$\hat{c} + \sum_{r\in\mathcal{R}}\bar{c}y^*_r \ge \hat{c} + \bar{c}N$$

(remember $\bar{c}$ is negative!)

```julia
"Solve OVRP relaxation using column generation"
function ovrp_progress(distances::Matrix; T::Int = 10, verbose::Bool = false)
    n = size(distances, 1) - 1 # number of customers
    routes, costs = initial_routes(dist)
    t = 0
    upper_bounds = []
    lower_bounds = []
    while true
        t += 1
        model = Model(() -> Gurobi.Optimizer(GRB_ENV))
        set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
        @variable(model, y[r = eachindex(routes)] >= 0)
        @constraint(model, visit_customer[i=1:n],
                    sum(y[r] for r = eachindex(routes) if i in routes[r]) >= 1)
        @objective(model, Min, sum(costs[r] * y[r] for r = eachindex(routes)))
        optimize!(model)
        #
        # save upper bound

        p = [-JuMP.shadow_price(visit_customer[i]) for i=1:n]
        route, cost, reduced_cost = new_route(distances, p)
        verbose && @printf("Found column with reduced cost: %.2g\n", reduced_cost)
        #
        # save lower bound

        if reduced_cost > -1e-6 || t > T
            return [routes[r] for r = eachindex(routes) if value(y[r]) >= 0.1], objective_value(model),
                   upper_bounds, lower_bounds
        end
        push!(routes, route); push!(costs, cost)
    end
end
```

Now, let's try again on our 20-city instance!

```julia
routes, total_cost, upper_bounds, lower_bounds = ovrp_progress(dist, T=10, verbose = false)
plot(0:(length(upper_bounds) - 1), [upper_bounds, lower_bounds], legend=:bottomright, label=["Upper" "Lower"])
```

Notice that unlike Benders decomposition, there are no monotonic guarantees on the lower bound, but there is a monotonic guarantee on the upper bound. Convergence is not great!

## 3 - Solving CVRP using column generation

As we mentioned earlier, OVRP is kind of "easy" and using column generation is probably overkill. But we'll see that we can get a much bigger edge on the harder problem of CVRP.

We're going to consider a super-simple version of CVRP, where each customer has a demand of 1, and the capacity of the vehicle is $Q\ge 1$. Our initial routes remain feasible.

All we need to do is modify the subproblem so that it only finds feasible routes, i.e. routes with less than $Q$ customers, or equivalently, less than $Q$ edges.

```julia
"Find next column to add, returns route, cost, and whether or not the reduced cost is nonnegative"
function new_route_cap(distances::Matrix, dual_variables::Vector, Q::Int=length(dual_variables))
    n = size(distances, 1) - 1 # number of customers
    @assert length(dual_variables) == n
    push!(dual_variables, 0) # depot
    model = Model(() -> Gurobi.Optimizer(GRB_ENV))
    set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
    @variable(model, x[i=1:(n+1), j=1:(n+1)], Bin)
    @variable(model, z[i=1:n], Bin)
    @constraint(model, flow_conservation[i = 1:n],
                sum(x[j, i] for j = 1:(n+1)) + z[i] == sum(x[i, j] for j = 1:(n+1)))
    @constraint(model, max_flow[i = 1:n],
                sum(x[i, j] for j = 1:(n+1)) <= 1)
    @constraint(model, no_self_flow[i = 1:(n+1)], x[i, i] == 0)
    @constraint(model, into_depot, sum(x[j, n+1] for j = 1:(n+1)) == 1)
    @constraint(model, out_of_depot, sum(x[n+1, j] for j = 1:(n+1)) == 0)
    @objective(model, Min, sum(x[i, j] * (distances[i, j] - dual_variables[i]) for i = 1:(n+1), j=1:(n+1)))

    # CAPACITY CONSTRAINT

    "Define the callback function"
    function eliminate_subtours(cb_data)
        status = callback_node_status(cb_data, model)
        if status == MOI.CALLBACK_NODE_STATUS_INTEGER
            # get value of current solution
            next = zeros(Int, n + 1)
            for i = 1:(n+1), j=1:(n+1)
                if callback_value(cb_data, x[i, j]) > 0.5
                    next[i] = j
                end
            end
            subtours = find_subtours(next)
            # solve dual subproblems
            for subtour in subtours
                if length(subtour) == 1 || n + 1 in subtour
                    continue
                else
                    cut = @build_constraint(sum(x[subtour[i], subtour[i+1]] for i = 1:(length(subtour) - 1)) +
                                            x[subtour[length(subtour)], subtour[1]] <= length(subtour) - 1)
                    MOI.submit(model, MOI.LazyConstraint(cb_data), cut)
                end
            end
        end
    end
    # set callback function and attach to model
    MOI.set(model, MOI.LazyConstraintCallback(), eliminate_subtours)
    optimize!(model)

    next = zeros(Int, n + 1)
    for i = 1:(n+1), j=1:(n+1)
        if value(x[i, j]) > 0.5
            next[i] = j
        end
    end

    route = [argmax(value.(z))]
    while route[end] != n+1
        push!(route, argmax(value.(x[route[end], :])))
    end
    reduced_cost = sum(distances[route[i], route[i+1]] - dual_variables[route[i]] for i = 1:(length(route)-1))
    return route, sum(distances[route[i], route[i+1]] for i = 1:(length(route)-1)), reduced_cost
end
```

And we correspondingly change the main problem:

```julia
"Solve CVRP relaxation using column generation"
function cvrp_progress(distances::Matrix, Q::Int; T::Int = 10, verbose::Bool = false)
    n = size(distances, 1) - 1 # number of customers
    routes, costs = initial_routes(dist)
    t = 0
    upper_bounds = []
    lower_bounds = []
    while true
        t += 1
        model = Model(() -> Gurobi.Optimizer(GRB_ENV))
        set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
        @variable(model, y[r = eachindex(routes)] >= 0)
        @constraint(model, visit_customer[i=1:n],
                    sum(y[r] for r = eachindex(routes) if i in routes[r]) >= 1)
        @objective(model, Min, sum(costs[r] * y[r] for r = eachindex(routes)))
        optimize!(model)
        push!(upper_bounds, objective_value(model))
        p = [-JuMP.shadow_price(visit_customer[i]) for i=1:n]
        route, cost, reduced_cost = new_route_cap(distances, p, Q)
        verbose && @printf("Found column with reduced cost: %.2g\n", reduced_cost)
        push!(lower_bounds, objective_value(model) + n * reduced_cost)
        if reduced_cost > -1e-6 || t > T
            return [routes[r] for r = eachindex(routes) if value(y[r]) >= 0.1], objective_value(model),
                   upper_bounds, lower_bounds
        end
        push!(routes, route); push!(costs, cost)
    end
end
```

Let's try it on our 20-city example.

```julia
Q = 4
@time routes, total_cost, upper, lower = cvrp_progress(dist, Q, T=100, verbose = true)
println("Initial cost: ", sum(dist[1:N, N+1]))
println("Final cost: ", total_cost)
```

Let's look at the progress curve:

```julia
plot(0:(length(upper) - 1), [upper, lower], legend=:bottomright, label=["Upper" "Lower"])
```

And let's observe the solution

```julia
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

This solution makes sense, but we do observe some funky routes. Recall that all this time, we have been solving the LP relaxation! So we are paying the price of keeping all routes with a nonzero (but possibly fractional) selection variable $y_r$.

## 4 - Restoring integrality

That was all very nice, but we still don't have an integral solution! What should we do about that?

Two approaches:
- integrate the pricing subproblem into the branch and bound tree. This is called "branch and price", it is the state of the art, but also hard to implement. We will discuss in lecture next week.
- use heuristics!

The simplest heuristic we can use is "price and branch". We simply keep adding columns to the relaxation until we no longer can, then we solve an integer program over the columns we've added.

We note that this heuristic will produce a feasible solution because of the initial routes we've chosen, but in general, it may not find a feasible solution!

Let's implement it for CVRP:

```julia
"Solve OVRP relaxation using column generation"
function cvrp_progress_integral(distances::Matrix, Q::Int; T::Int = 10, verbose::Bool = false)
    n = size(distances, 1) - 1 # number of customers
    routes, costs = initial_routes(dist)
    t = 0
    upper_bounds = []
    lower_bounds = []
    while true
        t += 1
        model = Model(() -> Gurobi.Optimizer(GRB_ENV))
        set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
        @variable(model, y[r = eachindex(routes)] >= 0)
        @constraint(model, visit_customer[i=1:n],
                    sum(y[r] for r = eachindex(routes) if i in routes[r]) >= 1)
        @objective(model, Min, sum(costs[r] * y[r] for r = eachindex(routes)))
        optimize!(model)
        push!(upper_bounds, objective_value(model))
        p = [-JuMP.shadow_price(visit_customer[i]) for i=1:n]
        route, cost, reduced_cost = new_route_cap(distances, p, Q)
        verbose && @printf("Found column with reduced cost: %.2g\n", reduced_cost)
        push!(lower_bounds, objective_value(model) + n * reduced_cost)
        if reduced_cost > -1e-6 || t > T
            model = Model(() -> Gurobi.Optimizer(GRB_ENV))
            set_optimizer_attributes(model, "TimeLimit" => 10, "OutputFlag" => 0)
            #
            # define new model with integer variables

            optimize!(model)
            return [routes[r] for r = eachindex(routes) if value(y[r]) >= 0.1], objective_value(model),
                   upper_bounds, lower_bounds
        end
        push!(routes, route); push!(costs, cost)
    end
end
```

```julia
Q = 4
@time routes, total_cost, upper, lower = cvrp_progress_integral(dist, Q, T=100, verbose = true)
println("Initial cost: ", sum(dist[1:N, N+1]))
println("Final cost: ", total_cost)
println("Gap: ", (total_cost - lower[end]) / lower[end])
```

We see that we terminated with a tiny gap, so this heuristic worked really well! Let's look at it plotted below:

```julia
plot(0:(length(upper) - 1), [upper, lower], legend=:bottomright, label=["Upper" "Lower"])
scatter!([length(upper) - 1], [total_cost], label="Integer solution")
```

And we can take a look at the solution:

```julia
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

## 5 - Large-scale neighborhood search

In the last part of this recitation, we'll talk a little more about heuristics, since this is often a great way to solve practical problems.

The one heuristic I want to talk about is large-scale neighborhood search (LNS). It has a fancy name, but it's really the simplest idea in the world:

> My MIP doesn't solve? OK, I'll just fix most of the variables. Then I'll keep changing a few at a time until I converge.

Let's apply this idea to the CVRP. Recall that its cousin the TSP has a simple formulation with a polynomial number of constraints, which doesn't scale very well. Let's write down the standard MTZ formulation for CVRP:

```julia
function cvrp_mtz(distances::Matrix, Q::Int; verbose::Bool=true)
    n = size(distances, 1) - 1 # number of customers
    model = Model(() -> Gurobi.Optimizer(GRB_ENV))
    set_optimizer_attributes(model, "TimeLimit" => 5, "OutputFlag" => ifelse(verbose, 1, 0))
    @variable(model, x[1:(n+1), 1:(n+1)], Bin)
    # Minimize total distance, but don't count distance leaving the depot
    @objective(model, Min,
               sum(dist[i, j] * x[i, j] for i = 1:n, j = 1:(n+1)))
    @constraint(model, one_successor[i=1:n],
                sum(x[i, j] for j=1:(n+1)) == 1)
    @constraint(model, flow_conservation[i=1:(n+1)],
                sum(x[i, j] for j=1:(n+1)) == sum(x[j, i] for j=1:(n+1)))
    @constraint(model, no_self_edges[i=1:(n+1)],
                x[i, i] == 0)
    # variable designates the number of customers that have been visited before this one
    @variable(model, 0 <= u[i=1:n] <= Q)
    @constraint(model, [i=1:n, j=1:n; j != i], u[j] >= u[i] + 1 - (Q) * (1 - x[i, j]))
    #
    # warm start the solution with our initial routes, as before

    optimize!(model)
    x_val = value.(x)
    routes = []
    while sum(x_val) > 1e-6
        current = findfirst(a -> a > 0.5, x_val[n+1, :])
        x_val[n+1, current] = 0.0
        route = []
        while true
            push!(route, current)
            current == n+1 && break
            next = findfirst(a -> a > 0.5, x_val[current, :])
            x_val[current, next] = 0.0
            current = next
        end
        push!(routes, route)
    end
    return routes, objective_value(model)
end
```

Let's solve our 20-customer instance.

```julia
routes, cost = cvrp_mtz(dist, 4)
println("Total cost: ", cost)
```

Looks like we did pretty well! Let's visualize the routes.

```julia
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

This matches our price-and-branch solution from earlier. Can we scale up?

```julia
N = 40

Random.seed!(1234)
locations = rand(N, 2) .* L
locations = vcat(locations, [L/2 L/2])
dist = [norm(locations[i, :] .- locations[j, :]) for i =1:N+1, j = 1:N+1];
plot(size=(500,500), xlim=(0, L), ylim=(0, L))
scatter!(locations[1:N, 1], locations[1:N, 2], label="Customers", legend=:topleft)
scatter!(locations[[N+1], 1], locations[[N+1], 2], label="Depot")
```

```julia
routes, total_cost = cvrp_mtz(dist, 4)
println("Total cost: ", total_cost)
```

Let's visualize:

```julia
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

This is visibly suboptimal (for example, see the left quadrant) and the gap is 30%.

We would like to "tweak" the solution by "destroying" some of the edges, then "repairing" the solution, hopefully with lower cost!

We can destroy the routes by removing $k$ edges, as follows:

```julia
function split_routes(route_list::Vector, k::Int)
    num_edges = [length(r) - 1 for r in route_list]
    total_edges = sum(num_edges)
    cum_edges = cumsum(num_edges)
    splits = sort(shuffle(1:total_edges)[1:k])
    routes_to_split = [findfirst(x -> x >= s, cum_edges) for s in splits]
    places_to_split = [splits[i] - (routes_to_split[i] == 1 ? 0 : cum_edges[routes_to_split[i] - 1])
                       for i = eachindex(splits)]
    new_routes = []
    for (r, route) in enumerate(routes)
        if r in routes_to_split
            where_to_split = [places_to_split[i] for i = eachindex(places_to_split) if routes_to_split[i] == r]
            start = 1
            for j in where_to_split
                push!(new_routes, route[start:j])
                start = j + 1
            end
            push!(new_routes, route[start:end])
        else
            push!(new_routes, route)
        end
    end
    return new_routes
end
```

For example, we can remove 7 edges.

```julia
Random.seed!(1234)
fragments = split_routes(routes, 7)
```

The "fragments" look like this:

```julia
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in fragments
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

We can then tweak our CVRP formulation to keep fragments intact and simply connect them optimally.

```julia
function repair_cvrp_mtz(distances::Matrix, Q::Int, fragments::Vector; verbose::Bool=true)
    n = size(distances, 1) - 1 # number of customers
    model = Model(() -> Gurobi.Optimizer(GRB_ENV))
    set_optimizer_attributes(model, "TimeLimit" => 5, "OutputFlag" => ifelse(verbose, 1, 0))
    @variable(model, x[1:(n+1), 1:(n+1)], Bin)
    # Minimize total distance, but don't count distance leaving the depot
    @objective(model, Min,
               sum(dist[i, j] * x[i, j] for i = 1:n, j = 1:(n+1)))
    @constraint(model, one_successor[i=1:n],
                sum(x[i, j] for j=1:(n+1)) == 1)
    @constraint(model, flow_conservation[i=1:(n+1)],
                sum(x[i, j] for j=1:(n+1)) == sum(x[j, i] for j=1:(n+1)))
    @constraint(model, no_self_edges[i=1:(n+1)],
                x[i, i] == 0)
    # variable designates the number of customers that have been visited before this one
    @variable(model, 0 <= u[i=1:n] <= Q)
    @constraint(model, [i=1:n, j=1:n; j != i], u[j] >= u[i] + 1 - (Q) * (1 - x[i, j]))

    #
    # fix our route fragments - your code here

    optimize!(model)
    x_val = value.(x)
    routes = []
    while sum(x_val) > 1e-6
        current = findfirst(a -> a > 0.5, x_val[n+1, :])
        x_val[n+1, current] = 0.0
        route = []
        while true
            push!(route, current)
            current == n+1 && break
            next = findfirst(a -> a > 0.5, x_val[current, :])
            x_val[current, next] = 0.0
            current = next
        end
        push!(routes, route)
    end
    return routes, objective_value(model)
end
```

Let's try to repair our fragments...

```julia
routes, cost = repair_cvrp_mtz(dist, 4, fragments)
println("Total cost: ", cost)
```

```julia
p = plot(size=(500,500), xlim=(0, L), ylim=(0, L))
p = scatter!(locations[1:N, 1], locations[1:N, 2], label="", legend=:topleft)
for route in routes
    p = plot!(locations[route, 1], locations[route, 2], label="", color="black")
end
p = scatter!(locations[[N+1], 1], locations[[N+1], 2], label="")
```

Looks like nothing changed, but we can do this iteratively!

```julia
Q = 4
routes, total_cost = cvrp_mtz(dist, Q, verbose=false)
println("Cost: ", total_cost)
Random.seed!(1234)
for t = 1:20
    fragments = split_routes(routes, 10)
    routes, total_cost = repair_cvrp_mtz(dist, Q, fragments, verbose=false)
    println("Cost: ", total_cost)
end
```

There is a tradeoff here: the more edges we remove, the larger the neighborhood we have to explore, and the more we will gain from an iteration, but the harder it becomes to optimize over the neighborhood.


# Continuous Signals

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Elementary Operations

Operations like superposition, temporal shifting and scaling are used to construct signals with a more complex structure than the previously introduced [standard signals](standard_signals.ipynb). A set of elementary operations is introduced that are frequently used in signal processing.

### Superposition

The weighted superposition $x(t)$ of two signals $x_1(t)$ and $x_2(t)$ is given as

\begin{equation}
x(t) = A \cdot x_1(t) + B \cdot x_2(t)
\end{equation}

with the complex weights $A, B \in \mathbb{C}$.

**Example**

The following example illustrates the superposition of two harmonic signals $x(t) = A \cdot \cos(\omega_1 t) + B \cdot \cos(\omega_2 t)$ with weights $A$, $B$ and angular frequencies $\omega_1$ and $\omega_2$.

```python
%matplotlib inline
import sympy as sym
sym.init_printing()

t = sym.symbols('t', real=True)

A = .3
om1 = 3
B = .5
om2 = 5

x = A*sym.cos(om1*t) + B*sym.cos(om2*t)

sym.plot(x, (t, -5, 5), ylim=[-1.2, 1.2], ylabel=r'$x(t)$');
```

### Temporal Shift

The temporal shift of a signal $s(t)$ by the time $\tau$ is a frequently applied operation in signal processing. For instance, to model the propagation of signals from an actuator to a sensor. The temporally shifted signal $x(t)$ is defined as

\begin{equation}
x(t) = s(t-\tau)
\end{equation}

with $\tau \in \mathbb{R}$. The signal $s(t)$ is

* shifted to the right (*delayed*) for $\tau > 0$
* shifted to the left for $\tau < 0$

**Example**

In order to illustrate the temporal shifting of signals, the construction of a staircase signal by a superposition of shifted [rectangular signals](standard_signals.ipynb#Rectangular-Signal) is considered

\begin{equation}
x(t) = \text{rect}\left(t - \frac{2}{3} \right) + \frac{2}{3} \cdot \text{rect}\left(t-\frac{3}{2} \right) + \frac{1}{3} \cdot \text{rect} \left(t-\frac{5}{2} \right)
\end{equation}

```python
rect = sym.Heaviside(t + 1/2) - sym.Heaviside(t - 1/2)
x = rect.subs(t, t-1/2) + 2/3*rect.subs(t, t-3/2) + 1/3*rect.subs(t, t-5/2)

sym.plot(x, (t, -1, 5), ylim=[-0.2, 1.2], ylabel='$x(t)$');
```

**Exercise**

* Add another step to the beginning of the staircase signal by modifying above example.

### Temporal Scaling

The temporal scaling of a signal $s(t)$ is defined as

\begin{equation}
x(t) = s(a \cdot t)
\end{equation}

with $a \in \mathbb{R}$. The signal $s(t)$ is

* stretched for $0 < a < 1$
* compressed for $a > 1$
* time-reversed and scaled for $a < 0$

An application of temporal scaling in signal processing is the modeling of the [Doppler effect](https://en.wikipedia.org/wiki/Doppler_effect).

**Example**

The following example illustrates the scaling of the staircase signal $y(t) = x(a \cdot t)$ introduced in the previous example.

```python
a = .5
y = x.subs(t, a*t)

sym.plot(y.subs(a, 1/2), (t, -3, 7), ylim=[-0.2, 1.2], ylabel=r'$y(t)$');
```

**Exercise**

* Modify above example such that the signal is compressed.
* Modify above example such that the signal is scaled and time reversed. What scaling factor `a` leads to a stretching/compression in this context?

### Temporal Flipping

The temporal flipping of a signal $s(t)$ is defined as

\begin{equation}
x(t) = s(\tau - t)
\end{equation}

for $\tau \in \mathbb{R}$. As $s(\tau - t) = s(- (t - \tau))$ the flipping operation can also be represented as a time-reversal of the signal $s(t)$ followed by a shift of $\tau$ of the reversed signal. For $\tau = 0$ this results in only a time-reversal of the signal.

The temporal flipping operation can interpreted geometrically as a mirroring of the signal $s(t)$ at the vertical axis $t=\frac{\tau}{2}$.

**Example**

The following example illustrates the temporal flipping of the staircase signal $x(t)$ introduced before.

```python
tau = 2
y = x.subs(t, tau - t)

sym.plot(y,(t, -5, 5), ylim=[-0.2, 1.2], ylabel=r'$s(\tau-t)$');
```

**Excercise**

* For what value $\tau$ does the signal start at $t=0$?
* Realize the temporal flipping by splitting it into two operations: (i) time-reversal and (ii) temporal shift.

**Copyright**

The notebook is provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebook for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Signals and Systems* by Sascha Spors.


```python
from math import log as ln

import numpy as np
import matplotlib.pyplot as plt

from chempy.units import default_constants as consts, default_units as u
from chempy.properties.water_density_tanaka_2001 import water_density
from chempy.properties.water_permittivity_bradley_pitzer_1979 import water_permittivity
import chempy.electrolytes as dh
import chempy.symbolic
```

```python
def DH_A_B(T):
    eps_r = water_permittivity(T, 1*u.bar, units=u)
    rho = water_density(T, units=u)
    b0 = 1 * u.mol/u.kg
    A = dh.A(eps_r, T, rho, b0=b0, constants=consts, units=u).simplified/ln(10)
    B = dh.B(eps_r, T, rho, b0=b0, constants=consts, units=u).rescale(1/u.angstrom)
    return A, B
```

```python
DH_A_B(298.15*u.kelvin)
```

```python
T = (np.linspace(0, 40)+273.15)*u.Kelvin
A, B = DH_A_B(T)
%matplotlib inline
plt.figure(figsize=(18,4))
plt.subplot(1, 2, 1)
plt.plot(T-273.15*u.K, A)
plt.xlabel(r"Temperature / $^\circ C$")
plt.ylabel("A")
plt.subplot(1, 2, 2)
plt.plot(T-273.15*u.K, B)
plt.xlabel(r"Temperature / $^\circ C$")
plt.ylabel("B / $\AA^{-1}$")
```

```python
import sympy as sp
sp.init_printing()
```

```python
constants = chempy.symbolic.get_constant_symbols()
constants.Avogadro_constant
```

```python
var_symbs = A, B, eps_r, T, rho, b0, I, z, a, I0, C, gamma = sp.symbols('A B epsilon_r T rho b^o I z a I^o C gamma')
one = sp.S(1)
var_symbs
```

```python
A_expr = dh.A(eps_r, T, rho, b0, constants, backend='sympy')
sp.Eq(A, A_expr)
```

```python
B_expr = dh.B(eps_r, T, rho, b0, constants, backend=sp)
sp.Eq(B, B_expr)
```

```python
sp.Eq(sp.log(gamma), dh.limiting_log_gamma(I, z, A, I0=I0, backend=sp))
```

```python
sp.Eq(sp.log(gamma), dh.extended_log_gamma(I, z, a, A, B, C=C, I0=I0, backend=sp))
```


# Introduction to the Finite Element Method (FEM): Nodal and Modal Interpolations

#### *Marco Lúcio Bittencourt* (mlb@fem.unicamp.br), *Cláudio Alessandro de Carvalho Silva* (cacs@thorus-scisoft.com.br) and *Alberto Costa Nogueira Junior* (albercn@br.ibm.com)

The basic concepts about the Finite Element Method presented in the previous class will now be generalized regarding interpolation functions (the very heart of FEM) and practical aspects of numerical integration.

## Normalized Local Coordinate Systems

In the FEM code presented in the previous class, the $0\leq x \leq L$ domain representing the bar length was discretized in a number of finite elements with the same length $h_e$. Off course, that was a simplification. In fact, the most common discretization involves elements of diferent sizes and - in higher geometric dimensions - different shapes. If all that had to be taken into account in the original problem domain, we would greatly reduce the flexibility of FEM for dealing with complicated domain geometries.

The basic technique employed to generalize the FEM is the use of normalized local coordinate systems to define the elements and perform most of the computations associated to each element (such as the computation of stiffness matrices and load vectors) instead of using the original global coordinate system in which the problem is defined. If suitable local/global mappings are defined, all operations can then be executed transparently.

The advantages of using normalized local coordinate systems are:
- Fixed integration limits for the computation of element matrices and load vectors.
- Simpler definition of polynomial interpolation functions.
- Use of distorted elements in FEM meshes.

The next figure shows the relationship between a normalized local coordinate system $-1\leq \xi_1 \leq 1$ and the subdomain $x_1 \leq x\leq x_2$ of a 1-D element of length $L$ defined in the global non-normalized system.

Normalized and non-normalized coordinate systems.

Assuming a linear mapping for $x(\xi_1)$ such as
\begin{equation}
x(\xi_1) = a\xi_1+b
\end{equation}
the coefficients $a$ and $b$ are determined by
\begin{equation}
\left. \begin{array}{l} x(-1) = a(-1)+b=x_1 \\ x(1) = a(1)+b=x_2 \end{array}\right\}
\rightarrow \begin{array}{l}  a=\frac 12(x_2-x_1)=\frac L2 \\ b=\frac 12(x_1+x_2) \end{array}
\end{equation}
and the inverse mapping is
\begin{equation}
\xi_1(x) = \frac{2x(\xi_1) - (x_1+x_2)}{x_2-x_1}
\end{equation}
*Natural coordinates* $L_1$ and $L_2$ ($0\leq L_i\leq 1$) can be also used to define a local coordinate system:

Natural coordinate system.

\begin{equation}
l_1+l_2 = l \;\;\rightarrow\;\; \frac{l_1}{l}+\frac{l_2}{l} =1 \;\;\rightarrow\;\;
L_1+L_2=1
\end{equation}

The previous local coordinate systems are related as
\begin{equation}
\xi_1(L_1) = 2L_1-1 \qquad\qquad L_1(\xi_1) = \frac{1}{2}(1+\xi_1)
\end{equation}

Mappings between normalized systems.

## 1-D Lagrange Polynomials

"For a given set of distinct points $x_j$ and numbers $y_j$, the *Lagrange polynomial* is the polynomial of the least degree that at each point $x_j$ assumes the corresponding value $y_j$, *i.e.* the functions coincide at each point." (*Wikipedia*)

The following expression defines a Lagrange polynomial $l_{p}^{(P_1)}(\xi_1)$ of order $P_1$ in the local system $\xi_1$:
\begin{equation}
l_{p}^{(P_1)}(\xi_1) =  \displaystyle\prod_{q=0(p\neq
q)}^{P_1}\frac{(\xi_1 -\xi_{1_q})} {(\xi_{1_p}-\xi_{1_q})}
\end{equation}

Lagrange polynomials of order $P_1$ can also be defined in direction $L_1$ of the natural coordinate system:
\begin{equation}
l_p^{(P_1)}(L_1) =  \prod_{q=0, p\neq q}^{P_1}\frac{(L_{1}-L_{1_q})}
{\displaystyle(L_{1_p}-L_{1_q})}
\end{equation}

When the Lagrange polynomials are used for interpolation in FEM, **one interpolation function is associated to each of the element's nodes.** Therefore, Lagrange polynomials of order $P_1$ are required for the interpolation on elements with $P_1+ 1$ nodes.

Mappings Nodes in the $\xi_1$ system.

The Python code below computes and plots 1-D Lagrange polynomials of fourth order. Most of the code is parametrized. You can easily compute and visualize polynomials of different orders.

```python
import numpy
import math #usefull functions: math.factorial(x), math.floor(), math.ceil()
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```python
NNodes = 5        # Number of nodes
degree = NNodes-1 # Degree of 1-D nodal functions

# Coordinates of nodes in $\xi$ system
xi_coords = numpy.linspace(-1.0, 1.0, NNodes)

# One term of a Lagrange polynomial
def l(xi, xi_p, xi_q):
    return (xi-xi_q)/(xi_p-xi_q)

# pth Lagrange polynomial of degree dg in the &\xi$ system
def lp(xi, p, dg):
    q = numpy.array(range(1,dg+1))
    for j in range(0, p):
        q[ j ] = j
    prod = 1.0
    for i in range(0,dg):
        prod = prod * l(xi,xi_coords[p],xi_coords[q[i]])
    return prod

# Array of values for plotting Lagrange polynomials of degree four
x   = numpy.linspace(-1.0, 1.0, 50)
pl  = numpy.array(range(0, NNodes))
lag = numpy.empty_like(pl, dtype=numpy.ndarray)

# Computes Lagrange polynomials for NNodes
for i in range(0, NNodes):
    lag[ i ] = lp( x, i, degree )

pyplot.figure(figsize=(8,3))
pyplot.grid(True)
pyplot.xlabel(r'$\xi$', fontsize=18)
pyplot.ylabel(r'$l_p$', fontsize=18)
pyplot.tick_params(axis='both', labelsize=12)
pyplot.plot(x,lag[0], 'r-', lw=2, label='$l_0$')
pyplot.plot(x,lag[1], 'b-', lw=2, label='$l_1$')
pyplot.plot(x,lag[2], 'g-', lw=2, label='$l_2$')
pyplot.plot(x,lag[3], 'y-', lw=2, label='$l_3$')
pyplot.plot(x,lag[4], 'm-', lw=2, label='$l_4$')
```

In the figure above, note the **colocation property**: $l_{p}^{(P_1)}(\xi_{1_q}) =\delta_{pq}$, that is, each Lagrange function is a *Kronecker delta* relative to the nodes. Interpolation functions with the colocation property relative to the nodes of an element are called **nodal interpolation functions**.

## 1-D Nodal Interpolation Functions

Using the concept of Lagrange polynomials, 1-D nodal interpolation functions for the finite element method can be defined either in the system $\xi_1$ or in the system $L_1$ as:

Local coordinate system $\xi_1$
\begin{equation}
N_p(\xi_1)=\phi_p(\xi_1)=\left\{
\begin{array}{lll}
 \frac{1}{2}(1-\xi_1)L_{p,P_1}^{(P_{1}-1)}(\xi_1) & p=0 \\
 \frac{1}{2}(1+\xi_1)L_{p,0}^{(P_{1}-1)}(\xi_1) & p=P_1 \\
 \frac{1}{4}(1-\xi_1)(1+\xi_1)L_p^{(P_{1}-2)}(\xi_1) & 0 < p < P_{1}
\end{array}\right.
\end{equation}

Local coordinate system $L_1$
\begin{equation}
 N_p(L_1)=\phi_p(L_1)=\left\{
\begin{array}{lll}
 (1-L_1)L_p^{(P_1-1)}(L_1) & p=0 \\
 L_1L_p^{(P_1-1)}(L_1) & p= P_1 \\
(1-L_1)L_1L_p^{(P_1-2)}(L_1) & 0 < p < P_1
\end{array}\right.
\end{equation}

where
\begin{equation}
L_{p,l}^{P_1-1}(\xi_{1}) = -\frac{\prod_{q=0,q\neq \{p,l\}}^{P_1}(\xi_{1} - {\xi_{1}}_{q})}{\prod_{q=0,q\neq \{p,l\}}^{P_1}({\xi_{1}}_{p} - {\xi_{1}}_{q})}\qquad\qquad
%
L_{p}^{P_1-2}(\xi_{1})=-4\frac{\prod_{q=1,q\neq p}^{P_1-1}(\xi_{1} - {\xi_{1}}_{q})}{\prod_{q=1,q\neq p}^{P_1-1}({\xi_{1}}_{p} - {\xi_{1}}_{q})}
\end{equation}

The stiffness and mass matrices for the bar in the local system $\xi_1$ are
\begin{equation}
[M_e] = \int_{-1}^{1} [N_e]^T[N_e]Ad\xi_1\qquad\qquad
[K_e] = \int_{-1}^{1} [B_e]^T[D][B_e]Ad\xi_1
\end{equation}

Those matrices have the following sparsity profiles for $P=4$:

Mass Matrix

Stiffness Matrix

Note that both matrices are full. In fact, no orthoganilty property was required when defining the 1-D nodal interpolation funcions.

## Some comments on polynomial expasion approximation

In the previous class, it was shown that the FEM employs a polynomial expansion of compact support functions to approximate the solution of the weak formulation. The better the characteristics of that polynomial expansion, the better the approximation.

On way to increase the quality of the approximation is, while keeping the same local interpolation, to increase the number of elements (*h*-refinement). Students are invited to test this property in the FEM code provided previously. Another way is to increase the order of the terms in the polynomial expansion. As seen in the previous sections, one can use higher order **nodal** interpolation functions provided each element had the required number of nodes ($P_1$ order requires elements with $P_1+1$ nodes).

However, higher order functions can be **nodal** or **modal**. The nodal interpolation has the colocation property in such way that each function is one on one node and zero on the others. This leads to a direct physical interpretation of the interpolation coefficients. However, increasing the interpolation order demands increasing the number of physical nodes, which limits the aplicability of the method, specially on higher geometric dimensions.

**Modal** interpolations does not have colocation property and then there may be no physical interpretation of the interpolation coefficients. There is no requirement of increasing the number of nodes to increase the interpolation order. This allows greater flexibility of having different interpolation orders on the same mesh.

## 1-D Jacobi Polynomials

*Jacobi polynomials* belong to the class of orthogonal polynomials.

The *Jacobi polynomials* of order $p$ with weights $\alpha,\beta$ in the variable $-1\leq \xi_1\leq 1$ are denoted $P_p^{\alpha,\beta}(\xi_1)$. They satisfy the following **orthogonality condition** in the $\xi_1$-coordinate system
\begin{equation}
\int_{-1}^{1} (1-\xi_1)^\alpha (1+\xi_1)^\beta P_n^{\alpha,\beta}(\xi_1) P_m^{\alpha,\beta}(\xi_1)d\xi_1 = C\delta_{mn}
\end{equation}
or in the $L_1$-coordinate system
\begin{equation}
\int_{0}^{1} (1-L_1)^\alpha L_1^\beta P_n^{\alpha,\beta}(2L_1-1) P_m^{\alpha,\beta}(2L_1-1)dL_1 = D\delta_{mn}
\end{equation}

*Legendre* and *Chebyshev* polynomials are special cases of Jacobi polynomials according to specific values of weights:
- Legendre polynomials: $\alpha=\beta=0$
- Chebyshev polynomials: $\alpha=\beta=-\frac12$

Jacobi polynomials are generated by the following recurrence relation:
\begin{equation}\begin{array}{rll}
{P}_{0}^{\alpha, \beta}(\xi_{1}) &=& 1\\
{P}_{1}^{\alpha, \beta}(\xi_{1}) &=& \frac{1}{2}[\alpha-\beta+(\alpha+\beta+2)\xi_{1}]\\
a_{p}^{1}{P}_{p+1}^{\alpha,\beta}(\xi_{1})&=& (a_{p}^{2}+a_{p}^{3}\xi_{1}){P}_{p}^{\alpha, \beta}(\xi_{1})-a_{p}^{4}{P}_{p-1}^{\alpha, \beta}(\xi_{1})\\
a_{p}^{1}&=&2(p+1)(p+\alpha+\beta+1)(2p+\alpha+\beta)\\
a_{p}^{2}&=&(2p+\alpha+\beta+1)(\alpha^{2}-\beta^{2})\\
a_{p}^{3}&=&(2p+\alpha+\beta)(2p+\alpha+\beta+1)(2p+\alpha+\beta+2)\\
a_{p}^{4}&=&2(p+\alpha)(p+\beta)(2p+\alpha+\beta+2)
\end{array}
\end{equation}

That recurrence relation is implemented by the Python code below.

```python
def JacobiP(r,m,alpha,beta):
# Pm, Pn -- mth-order Jacobi polynomial with weights alpha and beta evaluated at point r
# Pn0, Pn1, Pn2 -- Jacobi polynomials of order n-1, n and n+1
# a1n, a2n, a3n, a4n -- recursive coefficients to compute Jacobi polynomials

    #Jacobi polynomials evaluated at point r
    Pn0 = 1.
    Pn1 = 0.5*(alpha - beta + (alpha + beta + 2) * r);

    for n in range(1,m):
        a1n = 2.*(n+1)*(n + alpha + beta +1)*(2.*n + alpha + beta)
        a2n = (2.*n + alpha + beta + 1)*(alpha**2 - beta**2)
        a3n = (2.*n + alpha + beta)*(2.*n + alpha + beta + 1)*(2.*n + alpha + beta + 2)
        a4n = 2.*(n + alpha)*(n + beta)*(2.*n + alpha + beta + 2)

        Pn2 = (1./a1n)*((a2n + a3n * r)*Pn1 - a4n*Pn0)

        Pn0 = Pn1
        Pn1 = Pn2

    if m == 1:
        Pm = Pn1
    elif m > 1:
        Pm = Pn2
    else:
        Pm = Pn0

    return Pm

# Data for visualization of polynomials
NOrders = 5     # number of polynomials of consecutive orders to be generated
o   = numpy.array(range(1, NOrders+1))
jac = numpy.empty_like(o, dtype=numpy.ndarray)

for i in range(0, NOrders):
    jac[ i ] = JacobiP( x, o[i], 0, 0 ) # I am choosing Legendre for example

pyplot.figure(figsize=(8,3))
pyplot.grid(True)
pyplot.xlabel(r'$\xi$', fontsize=18)
pyplot.ylabel(r'$P_p$', fontsize=18)
pyplot.tick_params(axis='both', labelsize=12)
pyplot.title('Jacobi polynomials up to order %d' % NOrders, fontsize=18)
pyplot.plot(x,jac[0], 'r-', lw=2, label='$P_0$')
pyplot.plot(x,jac[1], 'b-', lw=2, label='$P_1$')
pyplot.plot(x,jac[2], 'g-', lw=2, label='$P_2$')
pyplot.plot(x,jac[3], 'y-', lw=2, label='$P_3$')
pyplot.plot(x,jac[4], 'm-', lw=2, label='$P_4$')
```

## 1-D Modal Interpolation Functions

Using Jacobi polynomials, 1-D **modal** interpolation functions for the finite element method can be defined either in the system $\xi_1$ or in the system $L_1$ as:

Local coordinate system $\xi_1$
\begin{equation}
N_p(\xi_1)=\phi_p(\xi_1)=\left\{
\begin{array}{lll}
 \frac{1}{2}(1-\xi_1) & p=0 \\
 \frac{1}{2}(1+\xi_1) & p=P_1 \\
 \frac{1}{4}(1-\xi_1)(1+\xi_1)P_{p-1}^{\alpha_1, \beta_1}(\xi_1) & 0 < p
 < P_1
\end{array}\right.
\end{equation}

Local coordinate system $L_1$
\begin{equation}
N_p(L_1)=\phi_p(L_1)=\left\{
\begin{array}{lll}
 1-L_1 & p=0 \\
 L_1 & p=P_1 \\
(1-L_1)L_1P_{p-1}^{\alpha_1, \beta_1}(2L_1 - 1) & 0 < p
 < P_1
\end{array}\right.
\end{equation}

```python
def NpModal(xi, p, P1, alpha, beta):
    """Returns the pth interpolation function of a P1th-order modal interpolation at xi.

    Parameters
    ----------
    xi   : -1 <= xi <= 1, point of evaluation.
    p    : index of the interpolation function 0 <= p <= P1
    P1   : highest degree of the modal interpolation functions
    alpha: weight of the Jacobi polynomials
    beta : weight of the Jacobi polynomials

    Returns
    -------
    Np : pth interpolation function of a P1th-order modal interpolation at xi.
    """
    if p == 0:
        return 0.5*(1-xi)  # N_0
    elif p == P1:
        return 0.5*(1+xi)  # N_P1
    elif p > 0 and p < P1:
        return 0.25*(1-xi)*(1+xi)*JacobiP( xi, p-1, alpha, beta )
    else:
        return 0

pOrder = 4           # highest degree of the modal interpolation functions
NFuncs = pOrder + 1  # number of interpolation functions N_0 to N_pOrder

# Data for visualization of polynomials
nf  = numpy.array(range(0, NFuncs))
Npm = numpy.empty_like(nf, dtype=numpy.ndarray)

for i in range(0, NFuncs):
    Npm[ i ] = NpModal(x, i, pOrder, 0., 0.)

pyplot.figure(figsize=(8,3))
pyplot.grid(True)
pyplot.xlabel(r'$\xi$', fontsize=18)
pyplot.ylabel(r'$N_p$', fontsize=18)
pyplot.tick_params(axis='both', labelsize=12)
pyplot.title('1-D modal interpolation functions $P=$ %d' % pOrder, fontsize=18)
pyplot.plot(x,Npm[0], 'k-', lw=2, label='$\phi_0$')
pyplot.plot(x,Npm[1], 'b-', lw=2, label='$\phi_1$')
pyplot.plot(x,Npm[2], 'g-', lw=2, label='$\phi_2$')
pyplot.plot(x,Npm[3], 'y-', lw=2, label='$\phi_3$')
pyplot.plot(x,Npm[4], 'r-', lw=2, label='$\phi_4$')
pyplot.text(-0.9, 0.8, r'$\phi_0$')
pyplot.text(0.0, 0.3, r'$\phi_1$')
pyplot.text(0.02, 0.085, r'$\phi_2$')
pyplot.text(-0.45, 0.05, r'$\phi_3$')
pyplot.text(0.8, 0.8, r'$\phi_4$')

```

Can you observe the those basis functions lead to a continuous interpolation of the global domain? In addition, is it possible to combine element of different interpolation orders?

The bar element mass and stiffness matrices for $P=6$ and $\alpha_1=\beta_1=1$ in the $\xi_1$ coordinate system have the following patterns:

Mass Matrix

Stiffness Matrix

## Mapping

The bar element mass and stiffness matrices are given in the global and local coordinate systems as:
\begin{equation}
[M_e] = \int_{x_{i-1}}^{x_i} [N_e]^T[N_e]Adx = \int_{-1}^{1} [N_e]^T[N_e]A |J|d\xi_1
\end{equation}
\begin{equation}
[K_e] = \int_{x_{i-1}}^{x_i} [B_e]^T[D][B_e]Adx = \int_{-1}^{1} [B_e]^T[D][B_e]A |J|d\xi_1
\end{equation}

The Jacobian of the local-global mapping is given by
\begin{equation}
[J] = \frac{dx(\xi_1)}{d\xi_1}
\end{equation}

The $x$ coordinate can be interpolated in an isoparametric way using the same shape functions for the unknown and global coordinates of the nodes as
\begin{equation}
x(\xi_1) = \sum_{i=0}^n \phi_i(\xi_1)X_i
\end{equation}

Based on that, the Jacobian is given by
\begin{eqnarray*}
[J] &=& \frac{dx(\xi_1)}{d\xi_1} = \sum_{i=0}^n \frac{d\phi_i(\xi_1)}{d\xi_1} X_i \\
&=& \left[\begin{array}{cccc} \phi_{0,\xi_1} & \phi_{1,\xi_1} & \ldots & \phi_{n,\xi_1} \end{array} \right]
\left[\begin{array}{c} X_0 \\ X_1 \\ \vdots \\ X_n \end{array} \right]
\end{eqnarray*}

The global derivatives in the bar strain matrix $[B]=[\begin{array}{cc} \phi_1'(x) & \phi_2'(x)\end{array}]$ are calculated applying the chain rule:
\begin{equation}
\phi_i(x)=\phi_i(x(\xi_1))\;\;\rightarrow\;\;
\phi_{i,\xi_1} = \phi_{i,x}x,_{\xi_1}
\end{equation}
\begin{equation}
\phi_{i,x} = [J]^{-1}\phi_{i,\xi_1}
\end{equation}

## Numerical Integration

The coefficients of the element matrices are generally computed by numerical integration.

Let $u(\xi_1)$ be a polynomial function of order $(2Q-k)$, that is, $u(\xi_1)\in P_{2Q-k}([-1,1])$. The integration of the product of $u(\xi_1)$ by $(1-\xi_1)^\alpha(1+\xi_1)^\beta$ is
\begin{equation}
I = \int_{-1}^1 (1-\xi_1)^\alpha(1+\xi_1)^\beta u(\xi_1)d\xi_1
\end{equation}

The function $u(\xi_1)$ can be decomposed as
\begin{equation}
u(\xi_1) = \sum_{i=0}^{Q-1} u(\xi_{1,i})h_i(\xi_1) + s(\xi_1)r(\xi_1)
\end{equation}
assuming
- $h_i(\xi_j)=\delta_{ij}$ denotes the Lagrange polynomials associated to the
$Q$ nodal points $\xi_{1,i}$.
- $s(\xi_{1})$ is a polynomial of $Q$ with roots on the nodes $\xi_{i,1}$,
ou seja, $s(\xi_{1,i})=0$.
- $r(\xi_{1})$ is a polynomial expansion of the remainder with order $Q-k$.

Therefore,
\begin{equation}
I = \sum_{i=0}^{Q-1} w_i^{\alpha,\beta}u(\xi_{1,i})+R(u)
\end{equation}
\begin{equation}
w_i^{\alpha,\beta} = \int_{-1}^1 (1-\xi_1)^\alpha(1+\xi_1)^\beta h_i(\xi_1)d\xi_1
\end{equation}
\begin{equation}
R(u) = \int_{-1}^1 (1-\xi_1)^\alpha(1+\xi_1)^\beta s(\xi_{1})r(\xi_{1})d\xi_1
\end{equation}

We are looking for a function $s(\xi_1)$ such as the remainder $R(u)$ is null, implying an exact integration.

For $s(\xi_1) = P_{Q}^{\alpha, \beta}(\xi_1)$ and $k=1$
\begin{equation}
R(u) = \int_{-1}^1 (1-\xi_1)^\alpha (1+\xi_1)^\beta P_{Q}^{\alpha, \beta}(\xi_1)r(\xi_1)d\xi_1 = 0
\end{equation}
because $r(\xi_1)$ has order $Q-1$ and $s(\xi_1)$ has order $Q$.

That choice defines the **Gauss-Jacobi quadrature**. $\xi_{1,i}$ are the roots of $P_{Q}^{\alpha, \beta}(\xi_1)$ determined by a Newton-Raphson procedure. In some cases, it is useful to use $\xi_1=\pm 1$ as nodal points which defines the **Gauss-Radau** and **Gauss-Lobatto quadratures**.

The Gauss, Gauss-Radau and Gauss-Lobatto quadratures exactly integrate the polinomial functions of $2Q-1$, $2Q-2$ e $2Q-3$, respectively.

Number of points required to exactly integrate a function of order $P$:
- Gauss-Legendre (GL): $2Q-1=P\rightarrow Q=\frac12 (P+1)$
- Gauss-Radau (GR): $2Q-2=P\rightarrow Q=\frac12 (P+2)$
- Gauss-Lobatto-Legendre (GLL): $2Q-3=P\rightarrow Q=\frac12 (P+3)$

## Numerical Integration Example

\begin{equation}
f(\xi_1) = 1-\xi_1^2 = (1-\xi_1)(1+\xi_1), \;\;\xi_1\in[-1,1]
\end{equation}
\begin{equation}
I=\int_{-1}^{1}f(\xi_1)d\xi_1=\frac 43
\end{equation}

  <col align="left">
  <col align="right">
  <col align="right">

 Quadratura 	 Coordinates 	 Weights

 GL$(\alpha=\beta=0)$ 	 -0,5774
0,5774	 1
1

 GLL($\alpha=\beta=0)$ 	-1,0000
0,0000
1,0000	 1/3
4/3
1/3

 GR$(\alpha=\beta=0)$ 	-1,0000
0,3333	 1/2
3/2

 GJ$(\alpha=1,\beta=1)$	0,0000	 4/3

The Gauss-Legendre and Gauss-Lobatto-Legendre quadratures are implemented by the Python code below.

```python
#First derivative of Jacobi polynomials
def DJacobiP(r,m,alpha,beta):
# DPm, DPn1 -- Derivative of the mth-order Jacobi polynomial at point r
# Pn0, Pn1, Pn2 -- Jacobi polynomials of order n-1, n and n+1
# a1n, a2n, a3n, a4n -- recursive coefficients to compute Jacobi polynomials
# b1n, b2n, b3n -- recursive coefficients to compute first derivatives of Jacobi polynomials

    # Derivative of the Jacobi polynomial of order m evaluated at point r
    DPm = 0.
    Pn0 = 1.
    Pn1 = 0.5*(alpha - beta + (alpha + beta + 2) * r)
    Pn2 = 0.

    for n in range(1,m+1):
        a1n = 2.*(n+1)*(n + alpha + beta +1)*(2.*n + alpha + beta)
        a2n = (2.*n + alpha + beta + 1)*(alpha**2 - beta**2)
        a3n = (2.*n + alpha + beta)*(2.*n + alpha + beta + 1)*(2.*n + alpha + beta + 2)
        a4n = 2.*(n + alpha)*(n + beta)*(2.*n + alpha + beta + 2)

        Pn2 = (1./a1n)*((a2n + a3n * r)*Pn1 - a4n*Pn0)

        if r == -1.:
            DPn1 = (-1.0)**(m-1)*0.5*(alpha+beta+m+1)*math.factorial(m+beta)/(math.factorial(beta+1.)*math.factorial(m-1))
        elif r == 1.:
            DPn1 = 0.5*(alpha+beta+m+1.)*math.factorial(m+alpha)/(math.factorial(alpha+1)*math.factorial(m-1))
        else:
            b1n = (2.*n + alpha + beta)*(1 - r**2)
            b2n = (n)*(alpha - beta - (2.*n + alpha + beta) * r)
            b3n = 2.*(n + alpha)*(n + beta)

            DPn1 = (1./b1n)*(b2n*Pn1 + b3n*Pn0)

        Pn0 = Pn1
        Pn1 = Pn2

        DPm = DPn1

    return DPm

#Newton-Raphson algorithm with polynomial deflation to compute polynomial roots
def JacobiRoots(alpha,beta,m):

    #Allocates vector for quadrature points
    x = numpy.zeros(m)
    #Stop criterium for Newton-Raphson algorithm
    epsilon = 1.e-16

    #Newton-Raphson algorithm with polynomial deflation
    for k in range(0,m):
        #Initial guess: roots of the mth-order Chebyshev polynomial
        r = -numpy.cos(numpy.pi * (2.*k + 1)/(2.*m))
        if k > 0:
            r = (r + x[k-1])/2.

        delta = 1.
        while abs(delta) > epsilon:
            s = 0.
            for i in range(0,k):
                s = s + 1./(r - x[i])

            Pm = JacobiP(r,m,alpha,beta)
            DPm = DJacobiP(r,m,alpha,beta)
            delta = -Pm/(DPm - Pm * s)
            r = r + delta

        x[k] = r

    return x

#Quadrature weights
def JacobiGaussWeights(x,alpha,beta,m):

    #Allocates vector for quadrature weights
    w = numpy.zeros(m)

    if quad_pts == 'GL':
        #case'GL'
        c = 2.**(alpha + beta + 1)

        g1 = alpha + m + 1
        g2 = beta + m + 1
        g3 = alpha + beta + m + 1

        #Computes factorials g1, g2, g3 e M (WARNING: For n integer Gamma[n] = (n-1)!)
        gama1 = math.factorial(g1-1)
        gama2 = math.factorial(g2-1)
        gama3 = math.factorial(g3-1)
        M = math.factorial(m)

        #Computes quadrature weights
        C1 = c*gama1*gama2
        for i in range (0,m):
            r = x[i]
            DPm = DJacobiP(r,m,alpha,beta)
            C2 = M*gama3*(1-r**2)
            w[i] = (C1/C2)*(DPm**(-2))

    elif quad_pts == 'GLL':
        #case 'GLL'
        c = 2.**(alpha + beta + 1)

        g1 = alpha + m
        g2 = beta + m
        g3 = alpha + beta + m + 1

        #Computes factorials g1, g2, g3 e M (WARNING: For n integer Gamma[n] = (n-1)!)
        gama1 = math.factorial(g1-1)
        gama2 = math.factorial(g2-1)
        gama3 = math.factorial(g3-1)
        M = math.factorial(m-1)

        #Computes quadrature weights
        C1 = c*gama1*gama2
        C2 = (m - 1.)*M*gama3
        C3 = beta + 1.
        C4 = alpha + 1.

        r = x[0]
        Pm = JacobiP(r,m-1,alpha,beta)
        w[0] = C3*(C1/C2)*(Pm**(-2))

        r = x[m-1]
        Pm = JacobiP(r,m-1,alpha,beta)
        w[m-1] = C4*(C1/C2)*(Pm**(-2))

        for i in range (1,(m-1)):
            r = x[i]
            Pm = JacobiP(r,m-1,alpha,beta)
            w[i] = (C1/C2)*(Pm**(-2))

    else:
        print('Invalid integration type!')

    return w

#Quadrature points and weights
def JacobiGaussQuad(N):

    #Gets number of quadrature points based on the order of the polynomial approximation
    if quad_pts == 'GL':
        n_GL = (N+1.)/2.
        n = math.ceil(n_GL)

    elif quad_pts == 'GLL':
        n_GLL = (N+3.)/2.
        n = math.ceil(n_GLL) #math.floor(n_GLL) keeps the nodal spectral approach n = N+1

    else:
        print('Invalid integration type!')

    n=int(n) #JacobiRoots and JacobiGaussWeights dont accept n as float!

    #Sets parameters to obtain quadrature points from Legendre polynomials
    #Legendre == Jacobi(alpha,beta) with alpha == 0 and beta == 0
    alpha = 0
    beta = 0

    #Sobroutines that evaluates the quadrature points and weights using modified
    #Newton-Raphson algorithm with polynomial deflation
    if quad_pts == 'GL':
        xi = JacobiRoots(alpha,beta,n)
        wi = JacobiGaussWeights(xi,alpha,beta,n)

    elif quad_pts == 'GLL':
        #Allocating vector for quadrature points
        xi = numpy.zeros(n)
        x = JacobiRoots(alpha+1,beta+1,n-2)
        xi[1:-1] = x.copy()
        xi[0] = -1.0
        xi[n-1] = 1.0
        wi = JacobiGaussWeights(xi,alpha,beta,n)

    else:
        print('Invalid integration type!')

    return xi, wi

# Quadrature points and weights
N = 2 # polynomial order

quad_pts = 'GL' # Gauss-Legendre
(xi,wi) = JacobiGaussQuad(N)
nip = len(xi)

print('Gauss-Legendre (GL)')
print('Coordinates |   Weights')
for i in range(0,nip):
    print('{:10.5f}  |{:10.5f}'.format(xi[i], wi[i]))

# Function to be integrated
fi = 1-xi**2
# Numerical integration
I  = numpy.dot(wi, fi)
print('Integral (GL)  = {:10.5f}'.format( I ))

quad_pts = 'GLL' # Gauss-Legendre-Lobatto
(xi,wi) = JacobiGaussQuad(N)
nip = len(xi)

print('')
print('Gauss-Legendre-Lobatto (GLL)')
print('Coordinates |   Weights')
for i in range(0,nip):
    print('{:10.5f}  |{:10.5f}'.format(xi[i], wi[i]))

# Function to be integrated
fi = 1-xi**2
# Numerical integration
I  = numpy.dot(wi, fi)
print('Integral (GLL) = {:10.5f}'.format( I ))
```

    Gauss-Legendre (GL)
    Coordinates |   Weights
      -0.57735  |   1.00000
       0.57735  |   1.00000
    Integral (GL)  =    1.33333

    Gauss-Legendre-Lobatto (GLL)
    Coordinates |   Weights
      -1.00000  |   0.33333
       0.00000  |   1.33333
       1.00000  |   0.33333
    Integral (GLL) =    1.33333

## Assignment

1. Implement a Python code using the 1-D modal interpolation functions and numeric quadrature to compute the local stiffness matrix of a bar element in the local coordinate system $\xi_1$.
2. Extend the code implemented in the previous item to compute the same stiffness matrix but in the global coordinate system. Hint: you need to choose a local-to-global mapping.
3. Extend the code presented in the previous class to solve a bar subjected to the same constant load $q(x)=q_0$ and the same boundary conditions using the 1-D modal interpolation.
4. Increase the polynomial order and check the convergence error.
5. Solve the bar problem again, now considering $q(x)=q_0\sin(\pi x/2)$. Plot the approximate error for the $h$ and $p$-refinements in terms of the number of degrees of freedom in logarithmic scale. Calculate the rates of convergence.

## References

- G. E. Karniadakis, S. J. Sherwin. *Spectral/hp Element Methods for CFD*. Oxford University
Press, Oxford, 2nd edition, 2005.
- M.L. Bittencourt. *Fully tensorial nodal and modal shape functions for triangles and tetrahedra*. IJNME, 63(2):1530-1558, 2005.
- M.L. Bittencourt, M. G. Vazquez, T. G. Vazquez. *Construction of shape functions for the h- and p- versions of the fem using tensorial product*. IJNME, 71(5):529-563, 2007.
- S. Dong, Z. Yosibash. *A parallel spectral element method for dynamic three-dimensional nonlinear
elasticity problems*. Computer & Structures, (4):61-85, 2008.
- M.L. Bittencourt, T. G. Vazquez. *Tensor-based gauss-jacobi numerical integration for high-order
mass and stiffness matrices.* IJNME, 79(5):599-638,2009.
- M.L. Bittencourt, *Computational Solid Mechanics: Variational Formulation and High Order Approximation*, CRC Press, 2014.

```python

```


## N-Body problems

Many physical problems require the evaluation of all pairwise interactions of a large number of particles, so-called N-body problems. These problems arise in molecular dynamics, astrodynamics and electromagnetics among others.

Their pairwise interactions can be expressed as:

\begin{equation}
f_i = \sum_{j=1}^n{P \left(\boldsymbol{x}_i, \boldsymbol{x}_j \right)w_j} \ \ \ \text{for } i=1,2,...,n
\end{equation}

*  where subscripts $i$,  $j$ respectively denote *target* and *source*
*  $f_i$ can be a *potential* (or *force*) at target point $i$
*  $w_j$ is the *source weight*
*  $\boldsymbol{x}_i, \boldsymbol{x}_j$ are the *spatial positions* of particles
*  $P \left(\boldsymbol{x}_i, \boldsymbol{x}_j \right)$ is the *interaction kernel*.

In order to evalute the potential $f_i$ at a target point $i$, we have to loop over each source particle $j$. Since there are $n$ target points $i$, this 'brute-force' approach costs $\mathcal{O} \left(n^2 \right)$ operations.

One possible approach in this kind of problem is to define a few classes, say `Point` and `Particle` and then loop over the objects and perform the necessary point-to-point calculations.

```python
import numpy
```

```python
class Point():
    """
    Arguments:
        domain: the domain of random generated coordinates x,y,z,
                default=1.0

    Attributes:
        x, y, z: coordinates of the point
    """
    def __init__(self, domain=1.0):
        self.x = domain * numpy.random.random()
        self.y = domain * numpy.random.random()
        self.z = domain * numpy.random.random()

    def distance(self, other):
        return ((self.x - other.x)**2 +
                (self.y - other.y)**2 +
                (self.z - other.z)**2)**.5
```

```python
class Particle(Point):
    """
    Attributes:
        m: mass of the particle
        phi: the potential of the particle
    """

    def __init__(self, domain=1.0, m=1.0):
        Point.__init__(self, domain)
        self.m = m
        self.phi = 0.
```

Now we create a list of `n` random particles, define a function to calculate their interaction via direct summation and run!

```python
n = 1000
particles = [Particle(m = 1 / n) for i in range(n)]
```

```python
def direct_sum(particles):
    """
    Calculate the potential at each particle
    using direct summation method.

    Arguments:
        particles: the list of particles

    """
    for i, target in enumerate(particles):
        for source in (particles[:i] + particles[i+1:]):
            r = target.distance(source)
            target.phi += source.m / r
```

```python
direct_sum(particles)
```

There was a noticeable lag there.  How long does this thing take for 1000 particles?

```python
orig_time = %timeit -o direct_sum(particles)
```

    1 loop, best of 3: 3 s per loop

```python
%load_ext line_profiler
```

```python
%lprun -f direct_sum direct_sum(particles)
```

## How do we use Numba on this problem?

Problem: Numba doesn't support jitting native Python classes.  There is a `jit_class` structure in Numba but it's still in early development.

But it's nice to have attributes for literate programming.

Solution: NumPy custom dtypes.

```python
particle_dtype = numpy.dtype({'names':['x','y','z','m','phi'],
                             'formats':[numpy.double,
                                        numpy.double,
                                        numpy.double,
                                        numpy.double,
                                        numpy.double]})
```

```python
myarray = numpy.ones(3, dtype=particle_dtype)
```

```python
myarray
```

    array([( 1.,  1.,  1.,  1.,  1.), ( 1.,  1.,  1.,  1.,  1.),
           ( 1.,  1.,  1.,  1.,  1.)],
          dtype=[('x', '<f8'), ('y', '<f8'), ('z', '<f8'), ('m', '<f8'), ('phi', '<f8')])

You can access an individual "attribute" like this:

```python
myarray[0]['x'] = 2.0
```

```python
myarray
```

    array([( 2.,  1.,  1.,  1.,  1.), ( 1.,  1.,  1.,  1.,  1.),
           ( 1.,  1.,  1.,  1.,  1.)],
          dtype=[('x', '<f8'), ('y', '<f8'), ('z', '<f8'), ('m', '<f8'), ('phi', '<f8')])

## [Exercise 1](./exercises/04.Direct.Summation.Exercises.ipynb#Exercise-1)

Write a function `create_n_random_particles` that takes the arguments `n` (number of particles), `m` (mass of every particle) and a domain within to generate a random number (as in the class above).
It should create an array with `n` elements and `dtype=particle_dtype` and then return that array.

For each particle, the mass should be initialized to the value of `m` and the potential `phi` initialized to zero.

```python
from numba import njit
```

```python
# %load snippets/nbody/create_n.py
```

**Note**: You can use "attribute" access on dtypes but there's a caveat.  If you need to debug this function without the decorator, you have to change them back to array access form.

```python
parts = create_n_random_particles(1000, .001, 1)
```

```python
parts[:3]
```

We don't have a `distance` method anymore, so we need to write a function to take care of that.

## [Exercise 2](./exercises/04.Direct.Summation.Exercises.ipynb#Exercise-2)
Write a JITted function `distance` to calculate the distance between two particles of dtype `particle_dtype`

```python
# %load snippets/nbody/distance.py
```

```python
distance(parts[0], parts[1])
```

```python
%%timeit
distance(parts[0], parts[1])
```

## [Exercise 3](./exercises/04.Direct.Summation.Exercises.ipynb#Exercise-3)
Modify the `direct_sum` function above to instead work a NumPy array of particles.  Loop over each element in the array and calculate its total potential.

```python
# %load snippets/nbody/direct_sum.py
```

```python
numba_time = %timeit -o direct_sum(parts)
```

```python
orig_time.best / numba_time.best
```


```
%matplotlib inline
from sympy import var, Matrix, eye, init_printing, roots
init_printing()
```

```
var("a:5")
var("b:5")
var("s");
```

# Formas canónicas controlador y controlabilidad

Tenemos un sistema de orden 4 con una representación de estado:

```
Ac = Matrix([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [-a4, -a3, -a2, -a1]])
bc = Matrix([[0], [0], [0], [1]])
cc = Matrix([[b4], [b3], [b2], [b1]])
```

```
Ac
```

```
bc
```

```
cc
```

La cual ya esta en la forma canónica controlador, podemos obtener su polinomio caracteristico:

```
PCc = (s*eye(4) - Ac).det()
PCc
```

La matriz sistema es la siguiente:

```
MSc  = (s*eye(4) - Ac).row_join(bc).col_join(-cc.T.row_join(Matrix([[0]])))
MSc
```

y su polinomio es:

```
pMSc = MSc.det()
pMSc
```

su Matriz de controlabilidad es:

```
MCc = ((bc.row_join(Ac*bc)).row_join(Ac*Ac*bc)).row_join(Ac*Ac*Ac*bc)
MCc
```

y su determinante es:

```
MCc.det()
```

Dado este sistema, lo podemos transformar a su forma controlabilidad:

```
Aco = Matrix([[-a1, 1, 0, 0], [-a2, 0, 1, 0], [-a3, 0, 0, 1], [-a2, 0, 0, 0]])
bco = bc
beta = Matrix([[1, 0, 0, 0], [a1, 1, 0, 0], [a2, a1, 1, 0], [a3, a2, a1, 1]]).inv()*Matrix([[b1], [b2], [b3], [b4]])
cco = Matrix([[beta[3]], [beta[2]], [beta[1]], [beta[0]]])
```

```
Aco
```

```
bco
```

```
cco
```

La cual tiene un polinomio caracteristico:

```
PCco = (s*eye(4) - Aco).det()
PCco
```

y una matriz sistema:

```
MSco = (s*eye(4) - Aco).row_join(bco).col_join(-cco.T.row_join(Matrix([[0]])))
MSco
```

con un polinomio:

```
pMSco = MSco.det()
pMSco.factor()
```

y una matriz de controlabilidad:

```
MCco = ((bco.row_join(Aco*bco)).row_join(Aco*Aco*bco)).row_join(Aco*Aco*Aco*bco)
MCco
```

con un determintante:

```
MCco.det()
```

## Ejemplo

```
A = Matrix([[0, 1, 0], [0, 0 , 1], [0, 0, 0]])
b = Matrix([[0], [0], [1]])
c = Matrix([[-1], [0], [1]])
x0 = Matrix([[1], [0], [0]])
```

La función de transferencia de este sistema es:

```
FT = c.T*(s*eye(3) - A).inv()*b
FT[0].factor()
```

su polinomio caracteristico:

```
PC = (s*eye(3) - A).det()
PC
```

su matriz sistema es:

```
MS = (s*eye(3) - A).row_join(b).col_join(-c.T.row_join(Matrix([[0]])))
MS
```

y el polinomio de esta matriz:

```
pMS = MS.det()
pMS.factor()
```

su matriz de controlabilidad:

```
C = (b.row_join(A*b)).row_join(A*A*b)
C
```

```
C.det()
```

Por el momento hemos visto que este sistema es controlable, sin embargo, aun no sabemos la retroalimentacion del estado que lo controlara.

Nuestra primera proposición será un sistema que se comporte como el siguiente:

$$
FT = \frac{(s-1)(s+1)}{(s-1)(s+1)^2}
$$

podemos ver que los polos de ese sistema son $+1$ y $-1$ con multiplicidad $2$, si averiguamos la forma del polinomio caracteristico, podemos ver los coeficientes de la retroalimentación:

```
((s+1)**2 *(s-1)).expand()
```

Si los coeficientes actuales son todos $0$ y tenemos que sustraer el vector de retroalimentación para obtener el que queremos, obtenemos que la retroalimentación debe ser de la forma:

```
f1 = Matrix([[1], [1], [-1]])
```

y por lo tanto, la matriz retroalimentada y el polinomio caracteristico, quedan de la siguiente manera:

```
Af1 = A + b*f1.T
Af1
```

```
PClc1 = (s*eye(3) - Af1).det()
PClc1.factor()
```

y el polinomio del sistema:

```
MSlc1 = (s*eye(3) - Af1).row_join(b).col_join(-c.T.row_join(Matrix([[0]])))
MSlc1
```

```
pMSlc1 = MSlc1.det()
pMSlc1.factor()
```

Ahora realicemos la simulación para ver el comportamiento del sistema.

```
from control import ss, step_response, forced_response
from numpy import linspace, matrix, sign
```

```
%matplotlib inline
from matplotlib.pyplot import plot, figure, gca, legend, style
```

```
heavyside = lambda x: 0.5 * (sign(x) + 1)
```

```
Af1 = matrix(Af1.tolist(), dtype=float)
b = matrix(b.tolist(), dtype=float)
c = matrix(c.tolist(), dtype=float)
f1 = matrix(f1.tolist(), dtype=float)
SR = ss(Af1, b, c.T, 0)
```

```
t = linspace(0, 10, 1000)
u = heavyside(t)
t, y, x = forced_response(SR, T=t, U=u)
v = (f1.T*x).T
```

```
f = figure(figsize=(15, 6))
style.use("ggplot")
p1, = plot(t, y)
p2, = plot(t, u)

ax = f.gca()
ax.set_ylim(-0.2, 1.2);

legend([p1, p2], [r"$y(t)$", r"$\delta$"]);
```

```
f = figure(figsize=(15, 6))
style.use("ggplot")
p1, = plot(t, x[0])
p2, = plot(t, x[1])
p3, = plot(t, x[2])

ax = f.gca()
ax.set_ylim(-0.2, 1.2);

legend([p1, p2, p3], [r"$x_1$", r"$x_2$", r"$x_3$"]);
```

```
f = figure(figsize=(15, 6))
style.use("ggplot")
p1, = plot(t, v)

ax = f.gca()
ax.set_ylim(-0.2, 1.2);

legend([p1], [r"$u(t)$"]);
```

```
A = Matrix([[0, 1, 0], [0, 0 , 1], [0, 0, 0]])
b = Matrix([[0], [0], [1]])
c = Matrix([[-1], [0], [1]])
x0 = Matrix([[1], [0], [0]])
```

```
((s+1)**3).expand()
```

```
f2 = Matrix([[-1], [-3], [-3]])
```

```
Af2 = A + b*f2.T
Af2
```

```
PClc2 = (s*eye(3) - Af2).det()
PClc2.factor()
```

```
MSlc1 = (s*eye(3) - Af2).row_join(b).col_join(-c.T.row_join(Matrix([[0]])))
MSlc1
```

```
pMSlc1 = MSlc1.det()
pMSlc1.factor()
```

```
T2, J2 = Af2.jordan_form()
J2
```

```
bJ2 = T2.inv()*b
cJ2 = T2.T*c
```

```
MSJ2 = (s*eye(3) - J2).row_join(bJ2).col_join(-cJ2.T.row_join(Matrix([[0]])))
MSJ2
```

```
FTJ2 = cJ2.T*(s*eye(3) - J2).inv()*bJ2
FTJ2[0].factor()
```

```
Af2 = matrix(Af2.tolist(), dtype=float)
b = matrix(b.tolist(), dtype=float)
c = matrix(c.tolist(), dtype=float)
SR = ss(Af2, b, c.T, 0)
```

```
t = linspace(0, 10, 1000)
u = heavyside(t)
t, y, x = forced_response(SR, T=t, U=u)
v = (f1.T*x).T
```

```
f = figure(figsize=(15, 6))
style.use("ggplot")
p1, = plot(t, y)
p2, = plot(t, u)

ax = f.gca()
ax.set_ylim(-0.2, 1.2);

legend([p1, p2], [r"$y(t)$", r"$\delta$"]);
```

```
f = figure(figsize=(15, 6))
style.use("ggplot")
p1, = plot(t, x[0])
p2, = plot(t, x[1])
p3, = plot(t, x[2])

ax = f.gca()
ax.set_ylim(-0.2, 1.2);

legend([p1, p2, p3], [r"$x_1$", r"$x_2$", r"$x_3$"]);
```

```
f = figure(figsize=(15, 6))
style.use("ggplot")
p1, = plot(t, v)

ax = f.gca()
ax.set_ylim(-0.2, 1.2);

legend([p1], [r"$u(t)$"]);
```

```

```

IPythonNotebooks/Teoria de Control I/Formas canonicas controlador y controlabilidad.ipynb