# Column Generation for the Cutting Stock Problem

## Objectives and Prerequisites

In this notebook, you will:

1. Learn about the Cutting Stock Problem, a classical problem from the Integer Programming (IP) literature.
2. Discover how delayed column generation can be implemented in Gurobi to solve this problem more efficiently.

This modelling example is at the advanced level. To fully understand the content of this notebook, the reader should be familiar with the following:

- Object-Oriented Programming.
- The Knapsack Problem.
- Delayed column generation as a decomposition technique.

#### Gurobi License

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

-----

## Motivation

Despite the great advancements that we've seen in mathematical optimization solver technology, some problems are still challenging to solve due to their large number of variables or constraints. Decomposition techniques — such as delayed column generation or Benders decomposition — apply the principle of "divide and conquer" to tackle these challenging problems.

Delayed column generation is typically used with large problems, which have so many variables that it's not possible to consider all of them explicitly. Experience with large problems indicates that, usually, most of these variables never enter the basis. Therefore, only a subset of these variables need to ever be considered when solving the problem. Delayed column generation exploits this fact to only generate variables (i.e. columns) that have the potential to improve the objective function. It's interesting to note that generating a column in the primal formulation amounts to adding a constraint to its dual. Thus, you can think of this technique as a cutting plane method in the dual space.

Delayed column generation has been successfully applied to many problems such as crew scheduling, vehicle routing, the capacitated p-median problem, and the Cutting Stock Problem. We will use the latter to illustrate how this technique can be implemented in the Gurobi-Python interface.

-----

## Problem Description

The Cutting Stock Problem deals with the problem of cutting stock material with the same, fixed width — such as paper rolls — into smaller pieces, according to a set of orders specifying both the widths and the demand requirements, so as to minimize the amount of wasted material.

-----

## Solution Approach

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

A mathematical optimization model has five components, namely:

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

First, we present a naïve IP that doesn't scale well because its linear relaxation is not tight. Then, we introduce a Full Master Problem that explicitly enumerates all possible cut patterns, which turns out to be amenable to delayed column generation.

### Naïve IP

#### Sets and Indices

$i \in \text{Stock}=\{1,2,\dots, N\}$: Set of available stock material units.

$j \in \text{Pieces}$: Set of final pieces.

#### Parameters

$N \in \mathbb{N}$: Upper bound for the number of stock material units that can be used.

$\text{Stock_length} \in \mathbb{R}^+$: Length of any stock material unit.

$\text{Piece_length}_j \in \mathbb{R}^+$: Length of final piece $j$.

$\text{Requirement}_j \in \mathbb{N}$: Demand requirement of final piece $j$.

#### Decision Variables

$\text{use}_i \in \{0,1\}$: 1 if stock material unit $i$ is used; 0 otherwise.

$\text{cut}_{i,j} \in \mathbb{N}$: Number of pieces of type $j$ cut out from stock material unit $i$.

#### Objective Function

- **Material Consumption:** Minimize the number of stock material units used.

$$
\begin{equation}
\text{Min} \sum_{i \in \text{Stock}}{\text{use}_i}
\tag{1.0}
\end{equation}
$$

#### Constraints

- **Satisfy Demand:** Total number of final pieces of type $j$ obtained must satisfy its demand requirement.

$$
\begin{equation}
\sum_{i \in \text{Stock}}{\text{cut}_{i,j}} \geq \text{Requirement}_j \quad \forall j \in \text{Pieces}
\tag{1.1}
\end{equation}
$$

- **Capacity:** Total length of the pieces cut out from stock material unit $i$ cannot exceed its length.

$$
\begin{equation}
\sum_{j \in \text{Pieces}}{\text{Piece_length}_j \cdot \text{cut}_{i,j}} \leq \text{Stock_length} \cdot \text{use}_i \quad \forall i \in \text{Stock}
\tag{1.2}
\end{equation}
$$

- **Symmetry breaking:** Stock material unit $i+1$ cannot be used if the previous unit $i$ wasn't. Note that this constraint set is not needed for correctness, but certainly helps to reduce the search space.

$$
\begin{equation}
\text{use}_{i+1} \leq \text{use}_i \quad \forall i \in \text{Stock} \setminus \{N\}
\tag{1.3}
\end{equation}
$$

### Full Master Problem

Now, instead of focusing on which stock material unit a particular piece is to be cut from, we consider all possible patterns (potentially generating multiple pieces of different widths) that can be used to cut from a stock material unit. The task is now to find how many times a particular pattern should be used.

#### Sets and Indices

$p \in \text{Patterns}$: Set of all possible cutting patterns, whose size grows exponentially with the number of final pieces. Note that each of these patterns implicitly considers the lengths of the final pieces and the stock material units, which in fact can be viewed as a knapsack constraint.

$j \in \text{Pieces}$: Set of final pieces.

#### Parameters

$\text{Requirement}_j \in \mathbb{N}$: Demand requirement of final piece $j$.

$\text{Frequency}_{p,j} \in \mathbb{N}$: Number of pieces of type $j$ cut out from pattern $p$.

#### Decision Variables

$\text{count}_p \in \mathbb{N}$: Number of times pattern $p$ is used.

#### Objective Function

- **Material Consumption:** Minimize the number of stock material units used.

$$
\begin{equation}
\text{Min} \sum_{p \in \text{Patterns}}{\text{count}_p}
\tag{2.0}
\end{equation}
$$

#### Constraints

- **Satisfy Demand:** Total number of final pieces of type $j$ obtained must satisfy its demand requirement.

$$
\begin{equation}
\sum_{p \in \text{Patterns}}{\text{Frequency}_{p,j} \cdot \text{count}_p} \geq \text{Requirement}_j \quad \forall j \in \text{Pieces}
\tag{2.1}
\end{equation}
$$

Recall that the number of patterns grows exponentially with the number of final pieces, so in practice the Full Master Problem cannot be solved in one shot. Instead, we consider a Restricted Master Problem (RMP), initialized with a few trivial patterns. Then we follow an iterative approach, alternating between solving a knapsack problem (i.e. the subproblem) to generate a new pattern (i.e. the entering column) and solving a larger RMP to update the objective function — representing the reduced cost of the next pattern to be selected — of the subproblem. This cycle is repeated until the reduced cost of the entering column is non-negative.

-----

## Python Implementation

In the following implementation of column generation, we use two main libraries:

- Numpy for scientific computing.
- Gurobi for mathematical optimization.

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

class MasterProblem:
    def __init__(self):
        self.model = gp.Model("master")
        self.vars = None
        self.constrs = None

    def setup(self, patterns, demand):
        num_patterns = len(patterns)
        self.vars = self.model.addVars(num_patterns, obj=1, name="Pattern")
        self.constrs = self.model.addConstrs((gp.quicksum(patterns[pattern][piece]*self.vars[pattern]
                                                          for pattern in range(num_patterns))
                                              >= demand[piece] for piece in demand.keys()),
                                             name="Demand")
        self.model.modelSense = GRB.MINIMIZE
        # Turning off output because of the iterative procedure
        self.model.params.outputFlag = 0
        self.model.update()

    def update(self, pattern, index):
        new_col = gp.Column(coeffs=pattern, constrs=self.constrs.values())
        self.vars[index] = self.model.addVar(obj=1, column=new_col,
                                             name=f"Pattern[{index}]")
        self.model.update()

class SubProblem:
    def __init__(self):
        self.model = gp.Model("subproblem")
        self.vars = {}
        self.constr = None

    def setup(self, stock_length, lengths, duals):
        self.vars = self.model.addVars(len(lengths), obj=duals, vtype=GRB.INTEGER,
                                       name="Frequency")
        self.constr = self.model.addConstr(self.vars.prod(lengths) <= stock_length,
                                           name="Knapsack")
        self.model.modelSense = GRB.MAXIMIZE
        # Turning off output because of the iterative procedure
        self.model.params.outputFlag = 0
        # Stop the subproblem routine as soon as the objective's best bound becomes
        #less than or equal to one, as this implies a non-negative reduced cost for
        #the entering column.
        self.model.params.bestBdStop = 1
        self.model.update()

    def update(self, duals):
        self.model.setAttr("obj", self.vars, duals)
        self.model.update()

class CuttingStock:
    def __init__(self, stock_length, pieces):
        self.stock_length = stock_length
        self.pieces, self.lengths, self.demand = gp.multidict(pieces)
        self.patterns = None
        self.duals = [0]*len(self.pieces)
        piece_reqs = [length*req for length, req in pieces.values()]
        self.min_rolls = np.ceil(np.sum(piece_reqs)/stock_length)
        self.solution = {}
        self.master = MasterProblem()
        self.subproblem = SubProblem()

    def _initialize_patterns(self):
        # Find trivial patterns that consider one final piece at a time,
        #fitting as many pieces as possible into the stock material unit
        patterns = []
        for idx, length in self.lengths.items():
            pattern = [0]*len(self.pieces)
            pattern[idx] = self.stock_length // length
            patterns.append(pattern)
        self.patterns = patterns

    def _generate_patterns(self):
        self._initialize_patterns()
        self.master.setup(self.patterns, self.demand)
        self.subproblem.setup(self.stock_length, self.lengths, self.duals)
        while True:
            self.master.model.optimize()
            self.duals = self.master.model.getAttr("pi", self.master.constrs)
            self.subproblem.update(self.duals)
            self.subproblem.model.optimize()
            reduced_cost = 1 - self.subproblem.model.objVal
            if reduced_cost >= 0:
                break

            pattern = [0]*len(self.pieces)
            for piece, var in self.subproblem.vars.items():
                if var.x > 0.5:
                    pattern[piece] = round(var.x)
            self.master.update(pattern, len(self.patterns))
            self.patterns.append(pattern)
    def solve(self):
        """
        Gurobi does not support branch-and-price, as this requires to add columns
        at local nodes of the search tree. A heuristic is used instead, where the
        integrality constraints for the variables of the final root LP relaxation
        are installed and the resulting MIP is solved. Note that the optimal
        solution could be overlooked, as additional columns are not generated at
        the local nodes of the search tree.
        """
        self._generate_patterns()
        self.master.model.setAttr("vType", self.master.vars, GRB.INTEGER)
        self.master.model.optimize()
        for pattern, var in self.master.vars.items():
            if var.x > 0.5:
                self.solution[pattern] = round(var.x)

```

### Test Instance

We now test our implementation using a toy example with 13 final pieces to be cut out from stock material units of 5,600 cm. The set of orders specifying the lengths and the demand requirements of final pieces is presented below:

| ID | Final Piece Length (cm) | Demand (units)|
|----|----|----|
| 0 | 1380 | 22 |
| 1 |  1520 | 25 |
| 2 | 1560 | 12 |
| 3 | 1710 | 14 |
| 4 | 1820 | 18 |
| 5 | 1880 | 18 |
| 6 | 1930 | 20 |
| 7 | 2000 | 10 |
| 8 | 2050 | 12 |
| 9 | 2100 | 14 |
| 10 | 2140 | 16 |
| 11 | 2150 | 18 |
| 12 | 2200 | 20 |

```python
# Dictionary that maps each final piece ID with their width and demand requirement
orders = {0:[1380,22],
          1:[1520,25],
          2:[1560,12],
          3:[1710,14],
          4:[1820,18],
          5:[1880,18],
          6:[1930,20],
          7:[2000,10],
          8:[2050,12],
          9:[2100,14],
          10:[2140,16],
          11:[2150,18],
          12:[2200,20]}

mycsp = CuttingStock(5600, orders)
mycsp.solve()
```

    Using license file c:\gurobi\gurobi.lic

The patterns generated are the following:

```python
mycsp.patterns
```

    [[4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
     [0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0],
     [0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
     [0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
     [1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
     [1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0],
     [0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
     [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
     [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0],
     [0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
     [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0],
     [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
     [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
     [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0],
     [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
     [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
     [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
     [0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
     [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
     [0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
     [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
     [0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0],
     [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0],
     [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
     [0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0],
     [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]]

The patterns that are actually used and their respective frequency are reported below:

| Pattern ID | Frequency | Description |
|----|----|----|
| 4 | 1 | 3 units of final piece 4 |
| 17 | 7 | 1 unit and 2 units of final pieces 0 and 9 |
| 19 | 2 | 1 unit of final pieces 2, 5, and 10 |
| 20 | 4 | 2 unit and 1 units of final pieces 3 and 10 |
| 22 | 1 | 2 unit and 1 units of final pieces 3 and 11 |
| 26 | 5 | 1 unit of final pieces 1, 5, and 10 |
| 28 | 17 | 1 unit of final pieces 1, 6, and 11 |
| 29 | 4 | 1 unit and 2 units of final pieces 4 and 5 |
| 31 | 10 | 1 unit of final pieces 0, 7, and 12 |
| 32 | 3 | 1 unit of final pieces 2, 6, and 8 |
| 34 | 4 | 1 unit of final pieces 3, 4, and 8 |
| 35 | 5 | 1 unit of final pieces 0, 8, and 10 |
| 36 | 3 | 1 unit of final pieces 1, 5, and 12 |
| 38 | 7 | 1 unit of final pieces 2, 4, and 12 |

```python
print(mycsp.solution)
```

    {4: 1, 17: 7, 19: 2, 20: 4, 22: 1, 26: 5, 28: 17, 29: 4, 31: 10, 32: 3, 34: 4, 35: 5, 36: 3, 38: 7}

The minimum number of stock material units that must be used is:

```python
print(mycsp.min_rolls)
```

    73.0

The actual number of stock material units used by the solution found is:

```python
sum(mycsp.solution.values())
```

    73

## Conclusion

This notebook showed how to implement delayed column generation using Gurobi in an object-oriented framework. It also suggested a heuristic that can be used in lieu of a branch-and-price scheme, in the event that this is not compatible with Gurobi. Finally, it's worth mentioning that the reader can improve the performance of this implementation by replacing the subproblem routine, which uses a MIP solver, with an algorithm specifically tailored for the Knapsack problem.

-----

## References

1. Desaulniers, G., Desrosiers, J., & Solomon, M. M. (Eds.). (2006). Column generation (Vol. 5). Springer Science & Business Media.
2. Kantorovich, L. V., & Zalgaller, V. A. (1951). Calculation of rational cutting of stock. Lenizdat, Leningrad, 5, 11-14.

Copyright © 2021 Gurobi Optimization, LLC


```python
import symbolic_modern_robotics as smr
import sympy as s
s.init_printing()
```

```python
# Example 6.1
from IPython.display import Image
Image('exam_6.1_fig.PNG', width=500)
```

```python
# 1-1. Find Forward Kinematics, Space PoE
l1,l2,g = s.symbols('l1,l2,g')
m1,m2 = s.symbols('m1,m2')
theta_1,theta_2,thetadot_1,thetadot_2,thetaddot_1,thetaddot_2 = s.symbols('theta_1,theta_2,thetadot_1,thetadot_2,thetaddot_1,thetaddot_2', positive=True)

thetalist = s.Matrix([theta_1,theta_2])
dthetalist = s.Matrix([thetadot_1,thetadot_2])
ddthetalist = s.Matrix([thetaddot_1,thetaddot_2])

M = s.Matrix([[1, 0, 0, l1+l2],
              [0, 1, 0, 0],
              [0, 0, 1, 0],
              [0, 0, 0, 1]])

M01 = s.Matrix([[1, 0, 0, l1],
                [0, 1, 0, 0],
                [0, 0, 1, 0],
                [0, 0, 0, 1]])

M12 = s.Matrix([[1, 0, 0, l2],
                [0, 1, 0, 0],
                [0, 0, 1, 0],
                [0, 0, 0, 1]])

Mlist = [M01, M12]

Slist = s.Matrix([[0, 0, 1, 0, 0, 0],
                  [0, 0, 1, 0, -l1, 0]]).T

Blist = s.Matrix([[0, 0, 1, 0, l1+l2, 0],
                  [0, 0, 1, 0, l2, 0]]).T

# Check Forward Kinematics
T01 = smr.MatrixExp6(smr.VecTose3(Slist[:, 0]*thetalist[0]))
T12 = smr.MatrixExp6(smr.VecTose3(Slist[:, 1]*thetalist[1]))
ForKinSpace = T01@T12@M
T01 = smr.MatrixExp6(smr.VecTose3(Blist[:, 0]*thetalist[0]))
T12 = smr.MatrixExp6(smr.VecTose3(Blist[:, 1]*thetalist[1]))
ForKinBody = M@T01@T12

G01 = s.diag(0,0,0, m1, m1, m1)
G12 = s.diag(0,0,0, m2, m2, m2)
Glist = [G01, G12]

g_vec = s.Matrix([0, -g, 0])
Ftip = s.Matrix([0,0,0,0,0,0])

tau = smr.InverseDynamics(thetalist, dthetalist, ddthetalist, g_vec, Ftip, Mlist, Glist, Slist)
tau
```

    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The to_png function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The to_rgba function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The to_mask function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The MathtextBackendBitmap class was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)

$\displaystyle \left[\begin{matrix}g l_{1} m_{1} \cos{\left(\theta_{1} \right)} + g l_{1} m_{2} \cos{\left(\theta_{1} \right)} + g l_{2} m_{2} \cos{\left(\theta_{1} + \theta_{2} \right)} + l_{1}^{2} m_{1} \ddot{\theta}_{1} + l_{1}^{2} m_{2} \ddot{\theta}_{1} + l_{1}^{2} m_{2} \dot{\theta}_{1} \dot{\theta}_{2} \sin{\left(\theta_{2} \right)} + 2 l_{1} l_{2} m_{2} \ddot{\theta}_{1} \cos{\left(\theta_{2} \right)} + l_{1} l_{2} m_{2} \ddot{\theta}_{2} \cos{\left(\theta_{2} \right)} - 2 l_{1} l_{2} m_{2} \dot{\theta}_{1} \dot{\theta}_{2} \sin{\left(\theta_{2} \right)} - l_{1} l_{2} m_{2} \dot{\theta}_{2}^{2} \sin{\left(\theta_{2} \right)} + l_{2}^{2} m_{2} \ddot{\theta}_{1} + l_{2}^{2} m_{2} \ddot{\theta}_{2}\\l_{2} m_{2} \left(g \cos{\left(\theta_{1} + \theta_{2} \right)} + l_{1} \ddot{\theta}_{1} \cos{\left(\theta_{2} \right)} + l_{1} \dot{\theta}_{1}^{2} \sin{\left(\theta_{2} \right)} + l_{1} \dot{\theta}_{1} \dot{\theta}_{2} \sin{\left(\theta_{2} \right)} + l_{2} \ddot{\theta}_{1} + l_{2} \ddot{\theta}_{2}\right)\end{matrix}\right]$

```python
s.simplify(T01@T12@M)
```

    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The to_png function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The to_rgba function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The to_mask function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    C:\Users\cdi\anaconda3\lib\site-packages\IPython\lib\latextools.py:126: MatplotlibDeprecationWarning:
    The MathtextBackendBitmap class was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)

$\displaystyle \left[\begin{matrix}\cos{\left(\theta_{1} + \theta_{2} \right)} & - \sin{\left(\theta_{1} + \theta_{2} \right)} & 0 & l_{1} \cos{\left(\theta_{1} \right)} + l_{2} \cos{\left(\theta_{1} + \theta_{2} \right)}\\\sin{\left(\theta_{1} + \theta_{2} \right)} & \cos{\left(\theta_{1} + \theta_{2} \right)} & 0 & l_{1} \sin{\left(\theta_{1} \right)} + l_{2} \sin{\left(\theta_{1} + \theta_{2} \right)}\\0 & 0 & 1 & 0\\0 & 0 & 0 & 1\end{matrix}\right]$

```python
T01 = smr.MatrixExp6(smr.VecTose3(Blist[:, 0]*thetalist[0]))
T12 = smr.MatrixExp6(smr.VecTose3(Blist[:, 1]*thetalist[1]))
s.simplify(M@T01@T12)
```

$\displaystyle \left[\begin{matrix}\cos{\left(\theta_{1} + \theta_{2} \right)} & - \sin{\left(\theta_{1} + \theta_{2} \right)} & 0 & l_{1} \cos{\left(\theta_{1} \right)} + l_{2} \cos{\left(\theta_{1} + \theta_{2} \right)}\\\sin{\left(\theta_{1} + \theta_{2} \right)} & \cos{\left(\theta_{1} + \theta_{2} \right)} & 0 & l_{1} \sin{\left(\theta_{1} \right)} + l_{2} \sin{\left(\theta_{1} + \theta_{2} \right)}\\0 & 0 & 1 & 0\\0 & 0 & 0 & 1\end{matrix}\right]$

```python

```

```python

```

```python
smr.get_EoM_from_T(tau, ddthetalist, g)
```

$\displaystyle \left( \left[\begin{matrix}l_{1}^{2} m_{1} + l_{1}^{2} m_{2} + 2 l_{1} l_{2} m_{2} \cos{\left(\theta_{2} \right)} + l_{2}^{2} m_{2} & l_{2} m_{2} \left(l_{1} \cos{\left(\theta_{2} \right)} + l_{2}\right)\\l_{2} m_{2} \left(l_{1} \cos{\left(\theta_{2} \right)} + l_{2}\right) & l_{2}^{2} m_{2}\end{matrix}\right], \  \left[\begin{matrix}l_{1} m_{2} \dot{\theta}_{2} \left(l_{1} \dot{\theta}_{1} - 2 l_{2} \dot{\theta}_{1} - l_{2} \dot{\theta}_{2}\right) \sin{\left(\theta_{2} \right)}\\l_{1} l_{2} m_{2} \dot{\theta}_{1} \left(\dot{\theta}_{1} + \dot{\theta}_{2}\right) \sin{\left(\theta_{2} \right)}\end{matrix}\right], \  \left[\begin{matrix}g \left(l_{1} m_{1} \cos{\left(\theta_{1} \right)} + l_{1} m_{2} \cos{\left(\theta_{1} \right)} + l_{2} m_{2} \cos{\left(\theta_{1} + \theta_{2} \right)}\right)\\g l_{2} m_{2} \cos{\left(\theta_{1} + \theta_{2} \right)}\end{matrix}\right]\right)$

```python
A_rot = s.Matrix([0, 0, 1, 0, 0, 0])
A_pris = s.Matrix([0, 0, 0, 0, 0, 1])
```

```python
import modern_robotics as mr
import numpy as np
dir(mr)
```

    ['Adjoint',
     'AxisAng3',
     'AxisAng6',
     'CartesianTrajectory',
     'ComputedTorque',
     'CubicTimeScaling',
     'DistanceToSE3',
     'DistanceToSO3',
     'EndEffectorForces',
     'EulerStep',
     'FKinBody',
     'FKinSpace',
     'ForwardDynamics',
     'ForwardDynamicsTrajectory',
     'GravityForces',
     'IKinBody',
     'IKinSpace',
     'InverseDynamics',
     'InverseDynamicsTrajectory',
     'JacobianBody',
     'JacobianSpace',
     'JointTrajectory',
     'MassMatrix',
     'MatrixExp3',
     'MatrixExp6',
     'MatrixLog3',
     'MatrixLog6',
     'NearZero',
     'Normalize',
     'ProjectToSE3',
     'ProjectToSO3',
     'QuinticTimeScaling',
     'RotInv',
     'RpToTrans',
     'ScrewToAxis',
     'ScrewTrajectory',
     'SimulateControl',
     'TestIfSE3',
     'TestIfSO3',
     'TransInv',
     'TransToRp',
     'VecTose3',
     'VecToso3',
     'VelQuadraticForces',
     '__builtins__',
     '__cached__',
     '__doc__',
     '__file__',
     '__loader__',
     '__name__',
     '__package__',
     '__path__',
     '__spec__',
     '__version__',
     'ad',
     'core',
     'np',
     'print_function',
     'se3ToVec',
     'so3ToVec']

```python

```


# REGRESION LINEAL MULTIVARIABLE

En este documento se implementará el algoritmo de gradiente descendiente para calcular los precios de una casa dependiendo de su tamaño en pies y número de habitaciones.

```python
import sympy as sym #libreria de matematica simbolica
import matplotlib.pyplot as plt
import csv

%matplotlib inline

#creo mi variable X la cual va ser una lista de listas donde
# la lista X[i] representa las variables en un ejemplo de entrenamiento
X= []
Y = []
n = 2 #en este ejemplo solo trabajamos con dos variables
with open('./data/dataHouses.csv') as csvarchivo:
    #['Size', 'Bedrooms', 'Price']
    entrada = csv.reader(csvarchivo)
    for reg in entrada:
        l = []
        l.append(1)# x[0] será 1 para poder ajustar las operaciones
        l.append(int(reg[0]))
        l.append(int(reg[1]))
        X.append(l)
        Y.append(int(reg[2]))

m = len(Y)#cantidad de ejemplos para el entrenamiento
```

Ahora procederemos a crear nuestra hipotesis y una funcion para evaluar esta con un x[i] dado

```python
#necesitamos crear las variables
x1,x2 = sym.symbols('x1 x2', real = True)
#Note que no se crea X0 por que esta es igual a 1

Tethas = [500,10,10] #inicio en cualquier valor.
#este arreglo se va ir modificando a medida que corra el algoritmo

def MakeHipotesis(Tet, Var1, Var2):
    '''Se encarga de crear la fucion de costo con '''
    H = Tet[0] + Tet[1]*Var1 + Tet[2]*Var2
    return H

Hipotesis = MakeHipotesis(Tethas, x1, x2)
sym.pprint(Hipotesis) #mostramos la ecuacion

def CalcHipotesis(H,Xi):
    return Hipotesis.subs({x1:Xi[1], x2:Xi[2]})#si quiero evaluarla 0

print(CalcHipotesis(Hipotesis, [1,2,2]))
```

    100⋅x₁ + 100⋅x₂ + 100
    500

Ahora se va definir una funcion que calcule el error cuadratico medio con el conjunto de prueba

```python

def CostFunction(H,X,Y):
    '''funcion que calcula el error cuadratico medio'''
    res = 0
    for i in range(0,m):
        res += (1/(2*m)) * ((CalcHipotesis(H,X[i])- Y[i]))**2
    return res

print(CostFunction(Hipotesis,X,Y))
```

    12215586187.3085

Ahora vamos a crear el algoritmo del gradiente descendiente para una sola iteracion y vamos  a validar que la funcion de costo disminuya su valor

```python
def GradienteDes(H, Thetas, X, Y, alpha):

    for theta in range(len(Thetas)):

        #desplazo Thetha[i]
        calcderivada = 0 #resultado del calculo
        for i in range(m):

            #calculo la cantidad que voy a restarle al valor actual de theha
            calcderivada += (1/m) * (CalcHipotesis(H,X[i]) - Y[i] ) * X[i][theta]

        #calculo el nuevo thetha
        Thetas[theta] = Thetas[theta] - (alpha * calcderivada)

```

Ahora se procede a ejecutar el algoritmo del gradiente varias veces, para acercar los valores de Theta a un minimo

```python
Costos = []
#ejecutamos el gradiente varias veces para calcular los valores de Thetha mas adecuados
for i in range(0,500):
    GradienteDes(Hipotesis,Tethas,X,Y,0.001)
    Hipotesis = MakeHipotesis(Tethas, x1, x2)
    Costos.append(CostFunction(Hipotesis,X,Y))
```

Observamos el valor que ha tomado la hipotesis

```python
#mostamos como esta la hipotesis en este instate
sym.pprint(Hipotesis) #mostramos la ecuacion
print('costo',CostFunction(Hipotesis,X,Y))
```

    -1.34308589107834e+1834⋅x₁ - 1.93986135140195e+1831⋅x₂ - 5.81514997541167e+183
    0
    costo 4.16773729248513e+3674

```python

iterCostos = range(len(Costos))
plt.figure()
#plt.title('h_theta(x), theta_0=50000 and theta_1=60', fontsize=15)
plt.xlabel('Iteraciones', fontsize=15)
plt.ylabel('Costos', fontsize=15)
#plt.scatter(X,Y,color='red',marker='x')
plt.plot(iterCostos,Costos)
```

Ahora procedemos a quitar el sesgo que existe con la forma mas basica, el NuevoX1 = X1/2000, NuevoX2 = X2/5, siendo 5 la casa con mas habitaciones, y 2000 podría ser el tamaño mas grande, en este caso no lo es pero igual lo tomaremos

```python
nuevoX = []#un arreglo para guardar las variables transformadas
nuevoThethas = [500,10,10]
for i in X:

    nuevoX.append([i[0], i[1]/2000, i[2]/5]) #hacemos la escala

#generamos la nuevo hipotesis
NHipotesis = MakeHipotesis(nuevoThethas, x1, x2)
sym.pprint(NHipotesis)
```

    100⋅x₁ + 100⋅x₂ + 100

Ahora ejecutaremos el algoritmo del gradiente para este nuevo conjunto de entrenamiento mejorado

```python
Costos = []
#ejecutamos el gradiente varias veces para calcular los valores de Thetha mas adecuados
for i in range(0,500):
    GradienteDes(NHipotesis,nuevoThethas,nuevoX,Y,0.001)
    NHipotesis = MakeHipotesis(nuevoThethas, x1, x2)
    Costos.append(CostFunction(NHipotesis,nuevoX,Y))
```

Ahora visualizaremos el valor de la funcion de costo con las iteraciones.

```python

iterCostos = range(len(Costos))
plt.figure()
#plt.title('h_theta(x), theta_0=50000 and theta_1=60', fontsize=15)
plt.xlabel('Iteraciones', fontsize=15)
plt.ylabel('Costos', fontsize=15)
#plt.scatter(X,Y,color='red',marker='x')
plt.plot(iterCostos,Costos)
```

```python
print(Costos[len(Costos) - 1])
```

    1.04445140896033e+3668

```python

```

.ipynb_checkpoints/Regresion lineal multiples variables-checkpoint.ipynb


# Nearest Neighbor Used-based Collaborative Filtering

The most basic implementation of the classic collaborative filtering algorithm.

| Description | See [Introduction to Algorithmic Marketing](https://algorithmicweb.wordpress.com/ ) book |
|--|:--|
| Dataset | Synthetic, no external dependencies |
| Libs | Sympy, Numpy |

```python
%matplotlib inline
import sympy as sy
import numpy as np
import matplotlib.pyplot as plt
```

```python
# Input rating matrix
# X stands for unknown ratings
X = None
R = np.array([
    [5, 4, X, 1, 2, 1],
    [4, X, 3, 1, 1, 2],
    [X, 5, 5, X, 3, 3],
    [2, X, 1, 4, 5, 4],
    [2, 2, 2, X, 4, X],
    [1, 2, 1, X, 5, 4]
])
```

```python
# Define common varibales and helper functions
n, m = R.shape
def user_common_ratings(R, u, v):
    return np.array(list(filter(
        lambda r: all(val is not None for val in r),
        np.column_stack((R[u,:], R[v,:]))
    ))).T

def known_user_ratings(R, u):
    return [x for x in R[u,:] if x is not None]

def known(r):
    return r is not None

def mean_r(R, u):
    return np.mean(known_user_ratings(R, u))
```

```python
# Calculate the matrix of user similarities
def similarity(R, u, v):
    I_uv = user_common_ratings(R, u, v)
    mu_u = mean_r(R, u)
    mu_v = mean_r(R, v)
    return sum([
          ((I_uv[0,i] - mu_u)*(I_uv[1,i] - mu_v)) /
          (np.linalg.norm(I_uv[0,:] - mu_u) * np.linalg.norm(I_uv[1,:] - mu_v))
          for i in range(I_uv.shape[1])])

user_similarity = np.array([[
    similarity(R, u, v)
    for u in range(n)] for v in range(n)])
```

```python
print(user_similarity)
```

    [[ 1.     0.875  0.941 -0.797 -0.594 -0.786]
     [ 0.875  1.     0.872 -0.84  -0.81  -0.882]
     [ 0.941  0.872  1.    -0.938 -0.87  -0.92 ]
     [-0.797 -0.84  -0.938  1.     0.86   0.953]
     [-0.594 -0.81  -0.87   0.86   1.     0.947]
     [-0.786 -0.882 -0.92   0.953  0.947  1.   ]]

```python
# Predict ratings based on the user similarities
k = 2 # neighborhood size

def predict_rating(R, u, i):
    # neighbors sorted by similarity
    all_neighbors = np.argsort(user_similarity[u])[::-1]
    # remove neighbors without ratings for i and select top k
    neighbors = list(filter(lambda v: known(R[v,i]) and not v==u, all_neighbors))[:k]
    mu_u = mean_r(R, u)
    score = 0
    norm = 0
    print("user %s, item %s <- user neighbors %s" % (u, i, neighbors))
    for v in neighbors:
        mu_v = mean_r(R, v)
        score = score + user_similarity[u,v]*(R[v,i] - mu_v)
        norm = norm + abs(user_similarity[u,v])
    return mu_u + score/norm

ratings = np.array([[ R[u,i] if known(R[u,i]) else predict_rating(R, u, i)
   for i in range(m)] for u in range(n)])

print("\nComplete rating matrix:")
np.set_printoptions(precision=2)
print(ratings)
```

    user 0, item 2 <- user neighbors [2, 1]
    user 1, item 1 <- user neighbors [0, 2]
    user 2, item 0 <- user neighbors [0, 1]
    user 2, item 3 <- user neighbors [0, 1]
    user 3, item 1 <- user neighbors [5, 4]
    user 4, item 3 <- user neighbors [3, 0]
    user 4, item 5 <- user neighbors [5, 3]
    user 5, item 3 <- user neighbors [3, 0]

    Complete rating matrix:
    [[ 5.    4.    3.59  1.    2.    1.  ]
     [ 4.    3.57  3.    1.    1.    2.  ]
     [ 6.33  5.    5.    2.45  3.    3.  ]
     [ 2.    2.59  1.    4.    5.    4.  ]
     [ 2.    2.    2.    4.    4.    3.73]
     [ 1.    2.    1.    3.92  5.    4.  ]]


# 1. 2D Linear Diffusion

We consider the 1d linear Convection equation, under a constant velocity

$$
\partial_t u = \nu \nabla^2 u
$$

```python
# needed imports
from numpy import zeros, ones, linspace, zeros_like
from matplotlib.pyplot import plot, contourf, show, colorbar
%matplotlib inline
```

```python
# Initial condition
import numpy as np
u0 = lambda x,y: np.exp(-(x-.3)**2/.05**2)*np.exp(-(y-.3)**2/.05**2)

ts = linspace(0., 1., 401)
x,y = np.meshgrid(ts,ts)
u = u0(x,y)
```

```python
contourf(x,y, u); colorbar() ; show()
```

### Time scheme

$$\frac{u^{n+1}-u^n}{\Delta t} - \nu \nabla^2 u^{n+1} = 0 $$

$$ \left(I - \nu \Delta t \nabla^2 \right) u^{n+1} = u^n $$

### Weak formulation

$$
\langle v, u^{n+1} \rangle + \nu \Delta t ~ \langle \nabla v, \nabla u^{n+1} \rangle = \langle v, u^n \rangle
$$

expending $u^n$ over the fem basis, we get the linear system

$$A U^{n+1} = M U^n$$

where

$$
M_{ij} = \langle b_i, b_j \rangle
$$

$$
A_{ij} = \langle b_i, b_j \rangle + \nu \Delta t ~ \langle \nabla b_i,  \nabla b_j \rangle
$$

## Abstract Model using SymPDE

```python
from sympde.core import Constant
from sympde.expr import BilinearForm, LinearForm, integral
from sympde.topology import ScalarFunctionSpace, Square, element_of
from sympde.calculus import grad, dot
```

```python
# ... abstract model
domain = Square()

V = ScalarFunctionSpace('V', domain)

x,y = domain.coordinates

u,v = [element_of(V, name=i) for i in ['u', 'v']]

nu = Constant('nu')
dt = Constant('dt')

# bilinear form
expr = v*u + nu*dt* dot(grad(v), grad(u))
a = BilinearForm((u,v), integral(domain , expr))

# bilinear form for the mass matrix
expr = u*v
m  = BilinearForm((u,v), integral(domain , expr))

# linear form for initial condition
from sympy import exp
expr = exp(-(x-.3)**2/.05**2)*exp(-(y-.3)**2/.05**2)*v
l = LinearForm(v, integral(domain, expr))
```

## Discretization using Psydac

```python
from psydac.api.discretization import discretize
```

```python
nu = 0.3   # viscosity
T = 0.02  # T final time
dt = 0.001
niter = int(T / dt)

degree = [3,3]    # spline degree
ncells = [64,64]   # number of elements
```

```python
# Create computational domain from topological domain
domain_h = discretize(domain, ncells=ncells, comm=None)

# Discrete spaces
Vh = discretize(V, domain_h, degree=degree)

# Discretize the bilinear forms
ah = discretize(a, domain_h, [Vh, Vh])
mh = discretize(m, domain_h, [Vh, Vh])

# Discretize the linear form for the initial condition
lh = discretize(l, domain_h, Vh)
```

```python
# assemble matrices and convert them to scipy
M = mh.assemble().tosparse()
A = ah.assemble(nu=nu, dt=dt).tosparse()

# assemble the rhs and convert it to numpy array
rhs = lh.assemble().toarray()
```

```python
from scipy.sparse.linalg import cg, gmres
```

```python
# L2 projection of the initial condition
un, status = cg(M, rhs, tol=1.e-8, maxiter=5000)
```

```python
from utilities.plot import plot_field_2d

nbasis = [W.nbasis for W in Vh.spaces]
plot_field_2d(Vh.knots, Vh.degree, un.reshape(nbasis)) ; colorbar() ; show()
```

```python
for i in range(0, niter):
    b = M.dot(un)
    un, status = gmres(A, b, tol=1.e-8, maxiter=5000)
```

```python
nbasis = [W.nbasis for W in Vh.spaces]
plot_field_2d(Vh.knots, Vh.degree, un.reshape(nbasis)) ; colorbar() ; show()
```

```python

```

```python

```


Markdown macros

$\newcommand{\I}{\mathrm{i}}
\newcommand{\E}{\mathrm{e}}
\newcommand{\D}{\mathop{}\!\mathrm{d}}
\newcommand{\bra}[1]{\langle{#1}|}
\newcommand{\ket}[1]{|{#1}\rangle}
\newcommand{\braket}[1]{\langle{#1}\rangle}
\newcommand{\bm}[1]{\boldsymbol{#1}}$

# Imports

```python
# numpy
import numpy as np
```

```python
# matplotlib
import matplotlib
import matplotlib.pylab as plt
```

```python
%matplotlib inline
from IPython.display import Image
```

```python
# sympy
import sympy as sy
sy.init_printing(use_unicode = True)
```

# Rotation d'une barre inclinée

```python
Image(filename='barre_z.png', width=300)
```

Une barre de masse $m$ et de longueur $l$ est fixée au point $O=(0,0,0)$ d'un repère fixe $(X,Y,Z)$. Elle est inclinée d'un angle $\theta=\mathrm{const.}$, tenue par une barre horizontale (parallèle au plan $XY$), et tourne autour de l'axe $Z$.

On note $\theta,\phi$ les angles des coordonnées sphériques. La barre tourne à vitesse angulaire constante $\dot\phi=\omega_0$.

## Cinématique
Le centre de masse se situe à une distance $l/2$ de l'origine $O$, ses coordonnées sphériques sont $(l/2,\theta,\phi)$. On note
$$\bm n = \cos\phi \hat{\bm X} + \sin\phi \hat{\bm Y}$$
la projection sur le plan $XY$ de la direction du centre de masse.

Les axes du repère mobile sont (voir figure):
\begin{align}
\hat{\bm x} &= \cos\theta \bm n - \sin\theta \hat{\bm Z} \\
\hat{\bm y} &= -\sin\phi \hat{\bm X} + \cos\phi \hat{\bm Y}\\
\hat{\bm z} &= \sin\theta \bm n + \cos\theta \hat{\bm Z}
\end{align}
La matrice de rotation du repère mobile par rapport au repère fixe est donc,
$$R(\theta,\phi) = \begin{pmatrix}
\cos\theta \cos\phi & -\sin\phi & \sin\theta \cos\phi \\
\cos\theta \sin\phi & \cos\phi & \sin\theta \sin\phi \\
-\sin\theta & 0 & \cos\theta \end{pmatrix}$$

Notez que la matrice de rotation peut se décomposer selon la formule
$$R(\theta,\phi) = R_Z(\phi)R_Y(\theta)$$

Le vecteur centre de masse est
$$\bm R = \frac{l}{2}(\sin\theta \bm n + \cos\theta \hat{\bm Z})$$
et le moment d'inertie (par rapport au centre de masse) est
$$I_x = I_y = I_l = \frac{ml^2}{12}$$
La vitesse angulaire est
$$\bm \omega = \dot\phi \hat{\bm Z} = \omega_0 \hat{\bm Z}$$
La vitesse du centre de masse est due à la rotation (constante):
$$\bm V = \bm \omega \times \bm R = \frac{l\omega_0}{2}\sin\theta \hat{\bm y}$$
(dans le repère mobile). L'accelération est
$$\dot{\bm V} =  -\frac{l\omega_0^2}{2}\sin\theta \bm n$$
(puisque $\dot{\hat{\bm y}}=-\omega_0\bm n$)

### Moment cinétique

```python
# matrices de rotation

phi, theta, psi = sy.symbols("phi, theta, psi", real=True)

def R_x(phi):
    c = sy.cos(phi)
    s = sy.sin(phi)
    return sy.Matrix([[1,  0, 0],
                  [0, c, -s],
                  [0, s,  c]])
def R_y(phi):
    c = sy.cos(phi)
    s = sy.sin(phi)
    return sy.Matrix([[ c, 0, s],
                  [ 0, 1, 0],
                  [-s, 0, c]])

def R_z(phi):
    c = sy.cos(phi)
    s = sy.sin(phi)
    return sy.Matrix([[c, -s, 0],
                  [s,  c, 0],
                  [0,  0, 1]])

def XYZ():
    return sy.Matrix([1,0,0]), sy.Matrix([0,1,0]), sy.Matrix([0,0,1])
```

La rotation de la barre est décrite par une rotation autour de $Y$ d'angle $\theta$ suivie d'une rotation autour de $Z$ d'angle $\phi$:

```python
R = R_z(phi)*R_y(theta)
R
```

Le paramètres sont

```python
m, g, l, omega_0 = sy.symbols("m, g, l, omega_0", real=True)
```

Les données cinétiques sont

```python
# cinétique
X, Y, Z = XYZ()
n = sy.cos(phi)*X + sy.sin(phi)*Y
y = R*Y
omega = omega_0*Z
r = (l/2)*(sy.sin(theta)*n + sy.cos(theta)*Z)
V = omega.cross(r)
a = -(l/2)*omega_0**2 * sy.sin(theta)*n
```

Le point délicat est le calcul du moment cinétique $\bm L$
$$\bm L = m\bm R \times \bm V + I\bm \omega$$
et de sa dérivée par rapport au temps
$$\dot{\bm L} = m\bm R \times \dot{\bm V} + \bm\omega \times (I \bm\omega)$$
Le problème est qu'on connaît les variables cinétiques dans le repère fixe, mais on a l'expression du moment d'inertie dans le centre de masse:
$$I = \frac{ml^2}{12}\mathrm{diag}(1,1,0)$$
Pour reconcilier les deux on doit choisir le repère pour exprimer $\bm L$. Dans le cas présent, il convient de transformer $I$ dans le repère universel.

On transforme le tenseur d'inertie "centre de masse" en tenseur "fixe":
$$I_0 = RIR^T$$
à l'aide donc de la matrice de rotation.

Un calcul plus simple est possible en utilisant la transformation de $\bm \omega$ dans le repère mobile. On expose cette autre méthode dans une section plus bas.

```python
R
```

```python
# tenseur I au repère fixe
RIR = R*sy.Matrix([[1,0,0],[0,1,0],[0,0,0]])*R.T
I_0 = (m*l**2/12)*RIR
RIR
```

```python
sy.expand(RIR.subs(sy.cos(theta)**2,1-sy.sin(theta)**2)).subs(sy.sin(phi)**2+sy.cos(phi)**2,1)
```

La partie centre de masse donne:
$$ m\bm R \times \dot{\bm V} = -\frac{m\omega_0^2l^2}{4}\sin\theta\cos\theta \hat{\bm y}$$
et la partie de rotation donne:
$$\bm \omega \times (I\bm \omega) = -\frac{m\omega_0^2l^2}{12}\sin\theta \cos\theta\hat{\bm y}$$
On a en définitive:
$$\dot{\bm L} = -\frac{m\omega_0^2l^2}{3}\sin\theta \cos\theta\hat{\bm y}$$

```python
m*r.cross(a)
```

```python
omega.cross(I_0*omega)
```

```python
m*r.cross(a) + omega.cross(I_0*omega)
```

## Forces

La force totale (gravité et contraintes) est
$$\bm F = -mg \hat{\bm Z} + f_1 \hat{\bm Z} + f_2 \bm n - f_3 \bm n$$

```python
f_1,f_2, f_3 = sy.symbols("f_1,f_2, f_3", real=True)
```

```python
# force
F = -m*g*Z + f_1*Z + f_2*n - f_3*n
sy.simplify(F-m*a)
```

On obtient les équations pour $f_1$ et $f_2$ à partir de l'équation de Newton $\bm F = m \dot{\bm V}$:
$$f_1 = mg$$
et
$$f_2 = f_3 - \frac{ml\omega_0^2}{2}\sin\theta$$

## Moment des forces

Le poids $mg$ et la corde $f_3$ exercent de moments:
$$\bm M = \frac{3l}{4}\hat{\bm z} \times (-\bm f_3) + \frac{l}{2}\hat{\bm z} \times (-mg)\hat{\bm Z}$$
ce qui donne:
$$\bm M = \frac{mgl}{2} \sin\theta \hat{\bm y} - \frac{3lf_3}{4} \cos\theta \hat{\bm y}$$
On se rappelle que dans le repère fixe on $\hat{\bm y} = -\sin\phi\hat{\bm X} + \cos\phi \hat{\bm Y}$.

```python
z = R*Z
z.cross(n)
```

```python
sy.simplify(-3*l*f_3/4 * z.cross(n) + l/2*(-m*g) * z.cross(Z))
```

### Équation d'Euler

L'équation d'Euler pour le moment cinétique est
$$\dot{\bm L} = \bm R \times (m\dot{\bm V}) + \bm \omega\times(I_0 \bm \omega) = \bm M$$
où on a utilisé que le terme d'accelération angulaire $\dot{\bm \omega}$ est nul.

On obtient une équation pour la force $\bm f_3$ (les composantes $X$ et $Y$ de l'équation donnenet la même solution):
$$\frac{ml^2 \omega_0^2}{12} \cos\theta\sin\theta\sin\phi = \left(\frac{mgl}{2} \sin\theta -  \frac{3l^2}{4} f_3\cos\theta\right)\sin\phi$$
ou en dégageant $f_3$:
$$f_3 = \frac{2mg}{3} \tan\theta + \frac{4m\omega_0^2l^2}{9}\sin\theta$$
Les autres forces sont
$$f_1 = mg$$
et
$$f_2 = \frac{2mg}{3} \tan\theta - \frac{m\omega_0^2l^2}{18}\sin\theta$$

## Calcul dans le repère du corps

Comme dit un peu plus haut il est possible de calculer $\dot{\bm L}$ en utilisant l'expression de $\bm \omega$ dans le repère mobile. En effet, on se rappelle que le produit vectoriel de deux vecteurs $\bm u \times \bm v$ peut s'écire en forme matricielle:
$$\bm u \times \bm v = A(\bm u)\bm v$$
où la matrice antisymétrique $A(\bm u$ est définie par
$$A(\bm u) = \begin{pmatrix} 0 & -u_z & u_y \\ u_z & 0 & -u_x \\ -u_y & u_x & 0 \end{pmatrix}$$
(on avait noté cette matrice un peu différemment, avec la lettre L, ce qui risque de se confondre avec le moment cinétique).

Reprenons l'expression de la parite angulaire variation du moment cinétique dans le repère fixe $\bm L_\omega =\omega \times RIR^T\omega$, et utilisons la transformation $R$ pour la réécrire en changeant la base:
\begin{align}
R^T\bm L_\omega &= R^T\bm \omega \times RIR^T \bm \omega \\
&= R^T A(\bm \omega) RIR^T \bm \omega \\
&= R^T \big(A(\bm \omega) R\big) I \big(R^T \bm \omega\big)\\
& = \big(R^T\bm \omega\big) \times  I \big(R^T\bm \omega\big) \\
& = \bm \Omega \times I \bm \Omega
\end{align}
où on a utilisé $R^T A(\bm \omega) R= R^T \bm \omega \times$ et $\bm \Omega = R^T\omega$.

Cette formule est plus facile à calculer parce qu'elle ne comporte pas la transformation du moment d'inertie au repère fixe (tournant vu depuis le corps).

```python
o = R.T*omega
o
```

```python
Icorps = (m*l**2/12)*sy.Matrix([[1,0,0],[0,1,0],[0,0,0]])
lo = sy.simplify(o.cross(Icorps*o))
lo
```

On fait de même avec le terme du centre de masse:
$$R^T \big(m\bm R \times \dot{\bm V}\big) = -m\big(R^T\bm R\big) \times \big(R^T \dot{\bm V}\big)$$

```python
lv = sy.simplify(m*(R.T*r).cross(R.T*a))
lv
```

```python
lo+lv
```

On obtient bien le résultat précédent:
\begin{align}
\dot{\bm L} &= m\bm R \times \dot{\bm V} + \bm\omega \times (I \bm\omega)\\
&=-\frac{m\omega_0^2l^2}{8}\sin2\theta\hat{\bm y} - \frac{m\omega_0^2l^2}{24}\sin2\theta\hat{\bm y}\\
&= -\frac{m\omega_0^2l^2}{6}\sin2\theta\hat{\bm y}
\end{align}

```python

```

# Code dessin

Le dessin de la barre utilise le package ["asymptote"](https://asymptote.sourceforge.io/index.html), lequel fait partie de $\TeX$. Il s'agit d'un langage statique, apparenté à C++. La compilation se fait par la commande `asy`:

`asy -f pdf fichier.asy`

où l'option `-f` force une sortie pdf (ou png ou svg, etc).

Voici le code (fichier "barre_z.asy"):

```cpp
import settings;
render = 4;
prc = false;

import three;
import solids;
texpreamble("\usepackage{bm}");

size(250, 0);

currentprojection  =  perspective(9, 3, 3);

pen pk = rgb(0, 0, 0);
pen pr = rgb(0.5, 0.3, 0.3);
pen pg = rgb(0.3, 0.3, 0.3);

// axes fixes
draw(Label("$X$", 1), O--X, pk, Arrow3);
draw(Label("$Y$", 1), O--Y, pk, Arrow3);
draw(Label("$Z$", 1), O--Z, pk, Arrow3);
label("$O$", O, 2*W);

// barre
real l = 1.6;
real a = 0.05;
real t = 45;
real theta = t*pi/180;
real q = 60;
real phi = q*pi/180;
triple ax = (sin(theta)*cos(phi), sin(theta)*sin(phi), cos(theta));
revolution C = cylinder(O, a, l, ax);

draw(surface(C),  gray(0.95)+opacity(0.2));
draw(C, 1bp+gray(0.7));
draw(O--l*ax, pr+opacity(0.5));

// axes mobiles (corps)
real b = 0.3;
triple r = b*(cos(phi)*X + sin(phi)*Y);
triple R = l/2*ax;
triple x = R + b*(cos(phi)*X + sin(phi)*Y)*cos(theta) - b*sin(theta)*Z;
triple y = R + b*(-sin(theta)*X + cos(theta)*Y);
triple z = R + b*ax;

draw(Label("$x$", 1, W), R--x, pg, Arrow3);
draw(Label("$y$", 1), R--y, pg, Arrow3);
draw(Label("$z$", EndPoint, align = E), R--z, pg, Arrow3);
label("$l$", l*ax+(0, -0.15, 0.0), pr+opacity(0.5));
label("$l/2$", 0.5*l*ax+(0, -0.1, 0.1), pr);

// rotation (arc de cercle)
//draw(arc(O, l/2, t, q, t, q+20),  pr);
draw(circle(O+l/2*cos(theta)*Z, l/2*sin(theta), Z), pk);
draw("$\phi$", arc(O, 0.6*b, 90, 0, 90, q),  pr, Arrow3);
draw(Label("$\theta$", N), arc(O, 0.6*b, 0, q, t, q),  pr, Arrow3);

// forces
real c = 0.4;
draw(Label("$\bm f_1$", 1, W), O--c*Z, pr, Arrow3);
draw(Label("$\bm f_2$", 1, S), O--c*(cos(phi), sin(phi), 0), pr, Arrow3);
draw(Label("$mg \hat{\bm Z}$", 1, W), R--(R-c*Z), pr, Arrow3);

// corde
real d = 3*l/4;
draw(Label("$3l/4$", EndPoint, 4*E, pr), (0,0,d*cos(theta))--(d*sin(theta)*cos(phi),d*sin(theta)*sin(phi),d*cos(theta)), 2bp+pk);
draw("$f_3$", (0,0,d*cos(theta))-- (-c*cos(phi),-c*sin(phi),d*cos(theta)), pr, Arrow3);
```

```python

```


# QGS model: Manually setting the basis and the inner products definition

## Atmosphere coupled to an ocean with special boundary conditions

In this notebook, we describe how to setup a user-defined basis for one of the model’s component. We will do it for the ocean, but the approach is similar for the other components. We will project the ocean equations on four modes proposed in

* S. Pierini. *Low-frequency variability, coherence resonance, and phase selection in a low-order model of the wind-driven ocean circulation.* Journal of Physical Oceanography, **41**(9):1585–1604, 2011. [doi:10.1175/JPO-D-10-05018.1](https://journals.ametsoc.org/doi/full/10.1175/JPO-D-10-05018.1).

* S. Vannitsem and L. De Cruz. *A 24-variable low-order coupled ocean–atmosphere model: OA-QG-WS v2*. Geoscientific Model Development, 7(2):649–662, 2014. [doi:10.5194/gmd-7-649-2014](https://doi.org/10.5194/gmd-7-649-2014).

These four modes are given by

* $\tilde\phi_1(x,y)=2 \, e^{−\alpha x} \sin(n x/2) \sin(y)$
* $\tilde\phi_2(x,y)=2 \, e^{−\alpha x} \sin(n x) \sin(y)$
* $\tilde\phi_3(x,y)=2 \, e^{−\alpha x} \sin(n x/2) \sin(2y)$
* $\tilde\phi_4(x,y)=2 \, e^{−\alpha x} \sin(n x) \sin(2y)$

The exponential factors represent an intensification of the flows, forcings and heat exchanges in the western part of the domain.
This ocean is then connected to a channel atmosphere using a symbolic basis of functions.

### Warning !

This initialization method is not yet well-defined in qgs. It builds the model block by block to construct an ad-hoc model version. It is an advanced and somewhat experimental feature of the model.

## Modules import

First, setting the path and loading of some modules

```python
import sys, os, warnings
```

```python
sys.path.extend([os.path.abspath('../')])
```

```python
import numpy as np
import matplotlib.pyplot as plt
from numba import njit
```

```python
from matplotlib import rc
rc('font',**{'family':'serif','sans-serif':['Times'],'size':12})
```

Initializing the random number generator (for reproducibility). -- Disable if needed.

```python
np.random.seed(210217)
```

Importing the model's modules

```python
from qgs.params.params import QgParams
from qgs.basis.base import SymbolicBasis
from qgs.inner_products.definition import StandardSymbolicInnerProductDefinition
from qgs.inner_products.symbolic import AtmosphericSymbolicInnerProducts, OceanicSymbolicInnerProducts
from qgs.tensors.qgtensor import QgsTensor
from qgs.functions.sparse_mul import sparse_mul3
from qgs.integrators.integrator import RungeKuttaIntegrator
```

and diagnostics

```python
from qgs.diagnostics.streamfunctions import MiddleAtmosphericStreamfunctionDiagnostic, OceanicLayerStreamfunctionDiagnostic
from qgs.diagnostics.temperatures import MiddleAtmosphericTemperatureDiagnostic, OceanicLayerTemperatureDiagnostic
from qgs.diagnostics.variables import VariablesDiagnostic, GeopotentialHeightDifferenceDiagnostic
from qgs.diagnostics.multi import MultiDiagnostic
```

and some SymPy functions

```python
from sympy import symbols, sin, exp, pi
```

## Systems definition

General parameters

```python
# Time parameters
dt = 0.1
# Saving the model state n steps
write_steps = 100

number_of_trajectories = 1
```

Setting some model parameters and setting the atmosphere basis

```python
# Model parameters instantiation with some non-default specs
model_parameters = QgParams({'n': 1.5})

# Mode truncation at the wavenumber 2 in both x and y spatial
# coordinates for the atmosphere
model_parameters.set_atmospheric_channel_fourier_modes(2, 2, mode="symbolic")

```

Creating the ocean basis

```python
ocean_basis = SymbolicBasis()
x, y = symbols('x y')  # x and y coordinates on the model's spatial domain
n, al = symbols('n al', real=True, nonnegative=True)  # aspect ratio and alpha coefficients
for i in range(1, 3):
    for j in range(1, 3):
        ocean_basis.functions.append(2 * exp(- al * x) * sin(j * n * x / 2) * sin(i * y))
```

We then set the value of the parameter α to a certain value (here α=1). Please note that the α is then an extrinsic parameter of the model that you have to specify through a substitution:

```python
ocean_basis.substitutions.append((al, 1.))
ocean_basis.substitutions.append((n, model_parameters.scale_params.n))
```

Setting now the ocean basis

```python
model_parameters.set_oceanic_modes(ocean_basis)
```

Additionally, for these particular ocean basis functions, a special inner product needs to be defined instead of the standard one proposed. We consider thus as in the publication linked above the following inner product:

```python
class UserInnerProductDefinition(StandardSymbolicInnerProductDefinition):

    def symbolic_inner_product(self, S, G, symbolic_expr=False, integrand=False):
        """Function defining the inner product to be computed symbolically:
        :math:`(S, G) = \\frac{n}{2\\pi^2}\\int_0^\\pi\\int_0^{2\\pi/n}  e^{2 \\alpha x} \\,  S(x,y)\\, G(x,y)\\, \\mathrm{d} x \\, \\mathrm{d} y`.

        Parameters
        ----------
        S: Sympy expression
            Left-hand side function of the product.
        G: Sympy expression
            Right-hand side function of the product.
        symbolic_expr: bool, optional
            If `True`, return the integral as a symbolic expression object. Else, return the integral performed symbolically.
        integrand: bool, optional
            If `True`, return the integrand of the integral and its integration limits as a list of symbolic expression object. Else, return the integral performed symbolically.

        Returns
        -------
        Sympy expression
            The result of the symbolic integration
        """
        expr = (n / (2 * pi ** 2)) * exp(2 * al * x) * S * G
        if integrand:
            return expr, (x, 0, 2 * pi / n), (y, 0, pi)
        else:
            return self.integrate_over_domain(self.optimizer(expr), symbolic_expr=symbolic_expr)

```

Finally setting some other model's parameters:

```python
# Setting MAOOAM parameters according to the publication linked above
model_parameters.set_params({'kd': 0.029, 'kdp': 0.029, 'r': 1.0e-7,
                             'h': 136.5, 'd': 1.1e-7})
model_parameters.atemperature_params.set_params({'eps': 0.76, 'T0': 289.3,
                                                 'hlambda': 15.06})
model_parameters.gotemperature_params.set_params({'gamma': 5.6e8, 'T0': 301.46})
```

Setting the short-wave radiation component as in the publication above: $C_{\text{a},1}$ and $C_{\text{o},1}$

```python
model_parameters.atemperature_params.set_insolation(103.3333, 0)
model_parameters.gotemperature_params.set_insolation(310, 0)
```

Printing the model's parameters

```python
model_parameters.print_params()
```

We now construct the tendencies of the model by first constructing the ocean and atmosphere inner products objects. In addition, a inner product definition instance defined above must be passed to the ocean inner products object:

```python
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    ip = UserInnerProductDefinition()

    aip = AtmosphericSymbolicInnerProducts(model_parameters)
    oip = OceanicSymbolicInnerProducts(model_parameters, inner_product_definition=ip)

```

and finally we create manually the tendencies function, first by creating the tensor object:

```python
aotensor = QgsTensor(model_parameters, aip, oip)
```

and then the Python-[Numba](https://numba.pydata.org/) callable for the model’s tendencies $\boldsymbol{f}$ :

```python
coo = aotensor.tensor.coords.T
val = aotensor.tensor.data

@njit
def f(t, x):
    xx = np.concatenate((np.full((1,), 1.), x))
    xr = sparse_mul3(coo, val, xx, xx)

    return xr[1:]
```

## Time integration

Defining an integrator

```python
integrator = RungeKuttaIntegrator()
integrator.set_func(f)
```

Start on a random initial condition and integrate over a transient time to obtain an initial condition on the attractors

```python
%%time
## Might take several minutes, depending on your cpu computational power.
ic = np.random.rand(model_parameters.ndim)*0.0001
integrator.integrate(0., 3000000., dt, ic=ic, write_steps=0)
time, ic = integrator.get_trajectories()
```

Now integrate to obtain a trajectory on the attractor

```python
%%time
integrator.integrate(0., 500000., dt, ic=ic, write_steps=write_steps)
reference_time, reference_traj = integrator.get_trajectories()
```

Plotting the result in 3D and 2D

```python
varx = 21
vary = 25
varz = 0

fig = plt.figure(figsize=(10, 8))
axi = fig.add_subplot(111, projection='3d')

axi.scatter(reference_traj[varx], reference_traj[vary], reference_traj[varz], s=0.2);

axi.set_xlabel('$'+model_parameters.latex_var_string[varx]+'$')
axi.set_ylabel('$'+model_parameters.latex_var_string[vary]+'$')
axi.set_zlabel('$'+model_parameters.latex_var_string[varz]+'$');
```

```python
varx = 21
vary = 25
plt.figure(figsize=(10, 8))

plt.plot(reference_traj[varx], reference_traj[vary], marker='o', ms=0.1, ls='')

plt.xlabel('$'+model_parameters.latex_var_string[varx]+'$')
plt.ylabel('$'+model_parameters.latex_var_string[vary]+'$');
```

```python
var = 21
plt.figure(figsize=(10, 8))

plt.plot(model_parameters.dimensional_time*reference_time, reference_traj[var])

plt.xlabel('time (days)')
plt.ylabel('$'+model_parameters.latex_var_string[var]+'$');
```

## Showing the resulting fields (animation)

Here, we want to show that the diagnostics adapt to the manually set basis.

This is an advanced feature showing the time evolution of diagnostic of the model. It shows simultaneously a scatter plot of the variable $\psi_{{\rm a}, 1}$, $\psi_{{\rm o}, 2}$ and $\theta_{{\rm o}, 2}$, with the corresponding atmospheric and oceanic streamfunctions and temperature at 500 hPa. Please read the documentation for more information.

Creating the diagnostics (for field plots, we must specify the grid step):

* For the 500hPa geopotential height:

```python
psi_a = MiddleAtmosphericStreamfunctionDiagnostic(model_parameters, delta_x=0.1, delta_y=0.1, geopotential=True)
```

* For the 500hPa atmospheric temperature:

```python
theta_a = MiddleAtmosphericTemperatureDiagnostic(model_parameters, delta_x=0.1, delta_y=0.1)
```

* For the ocean streamfunction:

```python
psi_o = OceanicLayerStreamfunctionDiagnostic(model_parameters, delta_x=0.1, delta_y=0.1, conserved=False)
```

* For the ocean temperature:

```python
theta_o = OceanicLayerTemperatureDiagnostic(model_parameters, delta_x=0.1, delta_y=0.1)
```

* For the nondimensional variables $\psi_{{\rm a}, 1}$, $\psi_{{\rm o}, 2}$ and $\theta_{{\rm o}, 2}$:

```python
variable_nondim = VariablesDiagnostic([21, 25, 0], model_parameters, False)
```

* For the geopotential height difference between North and South:

```python
geopot_dim = GeopotentialHeightDifferenceDiagnostic([[[np.pi/model_parameters.scale_params.n, np.pi/4], [np.pi/model_parameters.scale_params.n, 3*np.pi/4]]],
                                                    model_parameters, True)

```

```python
# setting also the background
background = VariablesDiagnostic([21, 25, 0], model_parameters, False)
background.set_data(reference_time, reference_traj)
```

Selecting a subset of the data to plot:

```python
stride = 10
time = reference_time[10000:10000+5200*stride:stride]
traj = reference_traj[:, 10000:10000+5200*stride:stride]
```

Creating a multi diagnostic with both:

```python
m = MultiDiagnostic(2,3)
m.add_diagnostic(geopot_dim, diagnostic_kwargs={'style':'moving-timeserie'})
m.add_diagnostic(theta_a, diagnostic_kwargs={'show_time':False})
m.add_diagnostic(theta_o, diagnostic_kwargs={'show_time':False})
m.add_diagnostic(variable_nondim, diagnostic_kwargs={'show_time':False, 'background': background, 'style':'3Dscatter'}, plot_kwargs={'ms': 0.2})
m.add_diagnostic(psi_a, diagnostic_kwargs={'show_time':False})
m.add_diagnostic(psi_o, diagnostic_kwargs={'show_time':False})

m.set_data(time, traj)
```

and show an interactive animation:

```python
rc('font',**{'family':'serif','sans-serif':['Times'],'size':12})
m.animate(figsize=(23,12))
```

or a movie (may takes some minutes to compute):

```python
%%time
rc('font',**{'family':'serif','sans-serif':['Times'],'size':12})
m.movie(figsize=(23.5,12), anim_kwargs={'interval': 100, 'frames':2000})
```


# 1. Teoria de Grafos

## 1.1. Definições Básicas

A Teoria de Grafos é um ramo da matemática que estuda as relações entre os objetos de um determinado conjunto. Um **grafo** é uma estrutura composta por **vértices** (nós) e **arestas** (arcos), de tal maneira que diversos problemas podem ser formulados utilizando esta estrutura.

Formalmente falando, um grafo $G=(V,E)$ é definido da seguinte forma:
* $V$ é o conjunto finito de elementos denominados vértices e
* $E$ é o conjunto finito de pares **não ordenados** de vértices chamados de arestas. Na prática, uma aresta conecta dois vértices.

A Figura 1 apresenta um exemplo de grafo, em que $V=\{A, B, C, D\}$ e $E=\{(A,B),(A,C),(B,D)\}$. Como mencionado anteriormente, $E$ é um conjunto não ordenado de vértices, o que significa que as arestas $e_1=(A,B)$ e $e_2=(B,A)$ representam a mesma relação e, por isso, apenas uma delas é incluída no conjunto $E$.

Figura 1: Exemplo de um grafo.

Algumas definições adicionais incluem: seja $e=(A,B)$ uma aresta tal que $e\in E$. Dizemos que $A$ e $B$ são os **extremos** desta aresta, e que $A$ e $B$ são vértices **adjacentes**. Ademais, temos que o **tamanho** do grafo $G$ é dado por $\left|V\right|+\left|E\right|$, em que $\left|V\right|$ e $\left|E\right|$ denotam, respectivamente, o número de vértices e arestas. No exemplo da Figura 1, temos que $\left|V\right|=4$ e $\left|E\right|=3$.

Dizemos que um grafo é **simples** (Figura 1, por exemplo) quando o mesmo não possui **laços** ou **arestas múltiplas**. Um laço é uma aresta com extremos idênticos, ao passo que arestas múltiplas são definidas como sendo duas ou mais arestas com o mesmo par de extremos, conforme demonstrado na Figura 2.

Figura 2: Exemplo de laços e arestas múltiplas em um grafo.

Dizemos que $H=(V^\prime,E^\prime)$ é um **subgrafo** de $G=(V,E)$ quando $V^\prime \subseteq V$ e $E^\prime \subseteq E$. Já o grafo $J=(V,E^\prime)$ é dito ser subgrafo **gerador** de $G$ quando $E^\prime \subseteq E$. A Figura 3 ilustra esses conceitos.

<image>

(a)

(b)

(c)

Figura 3: (a) grafo $G$, (b) subgrafo $H$ e (c) subgrafo gerador $J$.

</image>

Já com relação ao **grau** de um vértice $v\in V$, denotado por $d(v)$, o mesmo diz respeito à quantidade de nós adjacentes à $v$, sendo que laços contam duas vezes. Na Figura 4, por exemplo, temos que $d(A) = 2$, $d(B) = 4$, $d(C) = 1$ e $d(D) = 1.$

Figura 4: Exemplo de um grafo para contagem de graus de seus nós.

De acordo com o teorema conhecido por **Handshake Lemma**, temos a seguinte afirmação:

\begin{equation}
\tag{1}
\sum_{v\in V}d(v)=2\left|E\right|.
\end{equation}
Vamos tentar provar por indução? A prova será realizada de acordo com o **número de vértices** no conjunto $V$. Conforme vimos em aulas anteriores, uma prova por indução possui os seguintes passos:

1. Base: seja $G=(V,E)$ um grafo com apenas um vértice $v$, ou seja, $\left|V\right|=1$. Neste caso, existem duas possibilidades: (i) $d(v) = 0$ e (ii) $d(v) = 2$. Na primeira opção, temos que o número de arestas é igual à zero, ou seja, $\left|E\right|=0$. Assim sendo, a Equação 1 é verdadeira, ou seja, $\sum_{v\in V}d(v)=2\left|E\right|=2.0=0$. Para o segundo caso, temos que $G$ possui um laço no vértice $v$, ou seja, $d(v)=2$. Neste caso, a Equação 1 também é verdadeira, ou seja, $\sum_{v\in V}d(v)=2\left|E\right|=2.1=2$.

2. Hipótese de Indução: seja $G=(V,E)$ um grafo tal que a Equação 1 seja verdadeira.

3. Passo de Indução: seja $G^\prime=(V^\prime,E^\prime)$ um grafo tal que $V^\prime=V\cup\{v^\star\}$, ou seja, $G^\prime$ é formado pelos mesmos vértices de $G$ acrescido do nó $v^\star$. Assim sendo, de acordo com o Handshake Lemma, temos que:

\begin{equation}
\tag{2}
\sum_{v^\prime\in V^\prime}d(v^\prime)=2\left|E^\prime\right|.
\end{equation}
A equação acima pode ser escrita da seguinte forma:

\begin{equation}
\tag{3}
\sum_{v^\prime\in V^\prime}d(v^\prime)=\sum_{v\in V}d(v)+2d(v^\star),
\end{equation}
o que significa, na prática, que a somatória dos graus dos vértices de $G^\prime$ é igual à somatória dos graus dos vértices de $G^\prime$ acrescida do dobro do grau do novo vértice $v^\star$. Mas por que o dobro? A razão é que, ao adicionarmos $v^\prime$ no grafo $G$, temos que grau de cada vértice adjacente à $v^\star$ vai **aumentar** em uma unidade.

Pela hipótese de indução (Equação 2), podemos representar a Equação 3 como segue:

\begin{equation}
\tag{4}
\sum_{v^\prime\in V^\prime}d(v^\prime)=\sum_{v\in V}d(v)+2d(v^\star)=2\left|E\right|+2d(v^\star)=2(\left|E\right|+d(v^\star)).
\end{equation}
Temos que o número de arestas de $G^\prime$ é dado por $\left|E\right|+d(v^\star)$, ou seja, o número de arestas do grafo $G^\prime$ é dado pelo número de arestas de $G$ acrescido da quantidade de vizinhos do novo nó $v^\star$. Desta forma, temos que $\left|E^\prime\right|=\left|E\right|+d(v^\star)$. Finalmente, a Equação 4 pode ser representada da seguinte forma:

\begin{equation}
\tag{5}
\sum_{v^\prime\in V^\prime}d(v^\star)=2(\left|E\right|+d(v^\star)) = 2\left|E^\prime\right|.
\end{equation}

Um **caminho** $\pi$ em um grafo $G$ é uma sequência finita e não vazia de vértices **sem repetição**, como ilustra a Figura 5. Neste caso, temos o caminho em vermelho $\pi=\{A,B,D\}$. Já o **comprimento** $c$ de um caminho é dado pela quantidade de vértices que o mesmo visita, ou seja, $c(\pi)=3$.

Figura 5: Exemplo de caminhos em grafos.

Dizemos que um grafo $G$ é **conexo** se, para quaisquer dois vértices, existe um caminho conectando-os. Por outro lado, caso esta condição não possa ser satisfeita, dizemos que o grafo é **desconexo**, como ilustra a Figura 6.

<image>

(a)

(b)

Figura 6: Exemplos de grafo: (a) conexo e (b) desconexo.

</image>

Finalmente, seja um grafo $G=(V,E)$ e dois nós $v_1,v_2\in V$. Dizemos que $G$ é **cíclico** quando existem, pelo menos, dois ou mais caminhos que possam $v_1$ e $v_2$. Caso contrário, $G$ é dito ser **acíclico**. A Figura 6 ilustra essas situações, em que o grafo apresentado na Figura 6a é dito ser cíclico porque existem dois caminhos distintos (azul e vermelho) que conectam os nós $B$ e $C$.

<image>

(a)

(b)

Figura 6: Exemplos de grafo: (a) cíclico e (b) acíclico.

</image>

Os grafos também podem ser **ponderados** nas arestas, em que uma função $w:V\times V\rightarrow \Re$ é responsável por associar um valor à cada uma delas. A Figura 7 ilustra um exemplo de grafo ponderado.

Figura 7: Exemplo de grafo ponderado, em que $w(A,B)=17$.

Os tipos de grafos que vimos até agora são chamados de **grafos não direcionados** ou **grafos não orientados**. No entanto, existem grafos cujas arestas contemplam pares **ordenados** de vértices, sendo chamados de **grafos direcionados** ou **grafos orientados**. A Figura 8 apresenta um exemplo de grafo direcionado. Neste caso, nosso grafo $G=(V,E)$ é composto da seguinte maneira: $V=\{A,B,C,D\}$ e $E=\{(B,A),(A,C),(C,B),(B,D),(D,C)\}$. Assim sendo, temos que a aresta $(B,A)$ representa um arco que **sai** de $A$ e **entra** em $B$. Note que o oposto não é verdadeiro neste exemplo, ou seja, $(A,B)\notin E$. Podemos então dizer que $B$ é vizinho de $A$, mas o contrário não é verdadeiro neste exemplo.

Figura 8: Exemplo de grafo direcionado.

Já com relação aos graus dos nós de um grafo direcionado, existem duas definições: o **grau de saída** $d^+$ e o **grau de entrada** $d^-$. O grau de saída $d^+(v)$ de um vértice $v\in V$ é dado pelo número de arestas que **saem** de $v$, ao passo que o grau de entrada $d^-(v)$ é dado pelo número de arestas que **entram** em $v$.

Considere o seguinte teorema: para todo grafo orientado $G=(V,E)$, temos que:

\begin{equation}
\tag{7}
\sum_{v\in V}d^+(v) = \sum_{v\in V}d^-(v) = \left|E\right|.
\end{equation}
A demonstração do teorema acima é bastante intuitiva. Seja o grafo orientado $G = (V,E)$ cujo teorema acima é válido. Seja, agora, um grafo orientado $G^\prime=(V^\prime,E^\prime)$ de tal forma que $V^\prime=V\cup\{v^\star\}$. O número de arestas de $G^\prime$ é dado pelo número de arestas de $G$ acrescido pelo número de arestas que entram e saem de $v^\star$, ou seja, $\left|E^\prime\right|=\left|E\right|+d^+(v^\star)+d^-(v^\star)$. Baseando-se no teorema, temos que:

\begin{align}
\tag{8}
\left|E^\prime\right|& = \underbrace{\left|E\right|+d^+(v^\star)}+d^-(v^\star)\\
& = \sum_{v\in V^\prime}d^+(v)\color{red}{-d^-(v^\star)}+d^-(v^\star)\\
& = \sum_{v\in V^\prime}d^+(v).
\end{align}
Na formulação acima, tivemos que adicionar o termo em vermelho pois a somatória dos graus de saída de $G^\prime$ já contabiliza esse montante.

## 1.2. Representação

A complexidade de algoritmos baseados em grafos é dependente da maneira com a qual eles são representados. Basicamente, existem duas estruturas de dados principais utilizadas para representar grafos: (i) **matriz de adjacência** e (ii) **lista de adjcência**. O seu uso depende das operações que ditam a natureza do problema.

Dado um grafo qualquer $G=(V,E)$, a matriz de adjacência é uma matriz quadrada $A_{\left|V\right|\times\left|V\right|}$ cujas linhas e colunas são indexadas pelos vértices em $G$ da seguinte forma:

\begin{equation}
\tag{9}
  A_{ij} =
    \begin{cases}
      1 & \text{caso $(i,j)\in E$}\\
      0 & \text{caso contrário.}
    \end{cases}
\end{equation}
A Figura 9 ilustra um exemplo de um grafo não direcionado e sua respectiva matriz de adjacência. Note que a matriz $A$ é simétrica quando $G$ é não orientado.

Figura 9: Exemplo de um grafo e sua respectiva matriz de adjacência.

Já a Figura 10 apresenta um grafo direcionado e sua respectiva matriz de adjacência. No caso, a mesma já não é mais simétrica.

Figura 10: Exemplo de um grafo direcionado e sua respectiva matriz de adjacência.

Com relação à lista de adjacência, a mesma é construída com base na seguinte premissa: para cada vértice $j$ vizinho de $i$, adicionamos $j$ na lista ligada $L[i]$, em que L é o apontador para o primeiro elemento desta lista. Desta forma, $L[i]$ armazena todos os vizinhos do vértice $i$. As Figuras 11 e 12 apresentam exemplos de uma lista de adjacência para um grafo não direcionado e outro direcionado, respectivamente.

Figura 11: Exemplo de um grafo não direcionado e sua respectiva lista de adjacência.

Figura 12: Exemplo de um grafo direcionado e sua respectiva lista de adjacência.

No entanto, qual das duas estruturas de dados devemos utilizar? A resposta à esta pergunta depende muito da **aplicação**. Vejamos alguns exemplos de operações:

* Verificar se existe uma determinada aresta $(i,j)$:
    * Matriz de adjacência: $\theta(1)$ (a matriz já está alocada)
    * Lista de adjacência: $O(\left|V\right|)$ (tenho que percorrer toda a lista de adjacência do nó $i$)
* Complexidade de espaço:
    * Matriz de adjacência: $\theta(\left|V\right|^2)$ (geralmente mais adequada a **grafos densos**)
    * Lista de adjacência: $\theta(\left|V\right|+\left|E\right|)$ (geralmente mais adequada a **grafos esparsos**)

Dado um grafo com $\left|V\right|$ vértices, temos que o seu número máximo $m$ de arestas é dado por:

\begin{equation}
\tag{10}
m = \binom{\left|V\right|}{2} = \frac{\left|V\right|(\left|V\right|-1)}{2}\in\theta(\left|V\right|^2).
\end{equation}
Um grafo é dito denso se o seu número de arestas se **aproxima** do limitante $m$ acima. Por outro lado, um grafo é dito ser esparso se o seu número de arestas é **muito menor** do que este limitante.

## 1.3. Aplicações

Uma das principais aplicações em grafos diz respeito à **algoritmos de busca**. Basicamente, temos duas abordagens principais: busca em **largura** e busca em **profundidade**. A seguir, veremos cada uma delas.

### 1.3.1 Busca em Largura

Seja o nosso grafo $G=(V,E)$. Dizemos que um vértice $v\in V$ é **alcançável** se, a partir de um outro vértice $s\in V$, existe um caminho que conecta $s$ ao vértice $v$ em $G$. Ademais, temos que a **distância** de $s$ à $v$ é dada pelo caminho mais **curto** se $s$ à $v$. Caso o vértice $v$ não seja alcançável a partir de $s$, dizemos então que a distância entre $s$ e $v$ é **infinita**.

A busca em largura é um algoritmo bastante conhecido que objetiva realizar uma varredura no grafo a partir de um nó $s$ denominado **fonte**. O algoritmo percorre todos os vértices em $G$ que são alcançáveis a partir deste nó fonte, construindo uma **árvore de busca em largura** com raiz em $s$. Cada caminho de $s$ à $v$ nesta árvore corresponde ao caminho mais curto de $s$ à $v$.

O algoritmo funciona, basicamente, da seguinte forma:

1. Inicialmente, a árvore de busca em largura contém apenas o nó $s$.
2. Para cada vizinho $v$ de $s$, o nó $v$ e a aresta $(s,v)$ são inseridos na árvore.
3. O passo acima é repetido para todos os vizinhos de $s$ até que todos os nós alcançados por ele sejam inseridos na árvore.

O algoritmo de busca em largura é implementado utilizado-se uma **fila** $Q$. Além disso, o algoritmo atribui cores aos nós da seguinte forma:
* branco: significa que o nó ainda não foi visitado.
* cinza: significa que o nó foi visitado uma única vez.
* azul: significa que todos os vizinhos do nó já foram visitados.

Para explicar o funcionamento do algoritmo, tomemos como exemplo o grafo da Figura 12a. Neste caso, tomemos o vértice $A$ como sendo o nó fonte, conforme apresentado na Figura 12b. Neste caso, $A$ é então inserido na fila $Q$, recebe a cor cinza e o **custo** $0$. Além disso, os demais nós recebem o custo $\infty$. A Figura 11c ilustra o nó $A$ percorrendo seus dois vizinhos, isto é, os nós $B$ e $C$. Neste exemplo, assumimos que a distância entre nós dois adjacentes é igual à $1$. Assim sendo, por enquanto, o menor caminho entre $A$ e $B$ tem custo $1$. O mesmo ocorre entre $A$ e $C$. Em seguida, ambos nós $C$ e $B$ são inseridos na fila e o processo se repete para o próximo elemento (Figura 11d). A Figura 11d ilustra o nó $C$ visitando os seus vizinhos $A$, $B$ e $D$. No entanto, como $A$ já saiu da fila, ou seja, possui a cor azul, então o mesmo não pode mais ser modificado. Além disso, o custo do nó $D$ é atualizado para $2$, ou seja, o custo que o nó $C$ tinha + 1, e a cor do nó $C$ é mudada para azul. Já a Figura 11g ilustra o nó $B$ saindo da fila e analisando os seus vizinhos $A$, $C$ e $D$. Note que nenhum custo será atualizado. A Figura 11h apresenta o nó $D$ analisando seus vizinhos, mas não pode alterar o custo de nenhum deles. Finalmente, a Figura 11i apresenta a árvore de busca resultante, a qual é composta pelos nós e arestas azuis.

<image>

(a)

(b)

(c)

(d)

(e)

(f)

(g)

(h)

(i)

Figura 11: (a) grafo original, (b) busca começa pelo nó fonte $A$, (c) nó $A$ analisando seus vizinhos, (d) nó $A$ sai da fila, (e) nó $C$ analisando seus vizinhos, (f) nó $D$ entra na fila, (g) nó $B$ analisando seus vizinhos, (h) nó $D$ analisando seus vizinhos e (i) árvore de busca em largura resultante.

</image>

O algoritmo abaixo implementa a busca em largura utilizando o grafo da Figura 11a como exemplo, tendo $A$ como sendo o nó fonte.

```python
import numpy
import sys
import queue

# definindo cores
BRANCO = 0
CINZA = 1
AZUL = 2

# definindo vizinho vazio
NIL = -1

# definindo custo infinto
infty = sys.maxsize

number_nodes = 4 #define o número de nós (vértices)
cor = numpy.empty(number_nodes).astype('int') # cria vetor de cores para os nós
custo = numpy.empty(number_nodes).astype('int') # cria vetor de custos para os nós
caminho = numpy.empty(number_nodes).astype('int') # cria vetor de que vai armazenar a árvore de busca em largura resultante

Q = queue.Queue(maxsize=number_nodes) #definindo a fila

# assumimos os seguintes identificadores para cada nó para fins de indexação dos vetores
# A = 0, B = 1, C = 2 e D = 4

# definindo o grafo G
G = {
        0: [1, 2], # B e C são vizinhos de A
        1: [0, 2, 3], # A, C e D são vizinhos de B
        2: [0, 1, 3], # A, B e D são vizinhos de C
        3: [1, 2] # B e C são vizinhos de D
    }

def Busca_Largura(graph, source):

    # inicializa todos os nós de G
    i = 0
    for node in graph:
        cor[i] = BRANCO
        custo[i] = 0
        caminho[i] = NIL

        i = i+1

    # inicializando o nó fonte
    cor[source] = CINZA
    custo[source] = 0
    caminho[source] = NIL

    Q.put(source) # inserindo nó fonte na fila

    while not Q.empty():

        v = Q.get()

        # para cada nó vizinho de v
        for neighbour in graph[v]:

            if cor[neighbour] == BRANCO:
                cor[neighbour] = CINZA
                custo[neighbour] = custo[v]+1
                caminho[neighbour] = v
                Q.put(neighbour) # insere vizinho na fila
        cor[v] = AZUL

    return caminho

arvore_busca_largura = Busca_Largura(G, 0)
print(arvore_busca_largura)
```

    [-1  0  0  1]

O resultado acima nos mostra um vetor com $4$ posições, em que $A$ é o nó fonte (seu predecessor no caminho é -1), o nó $B$ possui como predecessor o nó $A$, o mesmo ocorre com o nó $C$. Finalmente, o nó $D$ possui o vértice $B$ como predecessor, conforma apresentado na Figura 11i.

Agora, vamos analisar a complexidade do algoritmo acima. Temos que o laço de inicialização percorre todos os vértices, possuindo complexidade de $\theta(\left|V\right|)$. O laço do comando ``while`` percorre, exatamente, todos os nós, ou seja, também possui complexidade $\theta(\left|V\right|)$. Já o comando ``if`` é verdadeiro apenas uma vez para cada nó pois, quando o mesmo troca de cor, não consegue voltar à cor original branca. Assim sendo, a complexidade do laço ``for`` acaba sendo $O(\left|E\right|)$. Isto significa que cada vizinho será visitado uma única vez, mas nada garante que todas as arestas serão visitadas. As complexidades de inserção e remoção da fila são dadas por $\theta(1)$. Desta forma, a complexidade final da busca em largura é dada por $O(\left|V\right|+\left|E\right|)$. Note que estamos utilizando uma lista de adjacência para representar o grafo acima.

### 1.3.2 Busca em Profundidade

Uma outra forma de realizarmos buscas em grafos é a chamada busca em **profundidade**. A estratégia consiste, basicamente, em realizar uma busca o "mais profunda possível" em um grafo a partir de um nó qualquer. De maneira geral, dados um grafo $G=(V,E)$ e um vértice $v\in V$, o algoritmo funciona da seguinte maneira:

1. Inicie a busca a partir de $v$.
2. Escolha um vizinho $u$ não visitado de $v$. Caso este nó exista, reinicie a busca novamente a partir de $u$.
3. Caso contrário, volte ao passo anterior da recursão e procure por outro vizinho ainda não visitado.

Uma outra maneira de enxergar e busca em profundidade é fazer uso de uma **pilha** ao invés da **fila** empregada pela busca em largura. De maneira geral, a ideia consiste no seguinte:

* Suponha que a busca tenha atingido um vértice $v$.
* Escolhe-se um vizinho $u$ não visitado de $v$, empilhe-se $u$ e a busca começa novamente por ele.
* Caso nenhum vértice não visitado não tenha sido encontrado, então desempilha-se um vértice e voltemos ao primeiro passo.

O algoritmo de busca em profundidade tambem atribui cores aos nós da seguinte forma:
* branco: significa que o nó ainda não foi visitado.
* cinza: significa que o nó foi visitado mas "ainda não finalizado.
* azul: significa que todos os vizinhos do nó já foram visitados.

Finalmente, o resultado do algoritmo é uma **árvore de busca em profundidade**, a qual é criada de maneira semelhante à árvore gerada pela busca em largura.

Vejamos como funciona a busca em profundidade utilizando o mesmo grafo-exemplo da busca em largura (Figura 11). A Figura 12a apresenta o grafo utilizado como exemplo, e a Figura 12b apresenta o nó fonte utilizado para iniciar o algoritmo. A Figura 12c ilustra a busca atingindo o nó $B$, que representa um dos vizinhos de A, e a Figura 12d ilustra a busca chegando ao primeiro vizinho de $B$, ou seja, o nó $C$. Já a Figura 12e apresenta a busca atingindo o nó $D$, e a Figura 12f apresenta a busca parada neste nó, dado que nenhum nó com a cor branca que é seu vizinho pode ser encontrado. Neste caso, é atribuída a cor azul ao nó $D$, e a recursão retorna ao nó $C$. A mesma situação também ocorre na Figura 12g, ou seja, não existe nó vizinho de $C$ com a cor branca. O mesmo recebe a cor azul e a recursão retorna ao nó $B$ (Figura 12h). Finalmente, a recursão retorna ao nó $A$ que recebe a cor azul. A árvore de busca de profundidade final é a dada pela Figura 12e.

<image>

(a)

(b)

(c)

(d)

(e)

(f)

(g)

(h)

(i)

Figura 12: (a) grafo original, (b) busca começa pelo nó fonte $A$, (c) nó $A$ analisando seu primeiro vizinho $B$, (d) nó $B$ analisando seu primeiro vizinho $C$, (e) nó $C$ analisando seu vizinho $D$, (f) nó $D$ tem sua cor mudada para azul pois todos os seus vizinhos foram avaliados, (g) nó $C$ tem sua cor mudada para azul pois todos os seus vizinhos foram avaliados, (h) nó $B$ tem sua cor mudada para azul pois todos os seus vizinhos foram avaliados e (i) nó $A$ tem sua cor mudada para azul pois todos os seus vizinhos foram avaliados.

</image>

O algoritmo abaixo implementa a busca em profundidade utilizando o grafo da Figura 12a como exemplo. Este grafo é o mesmo utilizado no exemplo anterior a respeito da busca em largura.

```python
# função que executa a busca em cada nó
def Visita_Node(graph, node):

    cor[node] = CINZA
    custo[node] = custo[node]+1

    # para cada nó vizinho de node
    for neighbour in graph[node]:
        if(cor[neighbour] == BRANCO):
            caminho[neighbour] = node
            Visita_Node(graph, neighbour)

    cor[node] = AZUL

# função principal para a busca em profundidade
def Busca_Profundidade(graph):

    # inicializa todos os nós de G
    i = 0
    for node in graph:
        cor[i] = BRANCO
        custo[i] = 0
        caminho[i] = NIL
        i = i+1

    # executa a busca em profundidade para cada nó branco do grafo
    i = 0
    for node in graph:
        if(cor[i] == BRANCO):
            Visita_Node(graph, node)
        i = i+1

    return caminho

arvore_busca_profundidade = Busca_Profundidade(G)
print(arvore_busca_profundidade)
```

Novamente, o resultado acima nos mostra um vetor com $4$ posições, em que $A$ é o primeiro nó analisado por questões de implementação (seu predecessor no caminho é -1), o nó $B$ possui como predecessor o nó $A$, o mesmo ocorre com o nó $C$. Finalmente, o nó $D$ possui o vértice $C$ como predecessor.

Agora, vamos calcular a complexidade do algoritmo acima. No função principal, isto é, ``Busca_Profundidade``, o primeiro laço itera $\theta(\left|V\right|)$ vezes, e o laço seguinte executa a função ``Visita_Node``apenas $\theta(\left|V\right|)$ vezes, pois quando um nó troca de cor, não volta mais a ser branco. No caso da função ``Visita_Node``, o comando ``if`` dentro o laço é executado apenas uma única vez para cada aresta que conecta o nó em questão e o seu vizinho, ou seja, esse laço executa $O(\left|E\right|)$ vezes. Desta forma, a complexidade final do algoritmo é dada por $O(\left|V\right|+\left|E\right|)$ vezes.

Este conteúdo foi baseado nas notas de aulas do Prof. Zanoni Dias (IC/Unicamp) disponíveis aqui.

```python

```