# Кратчайшие пути

Существуют вариации в зависимости от конкретного случая, но обычно базовой задачей о кратчайших путях считают следующую: дана вершина $s$, найти пути миниальной длины (длина пути -- сумма длин образующих его ребер) до всех остальных вершин. В случае, если длины всех ребер одинаковы, то эта задача решается с помощью базового обхода в ширину, в общем случае его недостаточно, одна есть похожий по простоте метод, на котором основано подавляющее большинство методов нахождения кратчайших путей.

### Обозначения
\begin{align}
u\rightarrow v~-~& ребро~из~u~в~v \\
u\rightsquigarrow v~-~& путь~из~u~в~v \\
u\rightsquigarrow z\rightsquigarrow v~-~& путь~из~u~в~v,~проходящий~через~z \\
u\rightsquigarrow z\rightarrow v~-~& путь~из~u~в~v,~в~котором~последнее~ребро~начинается~в~z \\
u\rightarrow z\rightsquigarrow v~-~& путь~из~u~в~v,~в~котором~первое~ребро~ведет~в~z \\
u_i\rightsquigarrow_{i=1}^{n-1} u_{i+1}=u_1\rightarrow\ldots\rightarrow u_n ~-~& путь,~состоящий~из~ребер~u_1\rightarrow u_2,~u_2\rightarrow u_3,\ldots,~u_{n-1}\rightarrow~u_n. \\
\omega(u\rightarrow v)~-~ & длина~ребра~u\rightarrow v. \\
\omega(u_i\rightsquigarrow_{i=1}^{n-1} u_{i+1})=\sum_{i=1}^{n-1}\omega(u_i\rightarrow u_{i+1})~-~& длина~пути, состоящего~из~ребер~u_1\rightarrow u_2,~u_2\rightarrow u_3,\ldots,~u_{n-1}\rightarrow~u_n. \\
d(u, v)~-~& минимальная~длина~пути~из~u~в~v.
\end{align}
Так как путей из одной вершины в другую может быть много, то обозначение $u\rightsquigarrow v$ предполагает какой-то путь. Если нужно будет уточнить промежуточные вершины в этом пути, то будут использованы правила конкатенации, указанные выше. Длина пути определяется как сумма длин ребер и, соответственно, используется только для путей с явным указанием последовательности ребер.

## Дерево кратчайших путей и динамическое программирование
У кратчайших путей есть одно чрезвычайно важное свойство: если $u_1\rightarrow\ldots\rightarrow u_n$ -- кратчайший путь из $u_1$ в $u_n$, то для всех $1\leq i<j\leq n$: $u_i\rightarrow\ldots\rightarrow u_j$ -- кратчайший путь из $u_i$ в $u_j$, иначе говоря любой кусок оптимального пути также является оптимальным, это свойство обычно называют оптимальной подструктурой. Для решений задач с подобным свойством оказался очень успешным метод динамического программирования, который в целом можно описать следующими принципами:
* Вместе с $d(u, v)$ вычисляем еще какие-то вспомагательные величины, также обозначающие кратчайшие пути, но обычно дополнительно чем-то ограниченые.
* Последовательно вычислять вспомагательные величины начиная с коротких путей постепенно переходя к более длинным.

Подробней уже в конкретных алгоритмах.

Теперь предположим, что мы каким-то образом научились вычислять $d$ для одной пары вершин или нескольких, обычно нам не только интересно само расстояние, но и сам путь. Возникает следующая проблема: $d(u, v)$ -- это одно число, а вот путь -- это уже последовательность. Вопрос в том, можно компактно представить себе этот набор оптимальных путей? В случае, если нас интересуют оптимальные пути от одной вершины до всех остальных, то это удается сделать довольно изящно, очень сильно помагает оптимальная подструктура: если
$$
d(u_1, u_n)=\omega(u_1\rightarrow\ldots\rightarrow u_n),
$$
то
\begin{align}
d(u_1, u_n)=\omega(u_1\rightarrow\ldots\rightarrow u_n)=\omega(u_1\rightarrow\ldots\rightarrow u_{n-1})+\omega(u_{n-1}\rightarrow u_n)=d(u_1, u_{n-1})+\omega(u_{n-1}\rightarrow u_n)
\end{align}
По сути мы просто выделили последнее ребро на оптимальном пути. Предположим, что мы как-то умеем выделять оптимальный путь $u_1\rightsquigarrow u_{n-1}$, тогда чтобы выделить оптимальный путь $u_1\rightsquigarrow u_{n}$, то достаточно запомнить последнее ребро на этом оптимальном пути $u_{n-1}\rightarrow u_n$ и дописать его в конец оптимального пути $u_1\rightsquigarrow u_{n-1}$. Пример этой концепции покажу чуть позже

## Сканирующий метод

Базовый метод для нахождения кратчайших путей заключается в следующем:
\begin{align}
&scan\_arc(v\rightarrow u): \\
&~ ~ ~ ~if (l(v) + \omega(v\rightarrow u) < l(u)) \\
&~ ~ ~ ~ ~ ~ ~ ~l(u)\leftarrow l(v)+\omega(v\rightarrow u) \\
&~ ~ ~ ~ ~ ~ ~ ~p(u)\leftarrow v \\
&~ ~ ~ ~ ~ ~ ~ ~mark(u)\leftarrow labelled \\
& \\
&scan\_vertex(v): \\
&~ ~ ~ ~for~u~such~that~v\rightarrow u~exists: \\
&~ ~ ~ ~~ ~ ~ ~scan\_arc(v\rightarrow u) \\
&~ ~ ~ ~mark(v)\leftarrow scanned \\
& \\
& l(s)\leftarrow 0,~mark(s)\leftarrow labelled \\
& while~exists~v~with~mark(v)=labelled:\\
&~ ~ ~ ~scan\_vertex(v)
\end{align}

Здесь $l(v)$ -- это некоторое расстояние до вершины $v$, которое изменяется по ходу работы алгоритма какждый раз немного улучшаясь, $p(v)$ обновляется вместе с $l$ и запоминает предыдущую вершиу на пути, который мы сейчас считаем минимальным, $mark(v)$ -- пометка вершины: labelled означает, что с момента последнего сканирования этой вершины расстояние до нее изменилось (т.е. уменьшилось), а значит нужно её отсканировать заново; scanned означает, что мы отсканировали вершину, попытались с помощью неё уменьшить расстояния до её соседей, и при этом после этого расстояние до этой вершины не изменялось.

Большинство известных методов нахождения кратчайших путей являются частыми случаями сканирующего метода и отличаются только тем, как выбирать вершины для сканирования в цикле.

### Анализ сканирующего метода
Основные два утверждения касательно сканирующего метода заключается в том, что
* Вне зависимости от выбора вершина для сканирования алгоритм завершается тогда и только тогда, когда в графе нет отрицательных циклов. Это также является необходимым и достаточным условием корректности задачи о кратчайших путях.
* По завершению алгоритма для всех $v$ выполняется
$$
l(v)=d(s, v)
$$
* По завершению $p(v)$ содежит последнее ребро на кратчайшем пути из $s$ в $v$, соответственно ребра $p(v)\rightarrow v$ образуют дерево кратчайших путей.

Первый факт обосновывается следующим образом: если нет отрицательных циклов, значит минимальный путь существует и является простым (без повторений вершин); промежуточные пути в алгоритме являются простыми, вершина помечается $labelled$ только если путь до неё уменьшился, а значит количество раз, когда мы вызовем сканирование вершины конечно.

Обоснования второго факта: для начала заметим, что сканирующий метод обязательно просмотрит все достижимые вершины, так как если запретить повторно сканировать вершину, то сканирущий метод вырождается в BFS. Для недостижимых вершин $l(v)=d(s, v)=+\infty$. Далее заметим, что так как $l(v)$ соответствует длине какого-то пути $s\rightsquigarrow v$, то по определению $d$
$$
l(v)\geq d(s, v).
$$
Пусть оптимальный путь $s\rightsquigarrow$ состоит из вершин $s=u_1, \ldots, u_k=v$, каждая из этих вершин была просканирована хотя бы раз, а значит по завершению работы алгорима выполняется
$$
l(u_i)-l(u_{i-1})\leq \omega(u_{i-1}\rightarrow u_i)
$$
(так как $l(u_i)$ может только уменьшаться и при этом после последнего сканирования $u_{i-1}$ это неравенство выполнялось). Просуммировав эти неравенства получаем
$$
l(v)=l(v)-l(s)\leq \sum_{i=1}^{n}\omega(u_{i-1}\rightarrow u_i)=d(s, v).
$$
Третее утверждение вытекает из второго и того факта, что $p(v)$ всегда обновляется вместе с $l(v)$.

```python
import random
import graphviz
from interactive_visualization.graph_utils import Graph, Arc, Node
from interactive_visualization.animation_utils import animate_list
```

```python
def mark_labelled(node):
    node.SetColor('red')

def mark_scanned(node):
    node.SetColor('green')

def process_node(node):
    node.SetColor('blue')

def set_previous(arc):
    arc.SetColor('green')

def unset_previous(arc):
    arc.SetColor('black')

def scan_arc(graph, arc, l, p, mark):
    if l[arc.end] > l[arc.beginning] + arc.weight:
        l[arc.end] = l[arc.beginning] + arc.weight
        if p[arc.end] is not None:
            unset_previous(p[arc.end])
        # Сохраняем arc, а не arc.beginning, чтобы было больше информации
        p[arc.end] = arc
        set_previous(p[arc.end])
        mark[arc.end] = True
        mark_labelled(graph.nodes[arc.end])

def scan_node(graph, node_id, l, p, mark):
    for arc in graph.nodes[node_id].arcs:
        scan_arc(graph, arc, l, p, mark)
    mark[node_id] = False
    mark_scanned(graph.nodes[node_id])

def random_choice(l, mark):
    labelled = [node_id for node_id, value in mark.items() if value == True]
    if len(labelled) == 0:
        return None
    return random.choice(labelled)

def base_scanning_method(graph, s, choice_function):
    l    = {key: float('Inf') for key in graph.nodes.keys()}
    p    = {key: None for key in graph.nodes.keys()}
    mark = {key: False for key in graph.nodes.keys()}

    l[s] = 0
    mark[s] = True
    mark_labelled(graph.nodes[s])

    out_lst = []

    while True:
        node_id = choice_function(l, mark)
        if node_id is None:
            break
        process_node(graph.nodes[node_id])
        out_lst.append(graph.Visualize(l))
        scan_node(graph, node_id, l, p, mark)
        out_lst.append(graph.Visualize(l))
    return l, p, out_lst
```

```python
arcs = [
    Arc(1, 3, 3),
    Arc(1, 4, 7),
    Arc(4, 3, 2),
    Arc(4, 5, 3),
    Arc(1, 5, 2),
    Arc(6, 4, 2),
    Arc(5, 6, 2),
    Arc(6, 7, 1),
    Arc(7, 2, 7),
    Arc(4, 2, 2),
    Arc(3, 2, 5)
]
Graph(arcs).Visualize()
```

```python
graph = Graph(arcs)
random_scanning_shortest_path_lst = []
l, p, random_scanning_shortest_path_lst = \
    base_scanning_method(graph, 1, random_choice)
```

```python
animate_list(random_scanning_shortest_path_lst);
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=15), Output()), _dom_classes=('widget-interac…

### Кратчайшие пути на ациклических графах
В случае, если в графе нет не только отрицательных циклов, но вообще любых других, то возникает интересная ситуация: если обрабатывать вершины в топологическом порядке, то каждая вершина будет просканирована ровно один раз. Доказать это очень просто: если в графе допустим топологический порядок, то при сканировании вершины $v$ расстояние обновится только для тех вершин, которые в топологическом порядке идут позже $v$.

Если вершины в графе пронумерованы в топологическом порядке, то в указанной выше процедуре для этого достаточно будет просто вместо случайной вершины выбирать наименьшую по номеру

```python
def least_id_choice(l, mark):
    labelled = [node_id for node_id, value in mark.items() if value == True]
    if len(labelled) == 0:
        return None
    return min(labelled)
```

```python
graph = Graph([
    Arc(0, 1, 4),
    Arc(0, 2, 2),
    Arc(1, 2, 5),
    Arc(2, 3, 3),
    Arc(1, 4, 10),
    Arc(3, 4, 4),
    Arc(4, 5, 11)
])
l, p, topological_scanning_shortest_path_lst = \
    base_scanning_method(graph, 0, least_id_choice)
animate_list(topological_scanning_shortest_path_lst);
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=11), Output()), _dom_classes=('widget-interac…

Это один из двух случаев, когда достигается асимптотическая оценка в $\mathcal{O}(E)$ ($E$ -- количество ребер): во-первых, топологическая сортировка осуществляется обходом в глубину за $\mathcal{O}(E)$; во-вторых каждая вершины сканируется ровно один раз, а значит какждое ребро также сканируется ровно один раз; в-третьих если вместо выбора минимальной вершины просто итерироваться в по всем вершинам в топологическом порядке пропуская недостижимые, то результат будет тот же, и при этом суммарно на это мы тратим $\mathcal{O}(V)$ ($V$ -- количество вершин).

На практике ациклические графы часто возникают при моделировании каких-либо протекающих во времени событий. Например одним из промежуточных результатов распознавания речи называе "словные сетки" -- это граф, в котором вершины помечены временнОй меткой, а на ребрах написано слово и информация об акустическом/языковом правдоподобие; ребра всегда ведут из вершин с меньшей временной отметков в вершину с большей и означают, что на этом интервале с такой-то вероятностью было произнесено такое-то слово. Пути в этом графе соответствуют временному интервалу, начинающемуся от временной отметки первой вершины, заканчивающемуся во временной отметке последней вершины. Forward-backward алгоритмы по сути являются частными случаями нахождения кратчайших путей в ациклическом графа, в том числе и алгоритм Витерби для нахождения наиболее правдоподобной последовательности состояний в марковской цепи.

### Обход в ширину и алгоритм Дейкстры
Второй случай, в котором оказывается возможной минимальная оценка для поиска кратчайших путей -- это случай единичных весов, т.е. когда длины всех ребер одинаковы. С точки зрения сканирующего метода в этом случае можно добиться того, чтобы каждая вершина сканировалась ровно один раз, все, что для этого нужно сделать, а это выбирать для очередного сканирования вершину с минимальным расстоянием.

```python
def least_distance_choice(l, mark):
    labelled = [node_id for node_id, value in mark.items() if value == True]
    if len(labelled) == 0:
        return None
    return min(labelled, key=lambda x: l[x])
```

```python
graph = Graph([
    Arc(1, 3, 1),
    Arc(1, 4, 1),
    Arc(4, 3, 1),
    Arc(4, 5, 1),
    Arc(1, 5, 1),
    Arc(6, 4, 1),
    Arc(5, 6, 1),
    Arc(6, 7, 1),
    Arc(7, 2, 1),
    Arc(4, 2, 1),
    Arc(3, 2, 1)
])
l, p, bfs_shortest_path_lst = \
    base_scanning_method(graph, 1, least_distance_choice)
animate_list(bfs_shortest_path_lst);
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=13), Output()), _dom_classes=('widget-interac…

На самом деле для того, чтобы при выборе вершины с минимальным расстоянием каждая вершина сканировалась единожды, достаточно более слабого условия -- чтобы в графе не было ребер с отрицательным весом. Ключевое соображение: кратчайший путь до вершины $v$ может проходить по тем вершинам, которые ближе к $s$, чем $v$. Из этого утверждения следует другое, чуть менее очевидное: если мы уже каким-то образом нашли первые $k$ ближайших вершин и посчитали минимальные пути, проходящие только через них, то расстояние до ближайшей из оставшихся вершин посчитано корректно. Если посмотреть на величины значения $l(v)$, которые мы получаем после $k$-ого сканирования (обозначим её за $l(v, k)$), то получится величина минимального пути от $s$ до $v$, имеющая в качестве промежуточных вершин только $k$ ближайших к $s$. Если обозначить за $u(k)$ -- $k$-ую ближайшую вершину к $s$, то мы получаем следующее соотношение
$$
l(v, k)=\min\{l(v, k-1), l(u(k), k-1)+\omega(u(k)\rightarrow v)\}
$$
Левая величина в минимуме -- это минимум среди путей, которые используют $u(k)$, правая -- минимум среди путей, которые не используют $u(k)$.

```python
graph = Graph(arcs)
l, p, dijkstra_shortest_path_lst = \
    base_scanning_method(graph, 1, least_distance_choice)
animate_list(dijkstra_shortest_path_lst);
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=13), Output()), _dom_classes=('widget-interac…

В общем виде этот метод впервые этого методы обосновал Эдсгер Дейкстра. Ключевым вопросом в этом алгоритме является нахождение минимальной вершины для сканирования. В случае, если все длины одинаковы, то это можно сделать обычной очередью: сохраняем в очереди все вершины с пометкой $labelled$, при сканировании ребра помещаем вершину в конец очереди если расстояние уменьшилось, при выборе новой вершины берем первую из очереди. В общем то в этом случае алгоритм вырождается в обход в глубину.

В общем случае для работы с вершинами, помеченными $labelled$ необходима специальная структура данных, позволяющая быстро делать следующие операции:
* Добавить вершину в множество
* Обновить расстояние для вершины из множества
* Найти миимальную по расстоянию вершину из множества
* Удалить вершину из множества

Одним из простых способов реализовать все эти операции -- использовании бинарной кучи или бинарного дерева, все операции выполняются за $\mathcal{O}(\log N)$, где $N$ -- размер множества, что ведет к общей сложности алгоритма в $\mathcal{O}(E\log V)$.

Наиболее асимптотически оптимальный известный алгоритм использует [кучу фибоначчи](https://en.wikipedia.org/wiki/Fibonacci_heap), которая умеет делать первые три операции за $\mathcal{O}(1)$ и последнюю за $\mathcal{O}(\log N)$, что ведет к общей сложности $\mathcal{O}(E+V\log V)$

К сожалению все описанные рассуждения ломаются, если в графе возникают ребра отрицательной длины, например единственность сканирования пропадает например на следующем примере

```python
graph = Graph([
    Arc(1, 2, 2),
    Arc(1, 3, 3),
    Arc(3, 2, -2),
    Arc(2, 4, 1),
])
l, p, dijkstra_shortest_path_lst = \
    base_scanning_method(graph, 1, least_distance_choice)
animate_list(dijkstra_shortest_path_lst);
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=9), Output()), _dom_classes=('widget-interact…

### Алгоритм Форда-Беллмана

Частично этот алгоритм похож на перидыдущий тем, что по сути является просто применением обычной очереди для общего случая. Обычно алгоритм Форда-Беллмана имеет следующий вид

```python
def ford_bellman(graph, s):
    l    = {key: float('Inf') for key in graph.nodes.keys()}
    p    = {key: None for key in graph.nodes.keys()}
    mark = {key: False for key in graph.nodes.keys()}

    l[s] = 0
    mark[s] = True
    mark_labelled(graph.nodes[s])

    out_lst = [graph.Visualize(l)]

    for k in range(len(graph.nodes)-1):
        for node in graph.nodes.values():
            scan_node(node)
        out_lst.append(graph.Visualize(l))
    return l, p, out_lst
```

Здесь я специально сделал так, что внешний цикл итерируется фиксированное число раз -- $V-1$, при этом переменная $i$ не используется. Внутренний цикл сканирует каждую вершину по одному разу, это эквивалентно тому, чтобы просканировать по одному разу каждое ребро. Оказывается, что такого количества проходов всегда достаточно, даже если в графе есть отрицательные ребра. Идея следующая: давайте рассмотрим величину $l(v, k)$ -- минимальная длина пути от $s$ до $v$, состоящего из не более, чем $k$ ребер, тогда для неё справедливо следующее соотношение
$$
l(v, k)=\min \{l(v, k-1), \min_u[l(u, k-1)+\omega(u\rightarrow v)]\}.
$$
Иначе говоря, либо оптимальный путь использует меньше, чем $k$ ребер, либо из него можно выделить последнее ребро, а оставшаяся часть использует $k-1$ ребро.

Один проход по всем ребрам гарантирует нам переход от $l(v, k-1)$ к $l(v, k)$, однако из-за того, что мы не считаем непосредственно эти величины, а храним их в одном массиве $l$, то получается что на итерации $k$ у нас обычно чуть лучше, чем $l(v, k)$, охватывают пути не только длины $k$, но гарантировать мы может охват только таких путей. Наконец, после $V-1$ итераций мы обязательно охватим все простые пути. Если же оказалось, что после $V-1$ итерации сканирование ребер продолжит уменьшать вес, то в графе есть отрицательный цикл. Этот факт позволяет использовать Алгоритм Форда-Беллмана для нахождения отрицательных циклов.

Возвращаясь к сканирующему методу: мы можем спокойно пропускать в цикле вершины, которые помечены $scanned$. С точки зрения вызовов $scan\_node$ это будет эквивалентно тому, чтобы использовать в сканирующем методе обычную очередь, но при этом использование очереди эффективнее.

# Примеры из МО
### Алгоритм Витерби
Одна из основных задач для марковских цепей заключается в том, чтобы найти последовательность состояний в марковской цепи наиболее правдоподобно соответствующую некоторой последовательности наблюдений
$$
H(O)=argmax_{v=\{v_0, \ldots, v_n\}}\prod_{i=1}^nP(v_{i-1}\rightarrow v_i~|~O_i)
$$
Например в распознавании речи $P(u\rightarrow v~|~O)$ -- это акустическая модель, классифицирующая кусок звука по нескольким языковым единицам, например фонемам. Из наиболее правдоподобной последовательности состояний можно извлечь последовательность фонем и в конце концов последовательность слов.

Если немного переписать величину выше, можно получить в точности задачу о кратчайшем пути
$$
argmax_{v=\{v_0, \ldots, v_n\}}\prod_{i=1}^nP(v_{i-1}\rightarrow v_i~|~O_i)=argmax_{v=\{v_0, \ldots, v_n\}}\sum_{i=1}^n\mathcal \log{P}(v_{i-1}\rightarrow v_i~|~O_i)=argmin_{v=\{v_0, \ldots, v_n\}}\sum_{i=1}^n(-\log P(v_{i-1}\rightarrow v_i~|~O_i))
$$
Стоит отметить, если $P$ -- вероятность, то что $-\log P\geq 0$, что, как мы уже обсудили, имеет значение для кратчайших путей, но не в этом случае.

Пока что мы перешли от умножения к сложению, и от максимума к минимуму, но мы пока еще не получили задачу о кратчайшем пути: пока что у нас задача нахождения минимального пути, содержащую фиксированное число переходов, котором веса ребер зависят от времени. Эта задачу можно свести к обычной задаче о кратчачйшем пути на так расгиренном графе как на следующих примерах.

```python
graph = Graph([
    Arc(0, 1),
    Arc(1, 2),
    Arc(2, 0),
    Arc(1, 1),
    Arc(2, 2),
    Arc(0, 0)
])
graph.Visualize()
```

```python
expanded_graph = graph.expand_in_time(4)
expanded_graph.Visualize()
```

## Отслеживание объекта на плоскости
Предположим, что у нас есть набор из $m$ сенсоров, расположенныз в точках $x_1, \ldots, x_m$ соответственно, которые умеют измерять расстояния до объекта (не важно, каким образом) и выдавать его в качестве функции правдоподобия (если сенсор выдает одно число, то можно считать, что он выдает индикатор-функцию для этого числа). Допустим мы снимаем показание на равных промежутках времени, на замере $t$ мы получаем функции правдоподобия $g_i(t, \|x-x_i\|)$. Оценка максимального правдоподобия положения объекта в пространстве c учетом сделанных замеров выглядит как-то так
\begin{align*}
x&=argmax_{y}\sum_{i=1}^m\log P(y~|~g_i) \\
&=argmax_{y}\sum_{i=1}^m\log [P(g_i~|~y)P(y)] \\
&=argmax_{y}\sum_{i=1}^m\sum_{t=0}^T\log [g_i(t, \|y(t)-x_i\|)P(y)]
\end{align*}
Если не накладывать никакие априорные ограничения ограничения на $x$, то вероятность $P(y)$ можно убрать и получить, что
$$
x(t)=argmax_{z}\sum_{i=1}^T \log g_i(t, \|z-x_i\|),
$$
т.е. оценка координат происходит раздельно раздельно. Такой подход например может привести к тому, что оценка будет сильно болтаться, в то время, как мы ожидаем, что между двумя последовательными оценками разница должна быть небольшой.

```python
import numpy as np
import scipy, scipy.stats, scipy.optimize
from scipy.stats import chi2

class Sensor:
    def __init__(self, x, y, r=10, scale=4):
        self.position = np.array([x, y])
        self.max_distortion_range = r
        self.distortion_scale = scale

    def loglikelihood(self, x, y, x_real, y_real):
        k = int(np.linalg.norm(self.position- np.array([x_real, y_real])))
        p = random.randint(1, self.max_distortion_range)
        return chi2.logpdf(np.linalg.norm(self.position - np.array([x, y])), k) + self.distortion_scale * chi2.logpdf(np.linalg.norm(self.position - np.array([x, y])), p)
```

Здесь сенсоры моделируются с помощью $\chi$-квадрат распределения. Используется тот факт, что максимум

```python
import matplotlib.pyplot as plt
```

```python
plt.rcParams['font.size'] = 15
plt.rcParams["figure.figsize"] = [11,11]
```

```python
path = [np.array([5, 5])]
for i in range(30):
    dx = 2 * np.random.rand(2) - 1
    path.append(path[-1] + dx)

def paths(path):
    figures = []
    for i in range(len(path)):
        fig = plt.figure()
        ax = fig.add_axes([0, 0, 1, 1])
        ax.set(xlim=(-10, 20), ylim=(-10, 20))
        ax.plot([x for x, y in path[:i+1]], [y for x, y in path[:i+1]])
        ax.scatter([x for x, y in path[i:i+1]], [y for x, y in path[i:i+1]], color='black')
        plt.close(fig)
        figures.append(fig)
    return figures

animate_list(paths(path));
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=30), Output()), _dom_classes=('widget-interac…

```python
distortion_level = 0.25
distortion_radius = 20
bottom_left = -10
top_right = 20
sensors = [
    Sensor(bottom_left, bottom_left, r = distortion_radius, scale=distortion_level),
    Sensor(bottom_left, top_right,  r = distortion_radius,scale=distortion_level),
    Sensor(top_right, bottom_left, r = distortion_radius, scale=distortion_level),
    Sensor(top_right, top_right, r = distortion_radius, scale=distortion_level)
]

def minimize_simple(f, x_min, y_min, x_max, y_max):
    best = (x_min, y_min, float('inf'))
    for i in range(10):
        for j in range(10):
            x = x_min + i * (x_max - x_min) / 10
            y = y_min + j * (y_max - y_min) / 10
            cost = f([x, y])
            if best[2] > cost:
                best = (x, y, cost)

    for i in range(10):
        step = 1.0 / (i + 1)
        dx, dy = step * (np.random.rand(2) * 2 - 1)
        cost = f([best[0] + dx, best[1] + dy])
        if cost < best[2]:
            best = (best[0] + dx, best[1] + dy, cost)

    return best[:2]

estimates = []
prev = [0, 0]
for x in path:
    def J(y):
        return -sum([sensor.loglikelihood(y[0], y[1], x[0], x[1]) for sensor in sensors])
    #estimates.append(scipy.optimize.minimize(J, prev, method='Nelder-Mead').x)
    estimates.append(minimize_simple(J, bottom_left, bottom_left, top_right, top_right))
    cur = estimates[-1]
    #print(J(cur), J([cur[0] - 1, cur[1]]), J([cur[0], cur[1] - 1]), J([cur[0] + 1, cur[1]]), J([cur[0], cur[1] + 1]))
    prev = estimates[-1]

def two_paths(path, estimates):
    figures = []
    for i in range(len(path)):
        fig = plt.figure()
        ax = fig.add_axes([0, 0, 1, 1])
        ax.set(xlim=(-10, 20), ylim=(-10, 20))
        ax.plot([x for x, y in path[:i+1]], [y for x, y in path[:i+1]])
        ax.plot([x for x, y in estimates[:i+1]], [y for x, y in estimates[:i+1]])
        ax.scatter([x for x, y in path[i:i+1]], [y for x, y in path[i:i+1]], color='black')
        ax.scatter([x for x, y in estimates[i:i+1]], [y for x, y in estimates[i:i+1]], color='black')

        plt.close(fig)
        figures.append(fig)
    return figures

max_like_distances = []
for x, y in zip(path, estimates):
    max_like_distances.append(np.linalg.norm(x - y))

animate_list(two_paths(path, estimates));
```

    <ipython-input-15-798f0051bc5b>:14: RuntimeWarning: invalid value encountered in double_scalars
      return chi2.logpdf(np.linalg.norm(self.position - np.array([x, y])), k) + self.distortion_scale * chi2.logpdf(np.linalg.norm(self.position - np.array([x, y])), p)

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=30), Output()), _dom_classes=('widget-interac…

Из-за помех получается так себе. Тем не менее мы можем попытаться как-то ограничить эти скачки, так как в реальном мире движение происходит планомерно. простой способ сделать это -- сузbть поиск максимума правдоподобия на фиксированном расстоянии от предыдущей оценки.

```python
estimates = []
prev = [0, 0]
for x in path:
    def J(y):
        return -sum([sensor.loglikelihood(y[0], y[1], x[0], x[1]) for sensor in sensors])
    #estimates.append(scipy.optimize.minimize(J, prev, method='Nelder-Mead').x)
    estimates.append(minimize_simple(J, prev[0] - 1, prev[1] - 1, prev[0] + 1, prev[1] + 1))
    cur = estimates[-1]
    prev = estimates[-1]

animate_list(two_paths(path, estimates));
base_distances = []
for x, y in zip(path, estimates):
    base_distances.append(np.linalg.norm(x - y))
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=30), Output()), _dom_classes=('widget-interac…

Уже лучше, но есть проблема, этот алгоритм все-таки жадный и учитывает только предыдущую оценку. Можно ограничить возможные точки поиска до заранее посчитанной сетки и смоделировать $P(y)$ марковской цепью на этой сетке: переходы разрешаются только на узлах, расстояние между которыми не больше некоторого порога. Таким образом мы получаем задачу поиска наиболее правдоподобной последовательности переходов в марковской цепи, где вероятноть перехода соответствует правдоподобию нахждения в узле на конце этого перехода

```python
pre_grid = [[(bottom_left + i * (top_right - bottom_left) / 30, bottom_left + j * (top_right - bottom_left) / 30) for j in range(31)] for i in range(31)]
grid = []
for row in pre_grid:
    grid.extend(row)
allowed_transitions = []

for i in range(len(grid)):
    allowed_transitions.append([])
    for j in range(len(grid)):
        if np.linalg.norm(np.array(grid[i]) - np.array(grid[j])) <= 1.0:
            allowed_transitions[-1].append(j)

```

```python
beam = 5.0
history_importance = 1.0
estimates = []

for i in range(len(grid)):
    if abs(grid[i][0]) < 0.5 and abs(grid[i][1]) < 0.5:
        token = (i, 0.0, None)
        break

tokens = [token]

for x in path:
    def J(y):
        return -sum([sensor.loglikelihood(y[0], y[1], x[0], x[1]) for sensor in sensors])

    new_tokens = dict()
    J_cache = dict()

    best_cost = float('inf')
    for token in tokens:
        state, cost, prev = token
        for j in allowed_transitions[state]:
            if j not in J_cache:
                new_cost = J(grid[j])
                J_cache[j] = new_cost
            else:
                new_cost = J_cache[j]

            if new_cost + history_importance * cost > best_cost + beam:
                continue

            if j not in new_tokens or new_tokens[j][1] > new_cost + cost:
                new_tokens[j] = (j, new_cost + history_importance * cost, token)

            best_cost = min(best_cost, new_cost + history_importance * cost)

    tokens = new_tokens.values()
    #print(best_cost)

reversed_estimates = []
tmp = min(tokens, key=lambda x: x[1])
while tmp is not None:
    reversed_estimates.append(grid[tmp[0]])
    tmp = tmp[2]
estimates = list(reversed(reversed_estimates))

markov_beam_search_distances = []
for x, y in zip(path, estimates):
    markov_beam_search_distances.append(np.linalg.norm(x - y))

animate_list(two_paths(path, estimates));
```

    HBox(children=(Button(description='Prev', style=ButtonStyle()), Button(description='Next', style=ButtonStyle()…

    interactive(children=(IntSlider(value=0, description='step', max=30), Output()), _dom_classes=('widget-interac…

К сожалению довольно часто этот код уводит приближение на бесконечность, скорее всего это происходит просто из-за ошибок вычисления и помех

```python
plt.plot([i for i in range(len(base_distances))], max_like_distances, label='Расстояние оценом МП')
plt.plot([i for i in range(len(base_distances))], base_distances, label='Расстояние в простом режиме')
plt.plot([i for i in range(len(base_distances))], markov_beam_search_distances, label='Расстояние при лучевом поиске')
plt.legend()
plt.show()
```


# Task 1: Introduction

Welcome to TensorFlow Beginner: Basic Image Classification! By the end of the project, you'd have created and trained a Neural Network model that, after the training, will be able to predict digits from hand-written images with a high degree of accuracy and along the way, you'd have developed a basic understanding of how Neural Networks work and you'd have developed a basic understanding of TensorFlow syntax with Keras as its front end.

This graph describes the problem that we are trying to solve visually. We want to create and train a model that takes an image of a hand written digit as input and predicts the class of that digit, that is, it predicts the digit or it predicts the class of the input image.

```python
import tensorflow as tf

print('Using TensorFlow version', tf.__version__)
tf.logging.set_verbosity(tf.logging.ERROR)
```

# Task 2: The Dataset

In order to understand our problem better, we will first import the data that we'd be working with and take a closer look at it. We are going to use the popular MNIST dataset which has lots of images of hand-written digits along with their labels.

```python
from tensorflow.keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
```

```python
print('x_train shape: ', x_train.shape)
print('y_train shape: ', y_train.shape)
print('x_test shape: ', x_test.shape)
print('y_test shape: ', y_test.shape)
```

```python
import matplotlib.pyplot as plt
%matplotlib inline

plt.imshow(x_train[0], cmap = 'binary')
plt.show()
```

```python
y_train[0]
```

```python
y_train[:10]
```

# Task 3: One Hot Encoding

We will change the way this label is represented from a class name or number to a list of all possible classes with all the classes set to 0 except the one which this example belongs to - which will be set to 1. For example:

| original label | one-hot encoded label |
|------|------|
| 5 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0] |
| 7 | [0, 0, 0, 0, 0, 0, 0, 1, 0, 0] |
| 1 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0] |

```python
from tensorflow.python.keras.utils import to_categorical

y_train_encoded = to_categorical(y_train)
y_test_encoded = to_categorical(y_test)
```

To make sure the encoding worked, let's check the shape of the encoded labels.

```python
print('y_train shape: ', y_train_encoded.shape)
print('y_test shape: ', y_test_encoded.shape)
```

And just like before, let's also take a look at the first label and make sure that encoding is correct:

```python
y_train_encoded[0]
```

# Task 4: Neural Networks

Consider the following graph:

The above graph simply represents the equation:

\begin{equation}
y = w1 * x1 + w2 * x2 + w3 * x3 + b
\end{equation}

Where the `w1, w2, w3` are called the weights and `b` is an intercept term called bias. The graph above, therefore, is simply a graphical representation of a simple linear equation. The equation can also be *vectorised* like this:

\begin{equation}
y = W . X + b
\end{equation}

Where `X = [x1, x2, x3]` and `W = [w1, w2, w3].T`. The .T means *transpose*. This is because we want the dot product to give us the result we want i.e. `w1 * x1 + w2 * x2 + w3 * x3`. This gives us the vectorised version of our linear equation.

What we are trying to ask, essentially, is if given a large amount of data (pairs of X and corresponding y), can we write an algorithm to figure out the optimal values of W and b? We need to find a way for our model to find the *optimal* values for W and b and not the absolute values. Absolute values probably don't even exist given the limitations of our mathematical model since we are *assuming* a linear function for a problem where it might be a much more complex one in reality and we don't know what that function is.

By taking the observed data and a proposed model, we want to write an algorithm to learn the values for W and b which best fit the data and ultimately, by doing that, we learn an approximate function which maps the inputs to outputs of our data. This type of algorithm is called an _optimization_ algorithm and there are a few different optimization algorithms that are typically used in training neural networks.

In our problem, our examples are of shape`(60000, 28, 28)`. The first dimension is simply the number of examples we have, so each example is of the shape `(28, 28)`. If we unroll this array into a single dimension, it will become a `28 * 28 = 784` dimensional vector. Now, it can probably be modeled somewhat like a linear equation, right? Given features from `x1` to `x784`, we get an output `y`. Here, each pixel value is a feature in our examples.

This may actually work for really simple problems but in our case, this model will turn out to be insufficient.

Turns out, we can learn much more complex functions by simply *cascading* the linear functions one after the other. The only additional thing that a node in a neural network does (as opposed to a node in a linear equation shown above) is that an activation function is applied to each linear output. The purpose of an activation functions is to help the neural network find non-linear patterns in the data because if we just cascaded the neurons or nodes like the ones described above, even with many layers of cascaded linear functions, the result will still be a linear function which means that, after training the mode, it will learn a linear function that best fit the data. This is a problem because in many, if not most cases, the input to output map is going to be much more complex than a linear function. So, the activation gives the model more flexibility, and allows the model to be able to learn non-linear patterns.

Now, instead of setting y to a weighted sum of our input features, we can get a few hidden outputs which are weighted sums of our input features passed through an activation function and then get the weighted sums of those hidden outputs and so on. We do this a few times, and then get to our output y. This type of model gives our algorithm a much greater chance of learning a complex function.

In the network above, we have two *hidden layers*. The first layer with all the X features is called the input layer and the output y is called the output layer. In this example, the output has only one __node__. The hidden layer can have a lot of nodes or a very few nodes depending on how complex the problem may be. Here, both the hidden layer have 2 nodes each. Each node gives the output of a linear function after the linear output passes through an activation function, and takes inputs from each node of the preceding layer. All the W's and all the b's associated with all of these functions will have to be "learned" by our algorithm as it attempts to optimize those values in order to best fit the given data. Note that the total number of learnable parameters in any layer depend on the number of nodes in that layer as well as on the number of nodes in the preceding layer. For example, learnable parameters for __hidden layer 1__ can be calculated as: (number of nodes of the layer) * (number of nodes of preceding layer) + (number of nodes of the layer). Why? The first part is obvious: if every node of a layer is connected to every node of the preceding layer, we can simply multiply the number of nodes of these two layers to get the total number of weight parameters. Also, the __bias__ from previous layer would be connected to each node in the layer as well - that gives us the second term. So, for __hidden layer 1__, we get: `2 * 2 + 2 = 6` learnable parameters.

In the hand-written digit classification problem, we will have 128 nodes for two hidden layers, we will have 10 nodes for the output layer with each node corresponding to one output class, and of course we already know that the input is a 784 dimensional vector.

# Task 5: Preprocessing the Examples

We will create a Neural Network which will take 784 dimensional vectors as inputs (28 rows * 28 columns) and will output a 10 dimensional vector (For the 10 classes). We have already converted the outputs to 10 dimensional, one-hot encoded vectors. Now, let's convert the input to the required format as well. We will use numpy to easily unroll the examples from `(28, 28)` arrays to `(784, 1)` vectors.

```python
import numpy as np

x_train_reshaped = np.reshape(x_train, (60000, 784))
x_test_reshaped = np.reshape(x_test, (10000, 784))

print('x_train_reshaped shape: ', x_train_reshaped.shape)
print('x_test_reshaped shape: ', x_test_reshaped.shape)
```

Each element in each example is a pixel value. Let's take a look at a few values of just one example.

```python
print(set(x_train_reshaped[0]))
```

Pixel values, in this dataset, range from 0 to 255. While that's fine if we want to display our images, for our neural network to learn the weights and biases for different layers, computations will be simply much more effective and fast if we *normalized* these values. In order to normalize the data, we can calculate the mean and standard deviation for each example.

```python
x_mean = np.mean(x_train_reshaped)
x_std = np.std(x_train_reshaped)

print('mean: ', x_mean)
print('std: ', x_std)
```

Now we will normalise both the training and test set using the mean and standard deviation we just calculated. Notice that we will need to apply the same mean and standard deviation to the test set even though we did not use the test set to calculate these values.

```python
epsilon = 1e-10
x_train_norm = (x_train_reshaped - x_mean)/(x_std + epsilon)
x_test_norm = (x_test_reshaped - x_mean)/(x_std + epsilon)
```

Note how we added a small value to our denominator. This is because, just in case if our std was close to zero, we'd get very large values as a result. In this case, that's obviously not true but we added this anyway as a good practice since this is typically done to ensure numerical stability.

We looked at some of the values for the first training example before. Let's take a look at it again, after having normalised the values.

```python
print(set(x_train_norm[0]))
```

# Task 6: Creating a Model

We use a Sequential class defined in Keras to create our model. All the layers are going to be Dense layers. This means, like our examples above, all the nodes of a layer would be connected to all the nodes of the preceding layer i.e. densely connected.

```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

model = Sequential([
    Dense(128, activation = 'relu', input_shape = (784,)),
    Dense(128, activation = 'relu'),
    Dense(10, activation = 'softmax')
])
```

Let's understand the code above. We are instantiating a Sequential model. We pass on a list of layers that we want in our model, in the order that we want them. So, we have two hidden layers with 128 nodes each and one output layer with 10 nodes. We set the input shape on the first hidden layer to correspond to the shape of a single example from our reshaped training and test sets - we know each example is a 784 dimensional vector for the 784 pixels of the images.

We have talked about each node having a weighted sum of the inputs of the preceding layer. And, before this sum is fed to the next layer's nodes, it goes through another function called an activation function. So, each node actually does two things. First step is the weighted sum, let's call it Z:

\begin{equation}
Z = W . X + b
\end{equation}

The second step in the node is the activation function output, let's call it A:

\begin{equation}
A = f(Z)
\end{equation}

There are various types of activation functions used in Neural Networks. One of the more common ones is a rectified linear unit of ReLU function. It's a pretty simple function: it's a linear function for all the positive values and is simply set to `0` for all the negative values. Something like this:

Another activation function we are using is called *softmax*. This function gives us probability scores for various nodes, in this case 10 nodes of the output layer, which sum upto 1. This activation gives us the probabilities for various classes given the input. The class with the highest probability gives us our prediction.

In addition to setting up our model architecture, we also need to define which algorithm should the model use in order to optimize the weights and biases as per the given data. We will use stochastic gradient descent.

We also need to define a loss function. Think of this function as the difference between the predicted outputs and the actual outputs given in the dataset. This loss needs to be minimised in order to have a higher model accuracy. That's what the optimasation algorithm essentially does - it minimises the loss during model training. For our multi-class classification problem, *categorical cross entropy* is commonly used.

Finally, we will use the accuracy during training as a metric to keep track of as the model trains.

```python
model.compile(
    optimizer = 'sgd',
    loss = 'categorical_crossentropy',
    metrics = ['accuracy']
)

model.summary()
```

Notice how we have over 100 thousand parameters (the weights and biases) to learn even though we are using only 2 hidden layers. For deep neural networks, this value can be, and often is, in millions.

In order to get the approximation of our function, we just need to fit the model to our data. We will use only training set to do this learning and will reserve the test set for later when we want to check the accuracy of our model. This is because, if we used only one set for both training and testing, the results may be biased and our model may have simply memorized all the examples instead of learning the relationship between features and label.

# Task 7: Training the Model

We are going to train the model for 5 epochs. Think of epoch like an iteration of all the examples going through the model. So, by setting the epochs to 3, we will go through all the training examples 3 times.

```python
h = model.fit(
    x_train_norm,
    y_train_encoded,
    epochs = 3
)
```

In order to ensure that this is not a simple "memorization" by the machine, we should evaluate the performance on the test set. This is easy to do, we simply use the `evaluate` method on our model.

```python
loss, accuracy = model.evaluate(x_test_norm, y_test_encoded)

print('test set accuracy: ', accuracy * 100)
```

# Task 8: Predictions

```python
preds = model.predict(x_test_norm)

print('shape of preds: ', preds.shape)
```

We probably can't go through all the 10000 predictions for now, but we can take a look at the first few. Let's plot the first few test set images along with their predicted and actual labels and see how our trained model actually performed.

```python
plt.figure(figsize = (12, 12))

start_index = 0

for i in range(25):
    plt.subplot(5, 5, i + 1)
    plt.grid(False)
    plt.xticks([])
    plt.yticks([])
    pred = np.argmax(preds[start_index + i])
    actual = np.argmax(y_test_encoded[start_index + i])
    col = 'g'
    if pred != actual:
        col = 'r'
    plt.xlabel('i={} | pred={} | true={}'.format(start_index + i, pred, actual), color = col)
    plt.imshow(x_test[start_index + i], cmap='binary')
plt.show()
```

It gets most of the predictions right!

```python
"""
Enter the index value in place of the value 8 below for the prediction
that you want to plot the probability scores for
"""
index = 8

plt.plot(preds[index])
plt.show()
```

Hopefully this gave you an insight into using Tensorflow and its Keras implementation to get started with training Neural Networks!

TensorFlow Beginner - Basic Image Classification.ipynb


# Modeling and Simulation in Python

Chapter 10 Example: Rigid Pendulum

Copyright 2017 Allen Downey

License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0)

```python
# If you want the figures to appear in the notebook,
# and you want to interact with them, use
# %matplotlib notebook

# If you want the figures to appear in the notebook,
# and you don't want to interact with them, use
# %matplotlib inline

# If you want the figures to appear in separate windows, use
# %matplotlib qt5

# tempo switch from one to another, you have to select Kernel->Restart

%matplotlib notebook

from modsim import *
```

### Pendulum

This notebook solves the Spider-Man problem from spiderman.ipynb, demonstrating a different development process for physical simulations.

In `pendulum_sympy`, we derive the equations of motion for a rigid pendulum without drag, yielding:

$ \ddot{x} = \frac{x}{x^{2} + y^{2}} \left(g y - vx^{2} - vy^{2}\right) $

$ \ddot{y} = - \frac{1}{x^{2} + y^{2}} \left(g x^{2} + y \left(vx^{2} + vy^{2}\right)\right) $

We'll use the same conditions we saw in `spiderman.ipynb`

```python
condition = Condition(g = 9.8,
                      m = 75,
                      area = 1,
                      rho = 1.2,
                      v_term = 60,
                      duration = 30,
                      length0 = 100,
                      angle = (270 - 45),
                      k = 20)
```

Now here's a version of `make_system` that takes a `Condition` object as a parameter.

`make_system` uses the given value of `v_term` to compute the drag coefficient `C_d`.

```python
def make_system(condition):
    """Makes a System object for the given conditions.

    condition: Condition with height, g, m, diameter,
               rho, v_term, and duration

    returns: System with init, g, m, rho, C_d, area, and ts
    """
    unpack(condition)

    theta = np.deg2rad(angle)
    x, y = pol2cart(theta, length0)
    P = Vector(x, y)
    V = Vector(0, 0)

    init = State(x=P.x, y=P.y, vx=V.x, vy=V.y)
    C_d = 2 * m * g / (rho * area * v_term**2)
    ts = linspace(0, duration, 501)

    return System(init=init, g=g, m=m, rho=rho,
                  C_d=C_d, area=area, length0=length0,
                  k=k, ts=ts)
```

Let's make a `System`

```python
system = make_system(condition)
system
```

      value

      init
      x     -70.71067811865477 dimensionless
y     -...

      g
      9.8

      m
      75

      rho
      1.2

      C_d
      0.340278

      area
      1

      length0
      100

      k
      20

      ts
      [0.0, 0.06, 0.12, 0.18, 0.24, 0.3, 0.36, 0.42,...

```python
system.init
```

      value

      x
      -70.71067811865477 dimensionless

      y
      -70.71067811865474 dimensionless

      vx
      0 dimensionless

      vy
      0 dimensionless

To write the slope function, we can get the expressions for `ax` and `ay` directly from SymPy and plug them in.

```python
def slope_func(state, t, system):
    """Computes derivatives of the state variables.

    state: State (x, y, x velocity, y velocity)
    t: time
    system: System object with length0, m, k

    returns: sequence (vx, vy, ax, ay)
    """
    x, y, vx, vy = state
    unpack(system)

    ax = x*(g*y - vx**2 - vy**2)/(x**2 + y**2)
    ay = -(g*x**2 + y*(vx**2 + vy**2))/(x**2 + y**2)

    return vx, vy, ax, ay
```

As always, let's test the slope function with the initial conditions.

```python
slope_func(system.init, 0, system)
```

    (<Quantity(0, 'dimensionless')>,
     <Quantity(0, 'dimensionless')>,
     <Quantity(4.9, 'dimensionless')>,
     <Quantity(-4.900000000000003, 'dimensionless')>)

And then run the simulation.

```python
%time run_odeint(system, slope_func)
```

    CPU times: user 132 ms, sys: 0 ns, total: 132 ms
    Wall time: 130 ms

### Visualizing the results

We can extract the x and y components as `Series` objects.

```python
xs = system.results.x
ys = system.results.y
```

The simplest way to visualize the results is to plot x and y as functions of time.

```python
newfig()
plot(xs, label='x')
plot(ys, label='y')

decorate(xlabel='Time (s)',
         ylabel='Position (m)')
```

    <IPython.core.display.Javascript object>

We can plot the velocities the same way.

```python
vxs = system.results.vx
vys = system.results.vy
```

```python
newfig()
plot(vxs, label='vx')
plot(vys, label='vy')

decorate(xlabel='Time (s)',
         ylabel='Velocity (m/s)')
```

    <IPython.core.display.Javascript object>

Another way to visualize the results is to plot y versus x.  The result is the trajectory through the plane of motion.

```python
newfig()
plot(xs, ys, label='trajectory')

decorate(xlabel='x position (m)',
         ylabel='y position (m)')
```

    <IPython.core.display.Javascript object>

We can also animate the trajectory.  If there's an error in the simulation, we can sometimes spot it by looking at animations.

```python
newfig()
decorate(xlabel='x position (m)',
         ylabel='y position (m)',
         xlim=[-100, 100],
         ylim=[-200, -50],
         legend=False)

for x, y in zip(xs, ys):
    plot(x, y, 'bo', update=True)
    sleep(0.01)
```

    <IPython.core.display.Javascript object>

Here's a function that encapsulates that code and runs the animation in (approximately) real time.

```python
def animate2d(xs, ys, speedup=1):
    """Animate the results of a projectile simulation.

    xs: x position as a function of time
    ys: y position as a function of time

    speedup: how much to divide `dt` by
    """
    # get the time intervals between elements
    ts = xs.index
    dts = np.diff(ts)
    dts = np.append(dts, 0)

    # decorate the plot
    newfig()
    decorate(xlabel='x position (m)',
             ylabel='y position (m)',
             xlim=[xs.min(), xs.max()],
             ylim=[ys.min(), ys.max()],
             legend=False)

    # loop through the values
    for x, y, dt in zip(xs, ys, dts):
        plot(x, y, 'bo', update=True)
        sleep(dt / speedup)
```

```python
animate2d(system.results.x, system.results.y)
```

    <IPython.core.display.Javascript object>

```python

```


# Example Model Project: the Train Illusion
Marius t'Hart, Megan Peters, Paul Schrater, Gunnar Blohm

**Disclaimer**:  this is a "toy" model used to demonstrate the [10 step procedure of how-to-model](https://doi.org/10.1523/ENEURO.0352-19.2019). It is not meant to be state of the art research.

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

----
# Phenomenon
*Part of Steps 1-2*

The train illusion occurs when sitting on a train and viewing another train outside the window. Suddenly, the other train *seems* to move, i.e. you experience visual motion of the other train relative to your train. But which train is actually moving?

Often people have the wrong percept. In particular, they think their own train might be moving when it's the other train that moves; or vice versa. The illusion is usually resolved once you gain vision of the surroundings that lets you disambiguate the relative motion; or if you experience strong vibrations indicating that it is indeed your own train that is in motion.

----
# Question

*Part of Step 1*

We asked the following (arbitrary) question for our demo project: "How do noisy vestibular estimates of motion lead to illusory percepts of self motion?"

----
# Background
*Part of Step 2*

You have learned all about the vestibular system in the Intro video. This is also where you would do a literature search to learn more about what's known about self-motion perception and vestibular signals. You would also want to examine any attempts to model self-motion, perceptual decision making and vestibular processing.

----
# Ingredients

*Part of step 3*

We determined that we probably needed the following ingredients for our model:
* Vestibular input: $v(t)$
* Binary decision output: $d$ - time dependent?
* Decision threshold: $\theta$
* A filter (maybe running average?): $f$
* An integration mechanism to get from vestibular acceleration to sensed velocity: $\int$

----
# Hypotheses

*Part of step 4*

Our main hypothesis is that the strength of the illusion has a linear relationship to the amplitude of vestibular noise.

Mathematically, this would write as $S = k \cdot N$, where $S$ is the illusion strength and $N$ is the noise level, and $k$ is a free parameter.
>we could simply use the frequency of occurance across repetitions as the "strength of the illusion"

We would get the noise as the standard deviation of $v(t)$, i.e. $N=\mathbf{E}[v(t)^2]$, where $\mathbf{E}$ stands for the expected value.

Do we need to take the average across time points?
> doesn't really matter because we have the generative process, so we can just use the $\sigma$ that we define

----
# Selected toolkit

*Part of step 5*

We chose to use a [Drift-Diffusion Model (DDM)](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2474742/) because it is a well-established framework that allows us to model decision making in the case of 2 alternative choices (here: self-motion vs. other train motion).

For our purposes simplest equation looks something like this:
\begin{align}
\dot e = \frac{de}{dt}= -c \cdot e + v
\end{align},
 where $e$ is the accumulated evidence and $v$ is our vestibular input already containing the noise (so we don't need to add more noise?). $c$ is the leakage constant, i.e. $c=0$ means perfect integration; $c=1$ means no integration (perfect leakage).

----
# Model draft

*Part of step 6*

Brainstorming on the whiteboard, we came up with this...

----
# Model implementation

*Part of step 7*

We need at least 3 functions:
1. vestibular signal generator
2. integrator (or drift-diffusion mechanism)
3. decision mechanism (threshold comparison)

**Note**: we did not add a filter (yet). We're not sure if we need one...

So let's go step by step... first we need to get set up...

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
!pip install pandas --quiet
plt.style.use('dark_background')
```

### 1. Vestibular signal generator

```python
def vestibular_signal(sig, mov):
  """
  Computes a vestibular signal that corresponds to the acceleration of the
  train with different amplitudes of noise
  Args:
    sig: scalar SD of noise
    mov: 0 means no self-motion; 1 means self-motion (or scaling or motion signal)
  Returns: vector array of vestibular signal v
  """
  # create white noise series for 10s with 1ms resolution
  x = np.linspace(-7, 14, 1001)
  z = 1/(1 + np.exp(-x))
  noise = norm.rvs(size=1000)
  v = sig*noise + mov*np.diff(z)/0.001

  return v
```

Let's see if that works... (*unit test*)

```python
v = vestibular_signal(1,1)

# plot signal
t = np.linspace(0, 10, 1000)
plt.plot(t,v)
plt.xlabel("time (s)")
plt.ylabel("vestibular signal (a.u.)")
```

### 2. integrator (DDM mechanism)

```python
def ddm(v, c):
  """
  Leaky integration of vestibular signal
  Args:
    v: array of vestibular signal
    c: leakage constant
  Outputs: time series of integrated vestibular signal (with leakage)
    = evidence e
  """
  e = np.random.normal(0, 0.1)
  E = []
  for i in range(len(v)):
    e += -c*e + v[i]*0.001
    E.append(e)
  return E

```

Let's test the DDM function... (*unit test*)

```python
e = ddm(v,c=0.001)

# plot result
plt.plot(t,e)
plt.xlabel("time (s)")
plt.ylabel("integrated evidence")
```

### 3. thresholding (decision mechanism)

```python
def threshold(e, thr):
  """
  Thresholding of motion evidence
  Args:
    motion evidence: e (array)
    threshold: thr
  Output: decision d if threshold was reached
  """
  d = any(np.array(e) > thr)*1
  return d
```

Now let's test this function... (*unit test*)

```python
d = threshold(e, .6)
d
```

    1

## Assembling the model

```python
def run_model(sig,c,thr,mov):
  """
  runs the full model to simulate self-motion decision, e.g. for train illusion
  Args:
    sig: SD of vestibular noise
    c: leakage constant
    thr: decision threshold
    mov: self-motion? ) no selfmotion; 1 self-motion
  Output: decision d (0: no self-motion; 1: self-motion)
  """
  v = vestibular_signal(sig, mov)
  e = ddm(v,c)
  d = threshold(e, thr)
  return d
```

Let's run the model and see if it works...

```python
d = run_model(200,0.001,0.8,1)
d
```

    1

----
# Model completion

*Part of step 8*

So the model seems to work. Running different parameters gives us different results. Are we done?
* **can we answer our question**: yes, in our model the illusion arises because integrating very noisy vestibular signals representing motion evidence sometimes accumulate to a decision threshold and sometimes do not reach that threshold.
* **can we speak to our hypothesis**: yes, we can now simulate different trials with different noise levels (and leakage and thrshold parameters) and evaluate the hypothesized linear relationship between  vestibular noise and how often our perceptual system is fooled...
* **does the model reach our goals**: yes, we wanted to generate a mechanistic model to be able to make some specific predictions that can then be tested experimentally later...

----
# Model evaluation & testing

*Part of step 9*

Ok, so we still need to actually evaluate and test our model performance. Since this is a conceptual model and we don't have actual data (yet), we will evaluate how our model behaves as a function of the 3 parameters. If we had data with different conditions, we could try to fit the model to the data and evaluate the goodness of fit, etc... If other alterative models existed, we could evaluate our model against those alternatives too.

So let's run out model in different parameter regimes and analyze the result to get some insight into the model performance

```python
import itertools # to automatically generat possible combinations of parameters

# define parameter list
params = {
    'sig': np.linspace(1,21,5)**2,
    'c': np.exp(np.linspace(-10,-1,5)),
    'thr': np.linspace(0,2,5),
    'mov': np.linspace(0,1,2),
}

# run combination of parameters
keys = list(params)
D = []
for i in range(0,100):
  for values in itertools.product(*map(params.get, keys)):
      d = run_model(**dict(zip(keys, values)))
      temp = list(values)
      temp.append(d)
      D.append(temp)
```

Now let's explicitly test our hypothsis for different parameter combinations...

```python
# want data frames:
import pandas as pd
df = pd.DataFrame(D, columns = ['Sig','c','Thr','Mov','Decisions'])
# multi panel layout:
axs = plt.figure(figsize=(12,12), constrained_layout=True).subplots(5, 5)
# plot for movement absent/present
Mov_s = np.unique(df['Mov'])
# plot for leakage parameter & threshold values:
c_s   = np.unique(df['c'])
Thr_s   = np.unique(df['Thr'])
# plot for data for both movement condition for each leakage/threshold combination
Sig_s   = np.unique(df['Sig'])
for Thr_n in range(len(Thr_s)):
  for c_n in range(len(c_s)):
    subdf0 = df[(df.Mov == 0) & (df.c == c_s[c_n]) & (df.Thr == Thr_s[Thr_n])].groupby(['Sig'])['Decisions'].mean()
    subdf1 = df[(df.Mov == 1) & (df.c == c_s[c_n]) & (df.Thr == Thr_s[Thr_n])].groupby(['Sig'])['Decisions'].mean()
    im0 = axs[Thr_n, c_n].plot(Sig_s, subdf0, label="no motion")
    im1 = axs[Thr_n, c_n].plot(Sig_s, subdf1, label="motion")
    axs[Thr_n, c_n].set_title("Thr = {}; c = {:.4f}".format(Thr_s[Thr_n], c_s[c_n]))
    axs[Thr_n, c_n].set_ylim(0, 1.1)
    axs[Thr_n, c_n].set_xlim(0, 450)
axs[4,2].set_xlabel("Noise level $\sigma$")
axs[2,0].set_ylabel("Proportion motion judgment")
axs[3,1].set_facecolor('grey')
axs[4,4].legend();
```

There seems to be some parameter redundancy. I.e. we could chose different parameter combinations to make the model do something sensible...

But it looks like $c=0.0004$ works well for $\theta = 1.5$ and $\sigma=50$ (highlighted plot). Lets run a few trials on that to analyze those results more specifically...

```python
# run "best" parameter combination
sig = 50
c = 0.0004
thr = 1.5
d0 = []
d1 = []
for i in range(0,1000):
    d0.append(run_model(sig,c,thr,0))
    d1.append(run_model(sig,c,thr,1))
print('\n Motion detected for no-motion =', sum(d0)/10, '%; and motion =', sum(d1)/10, '%')

```

     Motion detected for no-motion = 30.9 %; and motion = 60.0 %

This does indeed result in roughly 50% likelihood of experiencing the illusion both ways.

Finally, let's explicitly evaluate our hypothesis...

```python
sig = np.linspace(1,201,20)
c = 0.0004
thr = 1.5
D0 = []
D1 = []
for s in range(len(sig)):
  d0 = []
  d1 = []
  for i in range(0,100):
    d0.append(run_model(sig[s],c,thr,0))
    d1.append(run_model(sig[s],c,thr,1))
  D0.append(sum(d0)/100)
  D1.append(sum(d1)/100)
```

```python
plt.plot(sig,D0, label="no motion")
plt.plot(sig,D1, label ="motion")
plt.xlabel("Noise level")
plt.ylabel("% motion decisions")
plt.legend();
```

Our **hypothesis** of linear increase of illusion strength with noise only holds true in a limited range of noise... It's monotonic but saturating of course...

**And regarding our original question**: it is really the noise that pushes the integrated signal over the threshold. The less leaky the integration and the lower the threshold, the more motion decisions we get...

----
# Summary
*Part of Step 10*

Let's write a simple abstract following the guidelines...

**A. What is the phenomena**?  Here summarize the part of the phenomena which your modeling addresses.

_The "train illusion" occurs when sitting in a stationary train and experiencing relative visual motion of an adjacent train outside the window; sometimes we feel like we're moving even if we're not (and vice versa). Previous literature has suggested that vestibular signals are used to disambiguate self-motion from motion of an adjacent object._

**B. What is the key scientific question?**:  Clearly articulate the question which your modeling tries to answer.

_How noisy vestibular estimates of motion lead to illusory percepts of self motion is currently unknown._

**C. What was our hypothesis?**:  Explain the key relationships which we relied on to simulate the phenomena.

_We hypothesized that noisy vestibular signals are integrated leading the brain to decide whether self-motion is occurring or not, and that larger noise is linearly associated with more frequent errors in self-motion judgment._

**D. How did your modeling work?** Give an overview of the model, it's main components, and how the modeling works.  ''Here we ... ''

_To investigate this hypothesis, we constructed a drift diffusion model and simulated self-motion decisions under varying noise conditions, when true self motion was occurring or not._

**E. What did you find? Did the modeling work?** Explain the key outcomes of your modeling evaluation.

_We observed that higher noise did indeed lead to more frequent errors in self-motion perception but this relationship was not linear._

**F. What can you conclude?** Conclude as much as you can _with reference to the hypothesis_, within the limits of the modeling.

_We conclude that accumulated noisy vestibular information can explain the occurrence of the train illusion, and the higher the noise (or the lower the signal-to-noise ratio), the more frequently such illusions will occur._

**G. What are the limitations and future directions?** What is left to be learned? Briefly argue the plausibility of the approach and/or what you think is essential that may have been left out.

_Future research should investigate whether trial-by-trial variations of noisy vestibular signals actually correlate with self-motion judgments._

>If we put this all in one paragraph, we have our final complete abstract. But, first, do not include the letters in _your_ abstract, and second, we did paraphrase the answers a little so they fit together.

## Abstract
(A) The "train illusion" occurs when sitting in a stationary train and experiencing relative visual motion of an adjacent train outside the window; sometimes we feel like we're moving even if we're not (and vice versa). Previous literature has suggested that vestibular signals are used to disambiguate self-motion from motion of an adjacent object. (B) How noisy vestibular estimates of motion lead to illusory percepts of self motion is currently unknown. (C) We hypothesized that noisy vestibular signals are integrated leading the brain to decide whether self-motion is occurring or not, and that larger noise is linearly associated with more frequent errors in self-motion judgment. (D) To investigate this hypothesis, we constructed a drift diffusion model and simulated self-motion decisions under varying noise conditions, when true self motion was occurring or not. (E) We observed that higher noise did indeed lead to more frequent errors in self-motion perception but this relationship was not linear. (F) We conclude that accumulated noisy vestibular information can explain the occurrence of the train illusion, and the higher the noise (or the lower the signal-to-noise ratio), the more frequently such illusions will occur. (G) Future research should investigate whether trial-by-trial variations of noisy vestibular signals actually correlate with self-motion judgments.

----
# Final thoughts

Note that the model we built here was extremely simple and used artificial data on purpose. It allowed us to go through all the steps of building a model, and hopefully you noticed that it is not always a linear process, you will go back to different steps if you hit a roadblock somewhere.

There are many issues that we did not address in this model. However, if you're interested in how to actually approach modeling a similar phenomenon in a probabilistic way, we encourage you to read the paper by [Dokka et. al., 2019](https://doi.org/10.1073/pnas.1820373116), where the authors model how judgments of heading direction are influenced by objects that are also moving.


## Exercise 1: my first exercise

Here goes a general introduction to the exercise.

**a)**
Predict what this code will do

```Python
        a = 2
        b = 3
        for i in range(a, b):
            print(i)
```

**b)**
Modify the program so that it prints 2, 3 and 4.

**c)**
Here is an open question without code to end this first exercise.

## Exercise 2: my second exercise

Here goes a general introduction to the exercise.

**a)**
Implement this difference equation in Python
and generate a plot for values of $x$ from 1 to 10.

$$
\begin{equation}
x_{n+1} = 1.5 x_n -20
x_0 = 20
\label{_auto1} \tag{1}
\end{equation}
$$

**b)**
Question without code answer:
Can you think of an example case that has this difference equation as model?

**c)**
Another question without code answer.


Copyright (c) Microsoft Corporation. All rights reserved.

Licensed under the MIT License.

# Bayesian Personalized Ranking (BPR)

This notebook serves as an introduction to Bayesian Personalized Ranking (BPR) model for implicit feedback.  In this tutorial, we focus on learning the BPR model using matrix factorization approach, hence, the model is sometimes also named BPRMF.

The implementation of the model is from [Cornac](https://github.com/PreferredAI/cornac), which is a framework for recommender systems with a focus on models leveraging auxiliary data (e.g., item descriptive text and image, social network, etc).

## 0 Global Settings and Imports

```python
import sys
sys.path.append("../../")
import os
import cornac
import papermill as pm
import pandas as pd
from reco_utils.dataset import movielens
from reco_utils.dataset.python_splitters import python_random_split
from reco_utils.evaluation.python_evaluation import map_at_k, ndcg_at_k, precision_at_k, recall_at_k
from reco_utils.recommender.cornac.cornac_utils import predict_ranking
from reco_utils.common.timer import Timer
from reco_utils.common.constants import SEED

print("System version: {}".format(sys.version))
print("Cornac version: {}".format(cornac.__version__))
```

    System version: 3.6.8 |Anaconda, Inc.| (default, Feb 21 2019, 18:30:04) [MSC v.1916 64 bit (AMD64)]
    Cornac version: 1.1.2

```python
# Select MovieLens data size: 100k, 1m, 10m, or 20m
MOVIELENS_DATA_SIZE = '100k'

# top k items to recommend
TOP_K = 10

# Model parameters
NUM_FACTORS = 200
NUM_EPOCHS = 100
```

## 1 BPR Algorithm

### 1.1 Personalized Ranking from Implicit Feedback

The task of personalized ranking aims at providing each user a ranked list of items (recommendations).  This is very common in scenarios where recommender systems are based on implicit user behavior (e.g. purchases, clicks).  The available observations are only positive feedback where the non-observed ones are a mixture of real negative feedback and missing values.

One usual approach for item recommendation is directly predicting a preference score $\hat{x}_{u,i}$ given to item $i$ by user $u$.  BPR uses a different approach by using item pairs $(i, j)$ and optimizing for the correct ranking given preference of user $u$, thus, there are notions of *positive* and *negative* items.  The training data $D_S : U \times I \times I$ is defined as:

$$D_S = \{(u, i, j) \mid i \in I^{+}_{u} \wedge j \in I \setminus I^{+}_{u}\}$$

where user $u$ is assumed to prefer $i$ over $j$ (i.e. $i$ is a *positive item* and $j$ is a *negative item*).

### 1.2 Objective Function

From the Bayesian perspective, BPR maximizes the posterior probability over the model parameters $\Theta$ by optimizing the likelihood function $p(i >_{u} j | \Theta)$ and the prior probability $p(\Theta)$.

$$p(\Theta \mid >_{u}) \propto p(i >_{u} j \mid \Theta) \times p(\Theta)$$

The joint probability of the likelihood over all users $u \in U$ can be simplified to:

$$ \prod_{u \in U} p(>_{u} \mid \Theta) = \prod_{(u, i, j) \in D_S} p(i >_{u} j \mid \Theta) $$

The individual probability that a user $u$ prefers item $i$ to item $j$ can be defined as:

$$ p(i >_{u} j \mid \Theta) = \sigma (\hat{x}_{uij}(\Theta)) $$

where $\sigma$ is the logistic sigmoid:

$$ \sigma(x) = \frac{1}{1 + e^{-x}} $$

The preference scoring function $\hat{x}_{uij}(\Theta)$ could be an arbitrary real-valued function of the model parameter $\Theta$.  Thus, it makes BPR a general framework for modeling the relationship between triplets $(u, i, j)$ where different model classes like matrix factorization could be used for estimating $\hat{x}_{uij}(\Theta)$.

For the prior, one of the common pratices is to choose $p(\Theta)$ following a normal distribution, which results in a nice form of L2 regularization in the final log-form of the objective function.

$$ p(\Theta) \sim N(0, \Sigma_{\Theta}) $$

To reduce the complexity of the model, all parameters $\Theta$ are assumed to be independent and having the same variance, which gives a simpler form of the co-variance matrix $\Sigma_{\Theta} = \lambda_{\Theta}I$.  Thus, there are less number of hyperparameters to be determined.

The final objective of the maximum posterior estimator:

$$ J = \sum_{(u, i, j) \in D_S} \text{ln } \sigma(\hat{x}_{uij}) - \lambda_{\Theta} ||\Theta||^2 $$

where $\lambda_\Theta$ are the model specific regularization paramerters.

### 1.3 Learning with Matrix Factorization

#### Stochastic Gradient Descent

As the defined objective function is differentible, gradient descent based method for optimization is naturally adopted.  The gradient of the objective $J$ with respect to the model parameters:

$$
\begin{align}
\frac{\partial J}{\partial \Theta} & = \sum_{(u, i, j) \in D_S} \frac{\partial}{\partial \Theta} \text{ln} \ \sigma(\hat{x}_{uij}) - \lambda_{\Theta} \frac{\partial}{\partial \Theta} ||\Theta||^2 \\
& \propto \sum_{(u, i, j) \in D_S} \frac{-e^{-\hat{x}_{uij}}}{1 + e^{-\hat{x}_{uij}}} \cdot  \frac{\partial}{\partial \Theta} \hat{x}_{uij} - \lambda_{\Theta} \Theta
\end{align}
$$

Due to slow convergence of full gradient descent, we prefer using stochastic gradient descent to optimize the BPR model.  For each triplet $(u, i, j) \in D_S$, the update rule for the parameters:

$$ \Theta \leftarrow \Theta + \alpha \Big( \frac{e^{-\hat{x}_{uij}}}{1 + e^{-\hat{x}_{uij}}} \cdot \frac{\partial}{\partial \Theta} \hat{x}_{uij} + \lambda_\Theta \Theta \Big) $$

#### Matrix Factorization for Preference Approximation

As mentioned earlier, the preference scoring function $\hat{x}_{uij}(\Theta)$ could be approximated by any real-valued function.  First, the estimator $\hat{x}_{uij}$ is decomposed into:

$$ \hat{x}_{uij} = \hat{x}_{ui} - \hat{x}_{uj} $$

The problem of estimating $\hat{x}_{ui}$ is a standard collaborative filtering formulation, where matrix factorization approach has shown to be very effective.  The prediction formula can written as dot product between user feature vector $w_u$ and item feature vector $h_i$:

$$ \hat{x}_{ui} = \langle w_u , h_i \rangle = \sum_{f=1}^{k} w_{uf} \cdot h_{if} $$

The  derivatives of matrix factorization with respect to the model parameters are:

$$
\frac{\partial}{\partial \theta} \hat{x}_{uij} =
\begin{cases}
    (h_{if} - h_{jf})  & \text{if } \theta = w_{uf} \\
    w_{uf}             & \text{if } \theta = h_{if} \\
    -w_{uf}            & \text{if } \theta = h_{jf} \\
    0                  & \text{else}
\end{cases}
$$

In theory, any kernel can be used to estimate $\hat{x}_{ui}$ besides the dot product $ \langle \cdot , \cdot \rangle $.  For example, k-Nearest-Neighbor (kNN) has also been shown to achieve good performance.

#### Analogies to AUC optimization

By optimizing the objective function of BPR model, we effectively maximizing [AUC](https://towardsdatascience.com/understanding-auc-roc-curve-68b2303cc9c5) measurement.  To keep the notebook focused, please refer to the [paper](https://arxiv.org/ftp/arxiv/papers/1205/1205.2618.pdf) for details of the analysis (Section 4.1.1).

## 2 Cornac implementation of BPR

BPR is implemented in the [Cornac](https://cornac.readthedocs.io/en/latest/index.html) framework as part of the model collections.
* Detailed documentations of the BPR model in Cornac can be found [here](https://cornac.readthedocs.io/en/latest/models.html#bayesian-personalized-ranking-bpr).
* Source codes of the BPR implementation is available on the Cornac Github repository, which can be found [here](https://github.com/PreferredAI/cornac/blob/master/cornac/models/bpr/recom_bpr.pyx).

## 3 Cornac BPR movie recommender

### 3.1 Load and split data

To evaluate the performance of item recommendation, we adopted the provided `python_random_split` tool for the consistency.  Data is randomly split into training and test sets with the ratio of 75/25.

Note that Cornac also cover different [built-in schemes](https://cornac.readthedocs.io/en/latest/eval_methods.html) for model evaluation.

```python
data = movielens.load_pandas_df(
    size=MOVIELENS_DATA_SIZE,
    header=["userID", "itemID", "rating"]
)

data.head()
```

    100%|███████████████████████████████████████████████████████████████████████████████████████| 4.81k/4.81k [00:08<00:00, 590KB/s]

      userID
      itemID
      rating

      0
      196
      242
      3.0

      1
      186
      302
      3.0

      2
      22
      377
      1.0

      3
      244
      51
      2.0

      4
      166
      346
      1.0

```python
train, test = python_random_split(data, 0.75)
```

### 3.2 Cornac Dataset

To work with models implemented in Cornac, we need to construct an object from [Dataset](https://cornac.readthedocs.io/en/latest/data.html#module-cornac.data.dataset) class.

Dataset Class in Cornac serves as the main object that the models will interact with.  In addition to data transformations, Dataset provides a bunch of useful iterators for looping through the data, as well as supporting different negative sampling techniques.

```python
train_set = cornac.data.Dataset.from_uir(train.itertuples(index=False), seed=SEED)

print('Number of users: {}'.format(train_set.num_users))
print('Number of items: {}'.format(train_set.num_items))
```

    Number of users: 943
    Number of items: 1642

### 3.3 Train the BPR model

The BPR has a few important parameters that we need to consider:

- `k`: controls the dimension of the latent space (i.e. the size of the vectors  $w_u$  and  $h_i$ ).
- `max_iter`: defines the number of iterations of the SGD procedure.
- `learning_rate`: controls the step size $\alpha$ in the gradient update rules.
- `lambda_reg`: controls the L2-Regularization $\lambda$ in the objective function.

Note that different values of `k` and `max_iter` will affect the training time.

We will here set `k` to 200, `max_iter` to 100, `learning_rate` to 0.01, and `lambda_reg` to 0.001. To train the model, we simply need to call the `fit()` method.

```python
bpr = cornac.models.BPR(
    k=NUM_FACTORS,
    max_iter=NUM_EPOCHS,
    learning_rate=0.01,
    lambda_reg=0.001,
    verbose=True,
    seed=SEED
)
```

```python
with Timer() as t:
    bpr.fit(train_set)
print("Took {} seconds for training.".format(t))
```

    100%|██████████████████████████████████████████████████████████| 100/100 [00:07<00:00, 13.27it/s, correct=92.19%, skipped=9.38%]

    Optimization finished!
    Took 7.6953 seconds for training.

### 3.4 Prediction and Evaluation

Now that our model is trained, we can produce the ranked lists for recommendation.  Every recommender models in Cornac provide `rate()` and `rank()` methods for predicting item rated value as well as item ranked list for a given user.  To make use of the current evaluation schemes, we will through `predict_rating()` and `predict_ranking()` functions inside `cornac_utils` to produce the predictions.

Note that BPR model is effectively designed for item ranking.  Hence, we only measure the performance using ranking metrics.

```python
with Timer() as t:
    all_predictions = predict_ranking(bpr, train, usercol='userID', itemcol='itemID', remove_seen=True)
print("Took {} seconds for prediction.".format(t))
```

    Took 1.7393803596496582 seconds for prediction.

```python
all_predictions.head()
```

      userID
      itemID
      prediction

      75000
      811
      755
      0.117239

      75001
      811
      287
      2.579992

      75002
      811
      181
      3.743980

      75003
      811
      96
      1.959841

      75004
      811
      83
      1.122369

```python
k = 10
eval_map = map_at_k(test, all_predictions, col_prediction='prediction', k=k)
eval_ndcg = ndcg_at_k(test, all_predictions, col_prediction='prediction', k=k)
eval_precision = precision_at_k(test, all_predictions, col_prediction='prediction', k=k)
eval_recall = recall_at_k(test, all_predictions, col_prediction='prediction', k=k)

print("MAP:\t%f" % eval_map,
      "NDCG:\t%f" % eval_ndcg,
      "Precision@K:\t%f" % eval_precision,
      "Recall@K:\t%f" % eval_recall, sep='\n')
```

    MAP:	0.109077
    NDCG:	0.403395
    Precision@K:	0.354989
    Recall@K:	0.180183

```python
# Record results with papermill for tests
pm.record("map", eval_map)
pm.record("ndcg", eval_ndcg)
pm.record("precision", eval_precision)
pm.record("recall", eval_recall)
```

## References

1. Rendle, S., Freudenthaler, C., Gantner, Z., & Schmidt-Thieme, L. (2009, June). BPR: Bayesian personalized ranking from implicit feedback. https://arxiv.org/ftp/arxiv/papers/1205/1205.2618.pdf
2. Pan, R., Zhou, Y., Cao, B., Liu, N. N., Lukose, R., Scholz, M., & Yang, Q. (2008, December). One-class collaborative filtering. https://cseweb.ucsd.edu/classes/fa17/cse291-b/reading/04781145.pdf
3. **Cornac** - A Comparative Framework for Multimodal Recommender Systems. https://cornac.preferred.ai/


These are the slides presented by Jonathan Conning at the webinar [Modeling COVID-19: Disease Burdens, Economic Activities, and Policy Outcomes](http://www.roosevelthouse.hunter.cuny.edu/events/modeling-covid-19-disease-burdens-economic-activities-policy-outcomes/) at The Roosevelt House Public Policy Institute on May 27.

This is a jupyter notebook with interactive widgets.  To see these visualizations you must run this notebook on a jupyter server on your own computer (or through a cloud service such as Binder or google colab (I will post links for this soon.)

This can also be viewed as a slideshow with the RISE extension.

# Modeling COVID-19:
## Disease Burdens, Economic Activities, and Policy Outcomes

**Jonathan Conning,** Department of Economics, Hunter College and The Graduate Center, City University of New York

[ny times](https://www.nytimes.com/interactive/2020/us/coronavirus-us-cases.html)

## Infections and Externalities
Spring Breaker from Ohio | Unique Cell phones on one Ft. Lauderdale Beach 3/16
- | -
 |

**The same cellphones the following week**

[source](https://www.nytimes.com/2020/04/11/us/florida-spring-break-coronavirus.html)

## Modeling Epidemics: The SIR model

Three compartments: **Susceptible $S_t$, Infected $I_t$, Removed $R_t$**

Transitions depend on the **effective contact rate** $\beta$ (contact rate x transmission rate) and the **recovery rate** $\gamma$

## SIR  Model
$$
\begin{align}
\frac{dI}{dt} &= \beta \cdot S \cdot I - \gamma \cdot I  \\[8pt]
\frac{dS}{dt} &=-\beta \cdot S \cdot I  \\[8pt]
\frac{dR}{dt} &= \gamma \cdot I
\end{align}
$$

***Mass action*** or homogenous mixing assumption: transmission from infected to susceptibles is proportional to the product of the population sizes.

## Basic reproductive number $R_0$

- The average number of people an infectious person will infect, *assuming that the rest of the population is susceptible.*

$$R_0 = \frac{\beta}{\gamma}$$
- If $R_0>0$ epidemic grows, since new contagions arrive faster than removals.

  - $R_0$ for Covid19 in Oxford model for the UK: 2.25 to 2.75.

  - $R_0$ in Wuhan estimated between 1.95 to 3.0.

  - On Princess Diamond $R_0=14.8$

### with $R_0 = 2.0$

But the spread will eventually slow on its own

... even without changes in policy or behavior.

## Effective Reproductive Number $R_t = R_0 \cdot S_t$

- epidemic slows as fewer susceptibles left to infect
   - e.g. if $R_0 = 2.0$ and half have been removed (possibly via vaccination) then $R_t = 1.0$ and infections will decline.

- **Herd Immunity Threshold**  at $S_t = \frac{1}{R_0}$ or once $I_t+R_t = \left (1-\frac{1}{R_0} \right )$

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from ipywidgets import interact, interact_manual, fixed
from IPython.display import HTML, Video

import networkx as nx
import ndlib.models.epidemics as ep
import EoN

```

```python
N = 1000
I0, R0 = 1, 0
S0 = N - I0 - R0
# Contact rate, beta, and mean recovery rate, gamma, (in 1/days).
beta, gamma = 0.3, 1./10
# A grid of time points (in days)
t = np.linspace(0, 160, 160)

def deriv(y, t, N, beta, gamma):
    S, I, R = y
    dSdt = -beta * S * I / N
    dIdt = beta * S * I / N - gamma * I
    dRdt = gamma * I
    return dSdt, dIdt, dRdt

def plotSIR(beta=0.25, gamma=0.1, T=160):
    t = np.linspace(0, T, T)
    y0 = S0, I0, R0
    ret = odeint(deriv, y0, t, args=(N, beta, gamma))
    S, I, R = ret.T
    fig = plt.figure(facecolor='w',figsize=(12,8))
    ax = fig.add_subplot(211, facecolor='#dddddd', axisbelow=True)
    ax2 = fig.add_subplot(212, facecolor='#dddddd', axisbelow=True)
    ax.plot(t, S/1000, 'b', alpha=0.5, lw=2, label='Susceptible')
    ax.plot(t, I/1000, 'r', alpha=0.5, lw=2, label='Infected')
    ax.plot(t, R/1000, 'g', alpha=0.5, lw=2, label='Removed')
    ax.text(50,0.9,r'$R_0 =$'+f'{beta/gamma:.2f}', fontsize=18)
    ax.set_xlabel('Time /days')
    ax.set_ylabel('proportion')
    ax.set_ylim(0,1)
    ax.set_xlim(0,T)
    ax.yaxis.set_tick_params(length=0)
    ax.xaxis.set_tick_params(length=0)
    ax.grid(b=True, which='major', c='w', lw=2, ls='-')
    legend = ax.legend()
    legend.get_frame().set_alpha(0.5)
    for spine in ('top', 'right', 'bottom', 'left'):
        ax.spines[spine].set_visible(False)
    #ax.axhline(1-1/(beta/gamma),linestyle=':')
    ax2.plot(t, 1500*I/1000, 'g', alpha=0.5, lw=2, label='Deaths')
    legend2 = ax2.legend()
    legend2.get_frame().set_alpha(0.5)
    ax2.set_ylim(0,500)
    ax2.grid(b=True, which='major', c='w', lw=2, ls='-')
    plt.show()

```

SIR simuluation (not calibrated)

```python
plotSIR(beta=0.25)
```

```python
interact(plotSIR, beta=(0.1, 1, 0.005), gamma=(0.1, 1, 0.05), T=(150,250,10));
```

    interactive(children=(FloatSlider(value=0.25, description='beta', max=1.0, min=0.1, step=0.005), FloatSlider(v…

## Epidemics travel over Networks

- Where and how we work, where we socialize, where we live and who we live with

- Economic transactions also travel along networks

- Hence economic consequences of lockdowns and potential health-economic tradeoffs

## Fun with networks
Erdos-Renyi random graph $G(n,p)$ with $n$ nodes and a success probability of $p$ edges per node.

$G(4,1)$: with 4 nodes and success probability 1 have all possible edges (six in this case).  In general the number of possible edges is  $\frac{n\cdot(n-1)}{2}\cdot p$

```python
def drawg(n=25,m=0.2):
    g = nx.erdos_renyi_graph(n, m)
    nx.draw(g)
    plt.show()
```

```python
drawg(4,1)
```

$G(20, 0.1)$ The network will look different each time because it's created at random.

```python
drawg(20,0.1)
```

```python
interact_manual(drawg, n=(10,50,5), m=(0.025,1,0.025));
```

    interactive(children=(IntSlider(value=25, description='n', max=50, min=10, step=5), FloatSlider(value=0.2, des…

## Stochastic SIR model
Each potential transmission along a network edge is a Bernoulli trial ('coin flip'), so number of infections passed on folllows a binomial.

```python
def simsir(n, beta, gamma, T):
    states = np.zeros((T,3))
    S, I, R = n-1, 1, 0
    states[0, :] = [S,I,R]
    for t in range(1,T):
        contagions = np.random.binomial(n=1, p=beta*I/n, size = S).sum()
        removals = np.random.binomial(n=1, p=gamma, size = I).sum()
        S = S - contagions
        I = I  + contagions - removals
        R = R + removals
        states[t,:] = [S, I, R]
    return states

def plotsims(runs=5, n=100, beta=0.2, gamma=0.1, T=100):
    plt.figure(figsize=(12,7))
    if runs > 1:
        alp =0.4
    else:
        alp = 1
    for r in range(runs):
        states = simsir(n, beta, gamma, T)
        plt.plot(states[:,0], color ='blue', alpha = alp)
        plt.plot(states[:,1], color = 'red', alpha = alp)
        plt.plot(states[:,2], color = 'green', alpha = alp)
    plt.grid()

```

The path of the infection is stochastic (random luck). In some runs the infection never takes off.

```python
plotsims(runs = 5, n=1000, beta=0.175, gamma=0.1, T=200 )
```

```python
interact_manual(plotsims, runs = (1,10,1), n=fixed(1000), beta=(0.01,0.2,0.01), gamma=(0.01,0.2,0.01), T=fixed(200) );
```

    interactive(children=(IntSlider(value=5, description='runs', max=10, min=1), FloatSlider(value=0.2, descriptio…

# Epidemic spreading over networks

1. Over map grid

2. Karate Club example.

```python
G = nx.karate_club_graph()

def karate_ep(tau=0.3, ts =2, animate = False):
    nx_kwargs = {"with_labels":True} #optional arguments to be passed on to the networkx plotting command.
    print('doing Gillespie simulation')
    sim = EoN.Gillespie_SIR(G, tau, 0.1, return_full_data=True)
    sim.sim_update_color_dict({'S': '#becee8','I':'#ff2000', 'R':'darkgray'})
    if animate:
        print('creating animation video.. this will take time.')
        ani=sim.animate(time = ts, **nx_kwargs)
        ani.save('karate.mp4', fps=5, extra_args=['-vcodec', 'libx264'])
        print('saved karate.mp4')
    else:
        sim.display(time = ts, **nx_kwargs) #plot at time 1.
    plt.show()
```

```python
Video('media\SIR_grid_blue.mp4')
```

```python
karate_ep(tau=0.25, ts=5)
```

```python
interact(karate_ep, tau=(0.1,0.6,0.05), ts=(1,20,1),animate=fixed(False));
```

    interactive(children=(FloatSlider(value=0.3, description='tau', max=0.6, min=0.1, step=0.05), IntSlider(value=…

```python
Video('media\karate.mp4')
```


# Modélisation dromotherm 0D

## auteurs:
+ **Benoit STUTZ,Professeur**, LOCIE (Université Savoie Mont-Blanc,France)
+ **Frédédric BERNARDIN**, Chercheur HDR, chef d'unité adjoint Mobilité Durable et Sécurité / Cerema Clermont-Ferrand
+ **Alexandre CUER**, Directeur Adjoint / Cerema Clermont-Ferrand
+ **Fébron SEVI**, Stagiaire master au LOCIE
---

Un échangeur routier dromotherm est une chaussée constituée de trois couches :
+ Une couche de roulement ou de surface d'une épaisseur $h_s$
+ Une couche drainante poreuse d'une épaisseur $h_d$
+ Une couche de base d'une épaisseur $h_b$

Cette chaussée, grâce à la circulation de l'eau dans la couche poreuse, permet de récupérer de la chaleur en été et de chauffer la chaussée en hiver.

Pour quantifier l'énergie récupérable dans la chaussée, nous allons décrire, sous formes d'équations, les différents phénomènes thermiques ayant lieu dans les différentes couches de la chaussée. Le procedé de transfert de chaleur comprend :
- la conduction dans la chaussée,
- la convection entre la surface de la chaussée et l’air,
- la convection entre l’eau et la chaussée
- le rayonnement thermique entre la chaussée et l’atmosphère.

# Bilan d'énergie couche par couche
## Notations

Notation|description|unité
--|--|--
$C_i$ | Capacité calorifique volumique de la couche i | $\frac{J}{m^3.K}$
$h_i$ | Epaisseur de la couche i | m
$T_i$ | Température de la couche i | °C
$T_{injection}$ | Température d'entrée ou d'injection du fluide | °C
$T_{sortie}$ | Température de sortie du fluide | °C
$\alpha$ | albédo = 0.08 |
$\epsilon$ | émissivité = 0.92|
$\sigma$ | constante de Stefan-Boltzmann $5,67.10^{-8}$ |$\frac{W}{m^2K^4}$
$q_f$ | débit volumique du fluide | $\frac{m^3}{s}$
$k_i$ | conductivité thermique de la couche i | $\frac{W}{mK}$
$r_{i/j}$ | coefficient d'échange surfacique entre les couches i et j | $\frac{W}{m^2K}$
$H_v$ | coefficient d'échange convectif entre la couche de surface et l'air | $\frac{W}{m^2K}$
$h$ | coefficient d'echange convectif entre le fluide et la couche drainante | $\frac{W}{m^2K}$
$L$ | largeur de la chaussée | m

On peut écrire :

\begin{equation}
r_{i/j}=\frac{2k_i k_j}{h_i k_j+h_j k_i}
\end{equation}

On peut approximer $H_v$ par une fonction affine de la vitesse du vent, notée $V_{vent}$ exprimée en m/s :

\begin{equation}
H_v=5.8 + 4.1V_{vent}
\end{equation}

Toutes les formules ci-après s'entendent pour une surface de chaussée de 1 mètre dans le profil en long fois L mètres dans le profil en travers.

## Couche de surface

\begin{equation}
C_s h_s\frac{dT_s}{dt}=(1-\alpha)R_g+ R_{atm} -\epsilon\sigma T_s^{4}-H_v(T_s-T_{ext})-r_{s/d}(T_s-T_d)
\end{equation}

## Couche drainante

\begin{equation}
C_d h_d\frac{dT_d}{dt}=r_{s/d}(T_s-T_d)-r_{d/b}(T_d-T_b)- \frac{q_f C_f (T_{sortie}-T_{injection})}{L}
\end{equation}

## Couche de base

La couche de base est supposée adiabatique :

\begin{equation}
C_b h_b\frac{dT_b}{dt}=r_{d/b}(T_d-T_b)
\end{equation}

## Température de sortie du fluide

Notons $d\phi$ le flux de chaleur gagné par le fluide au sein d'un volume élémentaire de l'échangeur, de section $dS$ et de longueur $dx$

Notons $q_f$ le débit du fluide dans la couche drainante en $\frac{m^3}{s}$

Notons $C_f$ la capacité calorifique volumique du fluide en $\frac{J}{m^3.K}$

Notons $h$ le coefficient d'echange convectif entre le fluide et la couche drainante exprimé en $\frac{W}{m^2K}$

On suppose que la température de la couche drainante ne varie pas et on note $T_f$ la température au sien du fluide.

On a :

\begin{equation}
d\phi = q_f C_f (T_f(x+dx) - T_f(x))=q_f C_f dT_f = h (T_d - T_f)dS
\end{equation}

Ou encore :

\begin{equation}
dT_f = \frac{d\phi}{q_f C_f} =\frac{h(Td-Tf)dS}{q_f C_f}
\end{equation}

Ce qui donne :

\begin{equation}
\frac{d(Td-T_f)}{T_d-T_f} =\frac{-hdS}{q_f C_f}
\end{equation}

On parle d'écart de température logarithmique de l'échangeur. En intégrant, on obtient :

\begin{equation}
\ln \frac{T_{sortie}-T_d}{T_{injection}-T_d} = \frac{-hS}{q_f C_f}
\end{equation}

Avec $S$ surface d'échange sur toute la longueur du profil en travers, exprimée en $m^2$.

On en déduit l'expression de la température de sortie :

\begin{equation}
T_{sortie} = T_d+(T_{injection}-T_d)\exp^{\frac{ -h S}{q_f C_f}}
\end{equation}

Pour un dromotherme de 4 mètres de large dans le profil en travers et de 1 mètre de long dans le profil en long, on peut faire les constats suivants :
- par retour d'expérience, on sait que le débit induit par l'écoulement gravitaire est environ de environ 50 litre par heure.
- de part la porosité de la couche drainante, la surface d'échange de l'échangeur est très importante, environ 100 fois la surface du dromotherme
- le coefficient d'échange convectif entre le fluide et le drainant est aussi très élevé, probablement entre 1000 et 2000 $\frac{W}{m^2K}$

En conséquence, cette méthode de modélisation de la température de sortie au sein du modèle 0D n'est pas très opérationnelle, elle revient en pratique à considérer la température du fluide égale à la température de la couche drainante

# Cas pratique

Nous allons utiliser un jeu de données météo provenant de la station météorologique de Saint-Flour sur l'autoroute A75, pour l'été 2010. Ce jeu intègre notamment :
* Température air (°C)
* Température point de rosée (°C)
* Nature des précipitations
* Vitesse du vent (m/s)
* Rayonnement global (W/m2)
* Rayonnement atmosphérique (W/m2)

```python
from dromosense import getCsvDatas
from dromosense.constantes import *
import numpy as np
import matplotlib.pylab as plt

fig = plt.figure(figsize=(20, 10))
step, datas = getCsvDatas("meteo.csv",dir="datas",preview=True)
```

La résolution du système d'équations différentielles se fait gràce à une méthode de Runge-Kutta.

Plusieurs constantes sont automatiquement chargées par le module `dromosense.constantes`, notamment :
- les capacités calorifiques volumiques des différentes couches, en $\frac{J}{m^3.K}$, notées dans le code `Cs`, `Cd` et `Cb`,
- les conductivités des différentes couches, en $\frac{W}{mK}$, notées dans le code `ks`, `kd` et `kb`,
- l'émissivité notée `epsilon`
- l'abedo notée `albedo`
- la constante de Stéphan Boltzmann notée `sigma`, en $\frac{W}{m^2.K^{-4}}$

On définit les variables complémentaires.

```python
"""
L : Largeur de la chaussée en m
hs, hd et hb : épaisseurs des couches de surface, de base et de base exprimée en m
Hv : Coefficient convectif entre la surface et l'air (fonction affine de la vitesse du vent) en W/(m2K)
qf : débit fixe dans le dromotherme de 50 l/h par mètre linéaire de chaussée dans le profil en long
Tinjection : température d'injection du fluide dans le dromotherme en °C
"""
L=4
hs=0.06
hd= 0.08
hb=10
Hv = 5.8 + 4.1*datas[:,3]
qf=0.05/3600 # en m^3/s
Tinjection=10

B1 = (1-albedo)*datas[:,4] + datas[:,5] + Hv*datas[:,0]

```

On crée une méthode pour calculer le coefficient d'échange surfacique entre 2 couches de chaussée et on construit la fonction F définissant le système d'équations différentielles. Tout est prêt pour lancer la résolution avec la fonction `solve_ivp` de scipy

cf https://docs.scipy.org/doc/scipy/reference/integrate.html

```python
def rd(k1,k2,h1,h2):
    """
    calcule le coefficient d'échange surfacique entre 2 couches de conductivité k1 et k2 et d'épaisseurs h1 et h2
    """
    return 2*k1*k2/(h1*k2+h2*k1)

rds=rd(ks,kd,hs,hd)
rdb=rd(kd,kb,hd,hb)

def F(t, X):
    """
    Définition du système d'équations différentielles
    i est le temps en secondes
    """
    Ts=X[0]
    Td=X[1]
    Tb=X[2]
    i=int(t/step)

    y1 = ( B1[i] - Hv[i]*Ts - epsilon*sigma*(Ts+273.15)**4 - rds*(Ts-Td) ) / ( Cs*hs )
    y2 = ( rds*(Ts-Td) - rdb*(Td-Tb) - qf*Cf/L*(Td-Tinjection) ) / ( Cd*hd )
    y3 = rdb*(Td-Tb) / ( Cb * hb )

    return [y1,y2,y3]
```

```python
from scipy.integrate import solve_ivp
"""
on recrée le temps discrétisé
"""
t=step*np.arange(datas.shape[0])

solution = solve_ivp(F,[0,(datas.shape[0]-1)*step],[10,10,10],t_eval=t)
```

```python
fig = plt.figure(figsize=(20, 10))
plt.xlabel('Temps (en secondes)')
plt.ylabel('Températures (en °C)')
plt.title("Profils de températures 0D")
plt.plot(solution.t,solution.y[0],label="Température couche de surface")
plt.plot(solution.t,solution.y[1],label="Température de sortie du fluide = température couche drainante")
plt.plot(solution.t,solution.y[2],label="Température couche de base")
plt.legend(loc="upper right")
plt.show()
```

## Energie et taux de récupération
(mettre le code)

```python

```