# Лекция №3

# Свертки

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

## Определение
Во-первых, давайте посмотрим на математическое определение свертки в дискретной области времени. Позже мы пройдемся по тому, что говорит нам это уравнение.
${\begin{align}y[n]&=x[n]*h[n]=\sum_{k=-\infty}^\infty x[k]\cdot h[n-k]\end{align},}$

где:
* **x[n]** $-$ входой сигнал;
* **h[n]** $-$ импульсный отклик;
* **y[n]** $-$ выходной сигнал;
* **$*$** $-$ обозначает свертку.

Обратите внимание, что мы умножаем слагаемые $x[k]$ на смещенные по времени $h[n]$ и складываем их.
Краеугольный камень понимания свертки лежит в основе импульсного отклика и импульсного разложения.

## Разложение импульсной функции

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

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

Изучим следующий пример того, как сигнал разлагается на набор импульсных (дельта) функций:

Импульсная функция выглядит так: ${\delta(n)={\begin{cases}1, n=0\\ 0, n\ne0 \end{cases}}}$.  Тогда для данного примера $x[0]$ можно записать в виде ${x[0] = x[0] \cdot \delta[n-0] = 2 \cdot \delta[n-0]}$. Тогда $x[1]$ будет равен ${x[1] = x[1] \cdot \delta[n-1] = 3 \cdot \delta[n-1]}$, потому что ${\delta[n-1]}$ равна $1$ при $n = 1$ и ноль в других случаях. Таким же образом мы можем записать ${x[2] = x[2] \cdot \delta[n-2] = 1 \cdot \delta[n-2]}$, сдвинув ${\delta[n]}$ на 2. Следовательно, сигнал $x[n]$ может быть представлен добавлением $3$ сдвинутых и масштабированных импульсных функций.

В общем, сигнал может быть записан как сумма масштабированных и сдвинутых дельта-функций:
${\begin{align}x[n]=\sum_{k=0}^2 x[k]\cdot\delta[n-k]=x[0]\cdot\delta[n-0]+x[1]\cdot\delta[n-1]+x[2]\cdot\delta[n-2]\end{align}}$

## Импульсивный отклик
**Импульсный отклик** $-$ это выход системы, являющийся результатом импульсной функции в качестве входа.
Именно это обозначается за $h[n]$. Все волшебство именно тут. Для вас это перестанет быть магией на 2-3 курсе инстиута.

Существует огромное множество преобразования для импульсного отклика. Мы рассмотрим только конечные формулы без понимания того, как работает математика.

## Свертка в 2D
2D свертка является просто продолжением предыдущей 1D свертки путем свертки как горизонтального, так и вертикального направлений в двухмерной пространственной области. Свертка часто используется для обработки изображений, таких как сглаживание, повышение резкости и обнаружение краев изображений.

Импульсная (дельта) функция также находится в двумерном пространстве, поэтому ${\delta[m,n]={\begin{cases}1,\ m=n=0\\0,\ m\ne0,n\ne0\end{cases}}}$. Импульсный отклик в 2D обычно называют **«ядром»** или **«фильтром»** в обработке изображений.

Кроме того, выходные данные линейной и инвариантной по времени системы могут быть записаны путем свертки входного сигнала $x[m,n]$ и импульсной характеристики $h[m,n]$:
${\begin{align}y[m,n]=x[m,n]*h[m,n]=\sum_{j=-\infty}^\infty \sum_{i=-\infty}^\infty x[i,j]\cdot h[m-i,n-j]\end{align}}$

Изучим пример, чтобы уточнить, как работает свертка в **2D**-пространстве.
Допустим, что размер импульсного отклика (ядра) составляет $3\times3$, а его значения **a, b, c, d, ...**

Обратите внимание, что начало координат $(0,0)$ находится в центре ядра.

Давайте выберем простейший пример и вычислим свертку, например, результат в $(1,1)$:

${\begin{align}
y[1,1] & = \sum_{j=0}^2 \sum_{i=0}^2 x[i,j] \cdot h[1-i,1-j]  \\ &
= x[0,0] \cdot h[1,1] + x[1,0] \cdot h[0,1] + x[2,0] \cdot h[-1,1] \\ &
+ x[0,1] \cdot h[1,0] + x[1,1] \cdot h[0,0] + x[2,1] \cdot h[-1,0] \\ &
+ x[0,2] \cdot h[1,-1] + x[1,2] \cdot h[0,-1] + x[2,2] \cdot h[-1,-1]
\end{align}}$

Рассмотрим более конкртеный пример с двумерной сверткой. Предположим, у нас есть входные матрицы $3\times3$ и ядра $3\times3$ следующим образом:

Input | Kernel | Output
-|-|-
 |  |
Выходной сигнал в точке $(1,1)$ для этого примера будет:
${\begin{align}
\ y[1,1] & = \sum_{j=0}^2 \sum_{i=0}^2 x[i,j] \cdot h[1-i,1-j]  \\ &
\ = x[0,0] \cdot h[1,1] + x[1,0] \cdot h[0,1] + x[2,0] \cdot h[-1,1] \\ &
\ + x[0,1] \cdot h[1,0] + x[1,1] \cdot h[0,0] + x[2,1] \cdot h[-1,0] \\ &
\ + x[0,2] \cdot h[1,-1] + x[1,2] \cdot h[0,-1] + x[2,2] \cdot h[-1,-1] \\&
\ = 1 \cdot 1 + 2 \cdot 2 + 3 \cdot 1 \\ &
\ + 4 \cdot 0 + 5 \cdot 0 + 6 \cdot 0 \\ &
\ + 7 \cdot (-1) + 8 \cdot (-2) + 9 \cdot (-1) \\ &
\ = -24 \\
\end{align}}$

## Convolution animations

_N.B.: Blue maps are inputs, and cyan maps are outputs._

    No padding, no strides
    Arbitrary padding, no strides
    Half padding, no strides
    Full padding, no strides

    No padding, strides
    Padding, strides
    Padding, strides (odd)

## Transposed convolution animations

_N.B.: Blue maps are inputs, and cyan maps are outputs._

    No padding, no strides, transposed
    Arbitrary padding, no strides, transposed
    Half padding, no strides, transposed
    Full padding, no strides, transposed

    No padding, strides, transposed
    Padding, strides, transposed
    Padding, strides, transposed (odd)

## Dilated convolution animations

_N.B.: Blue maps are inputs, and cyan maps are outputs._

    No padding, no stride, dilation

*Источник: https://github.com/vdumoulin/conv_arithmetic*

## 2D Свертки в OpenCV (Фильтрация изображений)

Как и в одномерных сигналах, изображения также могут быть отфильтрованы с помощью различных низкочастотных фильтров (LPF), высокочастотных фильтров (HPF) и т.д.
**LPF** помогает удалять шумы, размывать изображения. Фильтры **HPF** помогают находить края в изображений.

**OpenCV** предоставляет функцию **cv2.filter2D()** для объединения ядра с изображением. В качестве примера мы попробуем усредняющий фильтр на изображении. Ядро усредняющего фильтра $5\times5$ будет выглядеть так:

${\begin{equation*}
\ K = \frac{1}{25} \begin{pmatrix}
1 & \ 1 & \ 1 & \ 1 & \ 1 \\
1 & \ 1 & \ 1 & \ 1 & \ 1 \\
1 & \ 1 & \ 1 & \ 1 & \ 1 \\
1 & \ 1 & \ 1 & \ 1 & \ 1 \\
1 & \ 1 & \ 1 & \ 1 & \ 1
\end{pmatrix}
\end{equation*}}$

Свертка выполняется согласно теоретическим примерам выше. Как результат мы получаем осредненные пиксели своими 25 соседями.

```python
import numpy as np
import cv2
import matplotlib.pyplot as plt

%matplotlib inline
```

```python
img = cv2.imread('img/8_ka.jpg')
## для отрисовки в pyplot
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
```

```python
kernel = np.ones((5,5), dtype=np.float32) / 25
dst = cv2.filter2D(img.copy(), -1, kernel)

fig, m_axs = plt.subplots(1, 2, figsize=(14, 10))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img)
ax2.set_title('Результат свертки')
ax2.imshow(dst);
```

## Размытие изображения (Сглаживание изображения)
Размытие изображения достигается путем свертывания изображения с помощью фильтра нижних частот. Это полезно для удаления шумов. Это фактически удаляет высокочастотный контент (например: шум, края) с изображения. Таким образом, края в этой операции немного размыты. (Ну, есть методы размытия, которые тоже не размывают края). **OpenCV** предоставляет в основном четыре типа техники размытия.

Более подробное описание реализации фильтров OpenCV есть в [документации](https://docs.opencv.org/3.4.2/d4/d86/group__imgproc__filter.html).

### 1. Усреднение
Это делается путем свертки изображения с помощью нормализованного прямоугольного фильтра. Он просто берет среднее значение всех пикселей в области ядра и заменяет центральный элемент. Это выполняется функцией **cv2.blur()** или **cv2.boxFilter()**. Для более подробной информации о ядре посмотрите в документации. Мы должны указать ширину и высоту ядра. Нормализованный блочный фильтр $3\times3$ будет выглядеть следующим образом:

${\begin{equation*}K = \frac{1}{9}
{\begin{pmatrix}
1 & \ 1 & \ 1 \\
1 & \ 1 & \ 1 \\
1 & \ 1 & \ 1 \end{pmatrix}}
\end{equation*}}$

```python
blur = cv2.blur(img,(3, 3))

fig, m_axs = plt.subplots(1, 2, figsize=(14, 10))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img)
ax2.set_title('Результат свертки')
ax2.imshow(blur);
```

### 2. Размытие по Гауссу
При этом вместо box filter используется ядро Гаусса. Это делается с помощью функции **cv2.GaussianBlur()**. Мы должны указать ширину и высоту ядра, которые должны быть положительными и нечетными. Мы также должны указать стандартное отклонение в направлениях **X** и **Y**, **sigmaX** и **sigmaY** соответственно. Если указан только **sigmaX**, то **sigmaY** принимается так же, как **sigmaX**. Если оба даны в виде нулей, они рассчитываются по размеру ядра. Размытие по Гауссу очень эффективно для удаления гауссовского шума с изображения.

Если вы хотите, вы можете создать ядро Гаусса с помощью функции **cv2.getGaussianKernel()**, а затем применить его с помощью **cv2.filter2D()**.

```python
## Код для размытия по Гауссу:

blur = cv2.GaussianBlur(img,(9,9),0)

fig, m_axs = plt.subplots(1, 2, figsize=(14, 10))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img)
ax2.set_title('Результат свертки')
ax2.imshow(blur);
```

### 3. Среднее размытие
Здесь функция **cv2.medianBlur()** принимает медиану всех пикселей в области ядра, и центральный элемент заменяется этим медианным значением. Это очень эффективно против шума соли и перца на изображениях. Интересно, что в вышеупомянутых фильтрах центральным элементом является вновь вычисленное значение, которое может быть значением пикселя в изображении или новым значением. Но при медианном размытии центральный элемент всегда заменяется некоторым пиксельным значением на изображении.

Эффективно снижает шум. Размер его ядра должен быть положительным нечетным целым числом.

В этой демонстрации я добавил шум к нашему исходному изображению и применил медианное размытие. Проверьте результат:

```python
## внесем шум
noise_img = img.copy()
mask = np.random.randint(0,2,size=noise_img.shape).astype(np.bool)
r = np.random.rand(*noise_img.shape)*np.max(noise_img)
noise_img[mask] = r[mask]

median = cv2.medianBlur(noise_img, 7)

fig, m_axs = plt.subplots(1, 2, figsize=(14, 10))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(noise_img)
ax2.set_title('Результат свертки')
ax2.imshow(median);
```

### 4. Двусторонняя фильтрация

**cv2.bilateralFilter()** очень эффективно удаляет шум, сохраняя края острыми. Но операция медленнее по сравнению с другими фильтрами. Мы уже видели, что фильтр Гаусса берет окрестность вокруг пикселя и находит его средневзвешенное значение по Гауссу. Этот гауссов фильтр является функцией только одного пространства, то есть при фильтрации учитываются соседние пиксели. Он не учитывает, имеют ли пиксели почти одинаковую интенсивность. Он не учитывает, является ли пиксель краевым или нет. Таким образом, это стирает края, что мы не хотим делать.

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

**Рекомендации к фильтру:**

* **Sigma values:** для простоты вы можете установить два значения сигмы одинаковыми. Если они маленькие ($<10$), фильтр не будет иметь большого эффекта, тогда как если они большие ($>150$), они будут иметь очень сильный эффект, делая изображение мультяшным.

* **Размер фильтра:** Большие фильтры (d$>5$) очень медленные, поэтому рекомендуется использовать d $=5$ для приложений реального времени и, возможно, d $=9$ для автономных приложений, которые нуждаются в сильной фильтрации шума.

Ниже приведены примеры использования двустороннего фильтра:

```python
## внесем шум
noise_img = img.copy()
mask = np.random.randint(0,2,size=noise_img.shape).astype(np.bool)
r = np.random.rand(*noise_img.shape)*np.max(noise_img)
noise_img[mask] = r[mask]

bilateral = cv2.bilateralFilter(noise_img, d=15, sigmaColor=250, sigmaSpace=250)

fig, m_axs = plt.subplots(1, 2, figsize=(14, 10))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(noise_img)
ax2.set_title('Результат свертки')
ax2.imshow(bilateral);
```

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

**Эрозия** (размывание/сужение) изображения обычно используется для избавления от случайных вкраплений на изображении. Идея состоит в том, что вкрапления при размывании устранятся, тогда как крупные и соответсвенно более визуально-значимые регионы остаются.

**Растягивание** (расширение) же, по идее, так же должно устранять шум и способствовать объединению областей изображения, которые были разделены шумом, тенями.
Применение же небольшого растягивания должно сплавить эти области в одну.

Морфологические операции, чаще всего, применяются над двоичными изображениями, которые получаются после порогового преобразования (thresholding).
***
**Подробнее с математической морфологией можно ознакомиться по этим ссылка: [1](https://ru.wikipedia.org/wiki/%CC%E0%F2%E5%EC%E0%F2%E8%F7%E5%F1%EA%E0%FF_%EC%EE%F0%F4%EE%EB%EE%E3%E8%FF), [2](https://habr.com/post/113626/), [3](http://wiki.technicalvision.ru/index.php/%D0%9C%D0%BE%D1%80%D1%84%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5_%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B8_%D0%BD%D0%B0_%D0%B1%D0%B8%D0%BD%D0%B0%D1%80%D0%BD%D1%8B%D1%85_%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F%D1%85).**

### 1. Эрозия
Основная идея эрозии похожа только на эрозию почвы, она размывает границы объекта переднего плана (всегда старайтесь, чтобы передний план оставался белым). Так, что это делает? Ядро скользит по изображению (как в **2D**-свертке). Пиксель в исходном изображении ($1$ или $0$) будет считаться $1$, только если все пиксели под ядром равны $1$, в противном случае он размыт (обнуляется).

Итак, что происходит, так это то, что все пиксели вблизи границы будут отбрасываться в зависимости от размера ядра. Таким образом, толщина или размер объекта переднего плана уменьшается или просто белая область уменьшается на изображении. Это полезно для удаления небольших белых шумов (как мы видели в главе о цветовом пространстве), отсоединения двух связанных объектов и т. д.

Здесь, в качестве примера, я бы использовал полное ядро $5\times5$. Давайте посмотрим, как это работает:

```python
img = cv2.imread('img/j.png')

kernel = np.ones((5,5),np.uint8)
erosion = cv2.erode(img,kernel,iterations = 1)

fig, m_axs = plt.subplots(1, 2, figsize=(6, 4))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Результат свертки')
ax2.imshow(erosion, cmap='gray');
```

### 2. Растягивание
Это просто противоположность эрозии. Здесь пиксельный элемент равен «$1$», если хотя бы один пиксель под ядром равен «$1$». Таким образом, увеличивается белая область в изображении или увеличивается размер объекта переднего плана. Обычно в таких случаях, как удаление шума, за эрозией следует расширение. Потому что эрозия удаляет белые шумы, но также уменьшает наш объект. Таким образом, мы расширяем это. Поскольку шум исчез, они не вернутся, но наша площадь объекта увеличивается. Это также полезно при соединении сломанных частей объекта.

```python
dilation = cv2.dilate(img,kernel,iterations = 1)

fig, m_axs = plt.subplots(1, 2, figsize=(6, 4))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Результат свертки')
ax2.imshow(dilation, cmap='gray');
```

### 3. Открытие
Открытие - это просто еще одно название эрозии, за которой следует расширение. Это полезно для удаления шума, как мы объяснили выше. Здесь мы используем функцию **cv2.morphologyEx()**.

```python
## внесем шум
noise_img = img.copy()
mask = np.random.randint(0,2,size=noise_img.shape).astype(np.bool)
r = np.ones(noise_img.shape) * 255
noise_img[mask] = r[mask]

opening = cv2.morphologyEx(noise_img, cv2.MORPH_OPEN, kernel)

fig, m_axs = plt.subplots(1, 2, figsize=(6, 4))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Результат свертки')
ax2.imshow(opening, cmap='gray');
```

### 4. Закрытие
Закрытие является обратным открытию, растягивание с последующей эрозией. Это полезно при закрытии небольших отверстий внутри объектов переднего плана или маленьких черных точек на объекте.

```python
## внесем шум
noise_img = img.copy()
mask = np.random.randint(0,2,size=noise_img.shape).astype(np.bool)
r = np.zeros(noise_img.shape)
noise_img[mask] = r[mask]

opening = cv2.morphologyEx(noise_img, cv2.MORPH_CLOSE, kernel)

fig, m_axs = plt.subplots(1, 2, figsize=(6, 4))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Результат свертки')
ax2.imshow(opening, cmap='gray');
```

### 5. Морфологический градиент
Это разница между расширением и размыванием изображения.

Результат будет выглядеть как контур объекта.

```python
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)

fig, m_axs = plt.subplots(1, 2, figsize=(6, 4))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Результат свертки')
ax2.imshow(gradient, cmap='gray');
```

## Simple Thresholding
Здесь все просто. Если значение пикселя больше порогового значения, ему присваивается одно значение (может быть белым), в противном случае ему присваивается другое значение (может быть черным). Используемая функция **cv2.threshold(src, thresh, maxval, type)**

* **src** $-$ исходное изображение, которое должно быть изображением в градациях серого
* **thresh** $-$ пороговое значение, которое используется для классификации значений пикселей
* **maxval** $-$ представляет значение, которое будет дано, если значение пикселя больше (иногда меньше) порогового значения
* **type** $-$ предоставляет различные стили порогового значения.

Различные типы:
1. cv2.THRESH_BINARY
2. cv2.THRESH_BINARY_INV
3. cv2.THRESH_TRUNC
4. cv2.THRESH_TOZERO
5. cv2.THRESH_TOZERO_INV

На выходе функция возвращает два значения. Первый $-$ **retval**, которое будет объяснено позже. Второй $-$ **thresholded image**.

Подробнее в [документации](https://docs.opencv.org/3.4.2/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57).

Рассмотрим разные типы threshold на примере градиента серого цвета.

```python
img = cv2.imread('img/grad_grayscale.png',0)

ret,thresh1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
ret,thresh3 = cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
ret,thresh4 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
ret,thresh5 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
plt.figure(figsize=(10,8))
for i in range(6):
    plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
    plt.title(titles[i])
    plt.xticks([]),plt.yticks([])
plt.show()
```

## Adaptive Thresholding
В предыдущем разделе мы использовали глобальное значение в качестве порогового значения. Но это может быть не хорошо во всех условиях, когда изображение имеет разные условия освещения в разных областях. В этом случае мы идем на адаптивный порог. При этом алгоритм вычисляет порог для небольших областей изображения. Таким образом, мы получаем разные пороговые значения для разных областей одного и того же изображения, и это дает нам лучшие результаты для изображений с разной освещенностью.

Он имеет три «специальных» входных параметра и только один выходной аргумент.

Адаптивный метод $-$ **cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)** - решает, как рассчитывается пороговое значение.

* **thresholdType**:
    * *cv2.ADAPTIVE_THRESH_MEAN_C*: пороговое значение является средним значением области соседства.

    * *cv2.ADAPTIVE_THRESH_GAUSSIAN_C*: пороговое значение представляет собой взвешенную сумму значений окрестностей, где веса представляют собой гауссово окно.

* **blockSize** $-$ определяет размер окна.

* **C** $-$ это константа, которая вычитается из вычисленного среднего или взвешенного среднего.

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

### Гауссово окно? Что?

Гауссова функция (гауссиан, гауссиана, функция Гаусса) — вещественная функция, описываемая следующей формулой:

${\displaystyle g\left(x\right)=a*exp{-{\frac {(x-b)^{2}}{2c^{2}}}}}$

Эта функция имеет широкое приминение в математической статистике. В таком случае параметры выражаются через среднеквадратичное отклонение ${\displaystyle \sigma }$ и математическое ожидание ${\displaystyle \mu }$ :

${\displaystyle a={\frac {1}{\sigma {\sqrt {2\pi }}}}}, {\displaystyle b=\mu }, {\displaystyle c=\sigma }$

В двумерном виде она имеет такое представление:

${\displaystyle g(x,y)=A\cdot exp{\left(-\left({\frac {(x-x_{0})^{2}}{2\sigma _{x}^{2}}}+{\frac {(y-y_{0})^{2}}{2\sigma _{y}^{2}}}\right)\right)}}$

Стандартным статистическим способом мы определили ширину гауссовой формы в терминах $\sigma$. Тем не менее, когда для сглаживания используется гауссиан, для сканеров характерно описывать ширину гауссиана с помощью другой связанной меры - полной ширины на половине максимума (FWHM).

FWHM - ширина ядра, равная половине максимума высоты гауссианы.

```python
from mpl_toolkits.mplot3d import Axes3D

def fwhm2sigma(fwhm):
    return fwhm / np.sqrt(8 * np.log(2))
FWHM = 1
sigma = fwhm2sigma(FWHM)

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
dx = 0.1
dy = 0.1
x = np.arange(-6, 6, dx)
y = np.arange(-6, 6, dy)
x2d, y2d = np.meshgrid(x, y)
kernel_2d = np.exp(-(x2d ** 2 + y2d ** 2) / (2 * sigma ** 2))
kernel_2d = kernel_2d / (2 * np.pi * sigma ** 2) # unit integral
ax.plot_surface(x2d, y2d, kernel_2d)
plt.show()
```

Таким образом, можно представить, что окно гаусса представляет из себя проекцию функции на плоскость. Проиллюстрировано на картинке:

Собственно, теперь перейдем к __threshold__.

```python
img = cv2.imread('img/sudoku.jpg')
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
img = cv2.medianBlur(img,5)
ret,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
th2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,2)
th3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,11,2)
titles = ['Original Image', 'Global Thresholding (v = 127)',
            'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]
plt.figure(figsize=(10,8))
for i in range(4):
    plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')
    plt.title(titles[i])
    plt.xticks([]),plt.yticks([])
plt.show()
```

## Бинаризация Оцу
В первом разделе я говорил вам, что есть второй параметр retVal. Его использование происходит, когда мы идем на бинаризацию Оцу. Так что же это?

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

Для этого используется наша функция **cv2.threshold()**, но передается дополнительный флаг *cv2.THRESH_OTSU*. Для порогового значения просто введите ноль. Затем алгоритм находит оптимальное пороговое значение и возвращает вас в качестве второго выхода retVal. Если пороговое значение Otsu не используется, **retVal** соответствует пороговому значению, которое вы использовали.

Проверьте ниже пример. Входное изображение является шумным изображением. В первом случае я применил глобальный порог для значения $127$. Во втором случае я применил порог Оцу напрямую. В третьем случае я отфильтровал изображение с гауссовым ядром $5\times5$, чтобы удалить шум, затем применил пороговое значение Оцу. Посмотрите, как фильтрация шума улучшает результат.

```python
img = cv2.imread('img/otsu_ex.jpg')
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
noise_img = img.copy()
mask = np.random.randint(0,2,size=noise_img.shape).astype(np.bool)
r = np.random.rand(*noise_img.shape)*np.max(noise_img)
noise_img[mask] = r[mask]

img = noise_img

# global thresholding
ret1,th1 = cv2.threshold(img,170,255,cv2.THRESH_BINARY)
# Otsu's thresholding
ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# Otsu's thresholding after Gaussian filtering
blur = cv2.GaussianBlur(img,(5,5),0)
ret3,th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# plot all the images and their histograms
images = [img, 0, th1,
          img, 0, th2,
          blur, 0, th3]
ret = [ret1, ret2, ret3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
          'Original Noisy Image','Histogram',"Otsu's Thresholding",
          'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
plt.figure(figsize=(14,10))
for i in range(3):
    plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
    plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
    plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
    plt.title(titles[i*3+1] + ', threshold =' + str(ret[i])), plt.xticks([]), plt.yticks([])
    plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
    plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()
```

## Почему Гаусс и при чем тут aliasing?

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

Проясним на примере:

    Изображение без сжатия
    Эффект alising от сжатия

## Image Pyramids

Обычно мы работали с изображением постоянного размера. Но в некоторых случаях нам нужно работать с изображениями разного разрешения одного и того же изображения. Например, при поиске чего-либо на изображении, например лица, мы не уверены, в каком размере будет присутствовать объект на изображении. В этом случае нам нужно будет создать набор изображений с разным разрешением и выполнить поиск объекта по всем изображениям. Эти наборы изображений с разным разрешением называются пирамидами изображений (потому что, когда они хранятся в стопке с самым большим изображением внизу и самым маленьким изображением сверху, они выглядят как пирамида).

Существует два вида пирамид изображений:
1. Пирамида Гаусса
2. Пирамиды Лапласа

Более высокий уровень (низкое разрешение) в гауссовой пирамиде формируется путем удаления последовательных строк и столбцов в изображении нижнего уровня (более высокое разрешение). Затем каждый пиксель на более высоком уровне формируется вкладом из 5 пикселей на базовом уровне с гауссовыми весами. Таким образом, изображение $M \times N$ становится изображением $M / 2 \times N / 2$. То есть, площадь уменьшается до четверти первоначальной площади. Это называется Октава. Та же самая картина продолжается, когда мы идем вверх в пирамиде (то есть разрешение уменьшается). Аналогично, при расширении область становится 4 раза на каждом уровне. Мы можем найти гауссовы пирамиды, используя функции ```cv2.pyrDown()``` и ```cv2.pyrUp()```.

```python
img = cv2.imread('img/8_ka.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

h, w, _ = img.shape
zero_back = np.zeros((h, w, 3), np.uint8)

lower_reso = cv2.pyrDown(img)
h, w, _ = lower_reso.shape
zero_back[:h, :w, :] = lower_reso
```

```python
fig, m_axs = plt.subplots(1, 2, figsize=(12, 9))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Пирамида')
ax2.imshow(zero_back, cmap='gray');
```

Теперь вы можете перейти вниз по пирамиде изображения с помощью функции ```cv2.pyrUp()```.

```python
higher_reso = cv2.pyrUp(lower_reso)
```

```python
fig, m_axs = plt.subplots(1, 2, figsize=(12, 9))
ax1, ax2 = m_axs

ax1.set_title('Исходное изображение')
ax1.imshow(img, cmap='gray')
ax2.set_title('Пирамида')
ax2.imshow(higher_reso, cmap='gray');
```

Лапласовские пирамиды образованы из гауссовых пирамид. Для этого нет специальной функции. Изображения пирамиды Лапласа подобны только краевым изображениям. Большинство его элементов - нули. Они используются в сжатии изображений. Уровень в лапласовой пирамиде формируется разницей между этим текущим в гауссовой пирамиде и ее расширенной версией верхнего уровня в гауссовой пирамиде.

```python
# Генерируем пирамиды Гаусса
G = img.copy()
gpA = [G]
for i in range(6):
    G = cv2.pyrDown(G)
    gpA.append(G)

# Генерируем пирамиды Лапласа
lpA = [gpA[5]]
for i in range(5,0,-1):
    GE = cv2.pyrUp(gpA[i])
    h_ge, w_ge, _ = GE.shape
    h_a, w_a, _ = gpA[i-1].shape
    print(f'Разница между высотой и шириной:', abs(h_ge-h_a), abs(w_ge-w_a))
    # делаем падинг для согласования размеров изображений
    gpA[i-1] = np.pad(gpA[i-1], pad_width=[(0, abs(h_ge-h_a)), (0, abs(w_ge-w_a)), (0, 0)], mode='edge')
    print(f'Размеры изображений после падинга: {GE.shape, gpA[i-1].shape}\n')
    L = cv2.subtract(gpA[i-1],GE)
    lpA.append(L)
```

```python
# Визуализация пирамид
fig, m_axs = plt.subplots(2, 3, figsize=(24, 15))
m_axs = m_axs.ravel(order='C')

for i, ax in enumerate(m_axs):
    ax.imshow(lpA[i])
```

## Blending with pyramids

Одним из применений пирамид является **смешивание изображений (blending)**. Например, при сшивании изображений вам нужно будет сложить два изображения вместе, но это может выглядеть не очень хорошо из-за разрыва между изображениями. В этом случае смешивание изображений с пирамидами обеспечивает плавное смешивание, не оставляя большого количества данных на изображениях. Одним из классических примеров этого является смешивание двух фруктов, апельсина и яблока. Теперь посмотрите на результат, чтобы понять, что я говорю:

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

* Загрузите два изображения яблока и апельсина
* Найдите гауссовы пирамиды для яблок и апельсинов (в данном конкретном примере количество уровней равно 6)
* Из гауссовых пирамид, найдите их лапласианские пирамиды
* Теперь соедините левую половину яблока и правую половину апельсина на каждом уровне лапласианских пирамид
* Наконец из этого совместного изображения пирамид, восстановить исходное изображение

### Практика

Смешать два изображения (blending) с помощью пирамид. Примеры изображений загружены ниже.

```python
img_left = cv2.imread('img/cap_1.jpg')
img_left = cv2.cvtColor(img_left, cv2.COLOR_BGR2RGB)
img_right = cv2.imread('img/cap_2.jpg')
img_right = cv2.cvtColor(img_right, cv2.COLOR_BGR2RGB)

fig, m_axs = plt.subplots(1, 2, figsize=(12, 9))
ax1, ax2 = m_axs

ax1.set_title('Изображение раз')
ax1.imshow(img_left)
ax2.set_title('Изображение два')
ax2.imshow(img_right);
```

```python
# Ваш код
```

```python

```


```python
import tensorflow as tf
import tensorflow_probability as tfp
tfd = tfp.distributions

print("TF version:", tf.__version__)
print("TFP version:", tfp.__version__)
```

    TF version: 2.4.1
    TFP version: 0.12.1

```python
# Additional imports and setting fixed random seed to have reproducibility

import matplotlib.pyplot as plt
import numpy as np
tf.random.set_seed(123)
```

# Tensorflow Distributions

## Coding Tutorials
#### 1. [Univariate Distributions](#univariate_distributions)
#### 2. [Multivariate Distributions](#multivariate_distributions)
#### 3. [The Independent Distribution](#the_independent_distribution)
#### 4. [Sampling and log probs](#sampling_and_log_probs)
#### 5. [Trainable Distributions](#trainable_distributions)

***
## Univariate distributions

```python
# Create a normal distribution from Tensorflow Distributions

```

```python
# Sample from the chosen distribution...

```

```python
# ... or sample multiple times

```

```python
# Obtain value of probability's density

```

```python
# Obtain value of logprobability

```

```python
# Verify that this really is the log of the probability

```

```python
# Plot a histogram, approximating the density

plt.hist(normal.sample(10000), bins=50, density=True)
plt.show()
```

```python
# Do the same for the exponential distribution

```

```python
# Sample as before

```

```python
# Create a Bernoulli distribution (discrete)

```

#### A word of caution on discrete distributions

```python
# Calculate Bernoulli prob and see that 0.5 and -1 do not give the correct probability!

for k in [0,0.5,1,-1]:
    print('prob result {} for k = {} '.format())
```

```python
# Replicate the scores to see what is occurring under the hood

def my_bernoulli(p_sucess, k):
    return np.power(p_sucess,k)*np.power((1-p_sucess),(1-k))
```

```python
# Evaluate it as before

for k in [0,0.5,1,-1]:
    print('prob result {} for k = {} '.format())
```

#### Work with batch distributions

```python
# Create a batched Bernoulli distribution

bernoulli_batch = tfd.Bernoulli(probs=[0.1, 0.25, 0.5, 0.75, 0.9])
bernoulli_batch
```

```python
# Sample from it, noting the shape

```

```python
# Use a batch shape with higher rank

probs = [[[0.5, 0.5],
          [0.8, 0.3],
          [0.25, 0.75]]]
bernoulli_batch_2D = tfd.Bernoulli(probs=probs)
bernoulli_batch_2D
```

```python
# Sample from this batch of distributions

```

```python
# Determine probabilities from this batch distribution

bernoulli_batch_2D.prob([[[1, 0],
                         [0, 0],
                         [1, 1]]])
```

***

## Multivariate Distributions

#### Basic multivariate distributions

```python
# Define 2D multivariate Gaussian with diagonal covariance matrix

normal_diag = tfd.MultivariateNormalDiag(loc=[0, 1], scale_diag=[1, 2])
normal_diag
```

    <tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[] event_shape=[2] dtype=float32>

```python
# Sample from it

normal_diag.sample(10)
```

    <tf.Tensor: shape=(10, 2), dtype=float32, numpy=
    array([[-0.8980837 , -2.6518288 ],
           [-0.44441807, -1.9765894 ],
           [-0.7855463 ,  1.3923931 ],
           [ 0.17604655, -2.050501  ],
           [ 0.635294  ,  2.3362408 ],
           [ 1.4230527 ,  1.0912329 ],
           [-0.21692705, -2.416981  ],
           [ 0.5174161 ,  0.76055366],
           [-1.0614088 , -0.08182514],
           [ 0.08606925, -1.5563867 ]], dtype=float32)>

```python
# Make a plot

plt_sample = normal_diag.sample(10000)
plt.scatter(plt_sample[:, 0], plt_sample[:, 1], marker='.', alpha=0.05)
plt.axis('equal')
plt.show()
```

#### Batches of multivariate distributions

```python
# Create three "batches" of multivariate normals

normal_diag_batch = tfd.MultivariateNormalDiag(loc=[[0, 0], [0, 0], [0, 0]],
                                               scale_diag=[[1, 2], [2, 1], [2, 2]])
normal_diag_batch
```

    <tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[3] event_shape=[2] dtype=float32>

```python
# Sample from it

samples = normal_diag_batch.sample(5)
samples
```

    <tf.Tensor: shape=(5, 3, 2), dtype=float32, numpy=
    array([[[-0.6383433 , -2.6664126 ],
            [-2.2985559 , -0.82325643],
            [-1.7312855 , -1.5238538 ]],

           [[ 0.4257505 , -3.5045025 ],
            [ 5.785669  , -0.17786829],
            [-0.68529975, -1.88765   ]],

           [[ 0.11156012,  2.1197336 ],
            [ 1.6149896 ,  0.77937406],
            [-0.64763886,  0.00861878]],

           [[ 0.43864295,  0.8764439 ],
            [-1.2682985 ,  0.35502046],
            [-2.6530807 ,  2.602631  ]],

           [[ 0.13832839, -0.82305825],
            [ 1.582189  ,  0.00951999],
            [ 3.270198  , -0.32847282]]], dtype=float32)>

```python
# Compute log probs

normal_diag_batch.log_prob(samples)
```

    <tf.Tensor: shape=(5, 3), dtype=float32, numpy=
    array([[-3.6234846, -3.5303197, -3.8891063],
           [-4.156848 , -6.7310886, -3.7282786],
           [-3.0989058, -3.1607602, -3.2766101],
           [-2.7232473, -2.7951164, -4.950737 ],
           [-2.6252697, -2.8439846, -4.5744324]], dtype=float32)>

```python
# Create a sample for a plot -- notice the shape

plt_sample_batch = normal_diag_batch.sample(10000)
plt_sample_batch.shape
```

    TensorShape([10000, 3, 2])

```python
# Plot samples from the batched multivariate Gaussian

fig, axs = (plt.subplots(1, 3, sharex=True, sharey=True, figsize=(10, 3)))
titles = ['cov_diag=[1, 2]','cov_diag=[2, 1]', 'cov_diag=[2, 2]']

for i, (ax, title) in enumerate(zip(axs,titles)):
    samples = plt_sample_batch[:,i,:] #take the ith batch [samples x event_shape]
    ax.scatter(samples[:, 0], samples[:, 1], marker='.', alpha=0.05)
    ax.set_title(title)
plt.show()
```

***

## The Independent Distribution

```python
# Start by defining a batch of two univariate Gaussians, then
# combine them into a bivariate Gaussian with independent components

```

```python
# Univariate density functions

import seaborn as sns

t = np.linspace(-4, 4, 10000)
densities = batch_of_normals.prob(np.repeat(t[:, np.newaxis], 2, axis=1)) # each column is a vector of densities for one distn

sns.lineplot(t, densities[:, 0], label='loc={}, scale={}'.format(locs[0], scales[0]))
sns.lineplot(t, densities[:, 1], label='loc={}, scale={}'.format(locs[0], scales[0]))
plt.ylabel('Probability density')
plt.xlabel('Value')
plt.legend()
plt.show()
```

```python
# Check their batch_shape and event_shape

```

```python
# Use Independent to convert the batch shape to the event shape

```

```python
# Note that dimension from batch_shape has shifted to event_shape

```

```python
# Create a plot showing joint density contours and marginal density functions

samples = bivariate_normal_from_Independent.sample(10000)
x1 = samples[:, 0]
x2 = samples[:, 1]
sns.jointplot(x1, x2, kind="kde", space=0, color='b', xlim=[-4, 4], ylim=[-4, 4])
```

```python
# Use MultivariateNormalDiag to create the equivalent distribution
# Note that diagonal covariance matrix => no correlation => independence (for the multivariate normal distribution)

```

```python
# Plot the joint density function of bivariate_normal_from_Independent
# Refer back to bivariate_normal_from_Independent to show that the plot is the same
# Summarise how Independent has been used

samples = bivariate_normal_from_Multivariate.sample(10000)
x1 = samples[:, 0]
x2 = samples[:, 1]
sns.jointplot(x1, x2, kind="kde", space=0, color='b', xlim=[-4, 4], ylim=[-4, 4])
```

#### Shifting batch dimensions to event dimensions using
`reinterpreted_batch_ndims`

```python
# Demonstrate use of reinterpreted_batch_ndims
# By default all batch dims except the first are transferred to event dims

loc_grid = [[-100., -100.],
            [100., 100.],
            [0., 0.]]
scale_grid = [[1., 10.],
              [1., 10.],
              [1., 1.]]

normals_batch_3by2_event_1 = tfd.Normal(loc=loc_grid, scale=scale_grid)
```

```python
# Highlight batch_shape

```

```python
# We now have a batch of 3 bivariate normal distributions,
# each parametrised by a column of our original parameter grid

```

```python
# Evaluate log_prob

```

```python
# Can reinterpret _all_ batch dimensions as event dimensions

```

```python
# Take log_probs

```

#### Using `Independent` to build a Naive Bayes classifier

#### Introduction to `newsgroups` data set

In this tutorial, just load the dataset, fetch train/test splits, probably choose a subset of the data.

Construct the class conditional feature distribution (with Independent, using the Naive Bayes assumption) and sample from it.

We can just use the ML estimates for parameters, in later tutorials we will learn them.

#### Import the data

The dataset required for this coding tutorial can be downloaded from the following link:

https://drive.google.com/file/d/1ZdVZFZv3eEw3Duwjg4fvtYAsgySoCRIm/view?usp=sharing

You should store this file in Drive for use in this Colab notebook.

```python
# Run this cell to connect to your Drive folder

from google.colab import drive
drive.mount('/content/gdrive')
```

```python
# Convenience function for retrieving the 20 newsgroups data set

# Usenet was a forerunner to modern internet forums
# Users could post and read articles
# Newsgroup corresponded to a topic
# Example topics in this data set: IBM computer hardware, baseball
# Our objective is to use an article's contents to predict its newsgroup,
# a 20-class classification problem.

# 18000 newsgroups, posts on 20 topics
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer

```

```python
# Get the train data

```

```python
# More information about the data set

```

```python
# Example article

```

```python
# Associated label

```

```python
# Name of label

```

```python
# Preprocessing boilerplate

n_documents = len(newsgroups_data['data'])

count_vectorizer = CountVectorizer(input='content', binary=True,
                                   max_df=0.25, min_df=1.01/n_documents) # ignore common words, words that appear once
binary_bag_of_words = count_vectorizer.fit_transform(newsgroups_data['data']) # input is a list of strings

```

```python
# Check shape

```

```python
# Check that the fit has been successful

```

```python
# Dict that will be useful later

inv_vocabulary = {value:key for key, value in count_vectorizer.vocabulary_.items()}
```

#### A Naive Bayes classifier for `newsgroup`

Each feature vector $x$ is a list of indicators for whether a word appears in the article. $x_i$ is 1 if the $i$th word appears, and 0 otherwise. `inv_vocabulary` matches word indices $i$ to words.

Each label $y$ is a value in $0, 1, \ldots, 19$.

The parts of a naive Bayes classifier for this problem can be summarised as:

- A probability distribution for the feature vector by class, $p(x|y = j)$ for each $j = 0, 1, \ldots, 19$. These probability distributions are assumed to have independent components: we can factorize the joint probability as a product of marginal probabilities
\begin{equation}
    p(x|y = j) = \prod_{i=1}^d p(x_i|y = j)
\end{equation}
These marginal probability distributions are Bernoulli distributions, each of which has a single parameter $\theta_{ji} := p(x_i = 1|y = j)$. This parameter is the probability of observing word $i$ in an article of class $j$.

- We will use the Laplace smoothed maximum likelihood estimate to compute these parameters. Laplace smoothing involves adding small counts to every feature for each class. Else, if a feature did not appear in the training set of a class, but then we observed it in our test data the log probability would be undefined.

- A collection of class prior probabilities $p(y = j)$. These will be set by computing the class base rates in the training set.

- A function for computing the probability of class membership via Bayes' theorem:

\begin{equation}
    p(y = j|x) = \frac{p(x|y = j)p(y = j)}{p(x)}
\end{equation}

```python
# Compute the parameter estimates (adjusted fraction of documents in class that contain word)

n_classes = newsgroups_data['target'].max() + 1
y = newsgroups_data['target']
n_words = binary_bag_of_words.shape[1]

alpha = 1e-6 # parameters for Laplace smoothing

theta = np.zeros([n_classes, n_words]) # stores parameter values - prob. word given class
for c_k in range(n_classes): # 0, 1, ..., 19
    class_mask = (y == c_k)
    N = class_mask.sum() # number of articles in class
    theta[c_k, :] = (binary_bag_of_words[class_mask, :].sum(axis=0) + alpha)/(N + alpha*2)
```

```python
# Check whether the most probable word in each class is reasonable

most_probable_word_ix = theta.argmax(axis=1) # most probable word for each class

for j, ix in enumerate(most_probable_word_ix):
    print('Most probable word in class {} is "{}".'.format(newsgroups_data['target_names'][j],
                                                           inv_vocabulary[ix]))
```

```python
# Define a distribution for each class

```

```python
# Take a sample of words from each class

n_samples = 10

```

```python
# Choose a class

chosen_class = 15
newsgroups_data['target_names'][chosen_class]
```

```python
# Indicators for words that appear in the sample

class_sample = sample[:, chosen_class, :]
class_sample
```

```python
# Perform inverse transform to test quality of fit

```

***

## Sampling and log probs

```python
# Make Multivariate Distribution

normal_distributions = tfd.MultivariateNormalDiag(loc=[[0.5, 1], [0.1, 0], [0, 0.2]],
                                 scale_diag=[[2, 3], [1, 3], [4, 4]])
normal_distributions
```

```python
# Sample

```

```python
# Multivariate Normal batched Distribution
# We are broadcasting batch shapes of `loc` and `scal_diag`
# against each other

loc = [[[0.3, 1.5, 1.], [0.2, 0.4, 2.8]],
        [[2., 2.3, 8], [1.4, 1, 1.3]]]
scale_diag = [0.4, 1., 0.7]
normal_distributions = tfd.MultivariateNormalDiag(loc=loc, scale_diag=scale_diag)
normal_distributions
```

```python
# Use independent to move part of the batch shape

```

```python
# Draw some samples

```

```python
# `[B, E]` shaped input

```

```python
# `[E]` shaped input (broadcasting over batch size)

```

```python
#`[S, B, E]` shaped input (broadcasting over samples)

```

```python
# `[S, b, e]` shaped input, where [b, e] is broadcastable over [B, E]

```

#### Naive Bayes example

Lets now use what we have learned and continue the Naive Bayes classifier we were building last tutorial.

#### Import the data

The dataset required for this coding tutorial can be downloaded from the following link:

https://drive.google.com/file/d/1ZdVZFZv3eEw3Duwjg4fvtYAsgySoCRIm/view?usp=sharing

You should store this file in Drive for use in this Colab notebook.

```python
# Run this cell to connect to your Drive folder

from google.colab import drive
drive.mount('/content/gdrive')
```

```python
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import BernoulliNB
from sklearn.metrics import f1_score
```

```python
# Making a function get_data which:
#   1) Fetches the 20 newsgroup dataset
#   2) Performs a word count on the articles and binarizes the result
#   3) Returns the data as a numpy matrix with the labels

def get_data(categories):

    newsgroups_train_data = fetch_20newsgroups(data_home='/path/to/20_Newsgroup_Data/',
                                               subset='train', categories=categories)
    newsgroups_test_data = fetch_20newsgroups(data_home='/path/to/20_Newsgroup_Data/',
                                              subset='test', categories=categories)

    n_documents = len(newsgroups_test_data['data'])
    count_vectorizer = CountVectorizer(input='content', binary=True,max_df=0.25, min_df=1.01/n_documents)

    train_binary_bag_of_words = count_vectorizer.fit_transform(newsgroups_train_data['data'])
    test_binary_bag_of_words = count_vectorizer.transform(newsgroups_test_data['data'])

    return (train_binary_bag_of_words.todense(), newsgroups_train_data['target']),  (test_binary_bag_of_words.todense(), newsgroups_test_data['target'])
```

```python
# Defining a function to conduct Laplace smoothing. This adds a base level of probability for a given feature
# to occur in every class.

def laplace_smoothing(labels, binary_data, n_classes):
    # Compute the parameter estimates (adjusted fraction of documents in class that contain word)
    n_words = binary_data.shape[1]
    alpha = 1 # parameters for Laplace smoothing
    theta = np.zeros([n_classes, n_words]) # stores parameter values - prob. word given class
    for c_k in range(n_classes): # 0, 1, ..., 19
        class_mask = (labels == c_k)
        N = class_mask.sum() # number of articles in class
        theta[c_k, :] = (binary_data[class_mask, :].sum(axis=0) + alpha)/(N + alpha*2)

    return theta
```

```python
# Getting a subset of the 20 newsgroup dataset

categories = ['alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space']

(train_data, train_labels), (test_data, test_labels) = get_data(categories=categories)
smoothed_counts = laplace_smoothing(labels=train_labels, binary_data=train_data, n_classes=len(categories))
```

To now make our NB classifier we need to build three functions:
* Compute the class priors
* Build our class conditional distributions
* Put it all together and classify our data

```python
# Function which computes the prior probability of every class based on frequency of occurence in
# the dataset

def class_priors(n_classes, labels):
    counts = np.zeros(n_classes)
    for c_k in range(n_classes):
        counts[c_k] = np.sum(np.where(labels==c_k, 1, 0))
    priors = counts / np.sum(counts)
    print('The class priors are {}'.format(priors))
    return priors
```

```python
# Run the function

```

```python
# Now we will do a function that given the feature occurence counts returns a Bernoulli distribution of
# batch_shape=number of classes and event_shape=number of features.

```

```python
# The final function predict_sample which given the distribution, a test sample, and the class priors:
#   1) Computes the class conditional probabilities given the sample
#   2) Forms the joint likelihood
#   3) Normalises the joint likelihood and returns the log prob

```

#### Computing log_probs

```python
# Predicting one example from our test data

```

```python
# Loop over our test data and classify.

probabilities = []
for sample, label in zip(test_data, test_labels):
    probabilities.append(tf.exp(predict_sample(tf_dist, sample, priors)))

probabilities = np.asarray(probabilities)
predicted_classes = np.argmax(probabilities, axis =-1)
print('f1 ', f1_score(test_labels, predicted_classes, average='macro'))
```

```python
# Make a Bernoulli Naive Bayes classifier using sklearn with the same level of alpha smoothing.

clf = BernoulliNB(alpha=1)
clf.fit(train_data, train_labels)
pred = clf.predict(test_data)
print('f1 from sklean ', f1_score(test_labels, pred, average='macro'))
```

***

## Trainable Distributions

#### Import the data

The dataset required for this coding tutorial can be downloaded from the following link:

https://drive.google.com/file/d/1ZdVZFZv3eEw3Duwjg4fvtYAsgySoCRIm/view?usp=sharing

You should store this file in Drive for use in this Colab notebook.

```python
# Run this cell to connect to your Drive folder

from google.colab import drive
drive.mount('/content/gdrive')
```

```python
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import BernoulliNB
from sklearn.metrics import f1_score
```

```python
# Define an exponential distribution

```

```python
# Plot

plt.hist(exponential.sample(5000), bins=100, density=True)
plt.show()
```

```python
# Define an exponential distribution with a trainable rate parameter

```

```python
# Define the negative log likelihood

```

```python
# Define a function to compute the loss and gradients

```

```python
# Optimize

def exponential_dist_optimisation(data, distribution):

    # Keep results for plotting
    train_loss_results = []
    train_rate_results = []

    optimizer = tf.keras.optimizers.SGD(learning_rate=0.05)

    num_steps = 10

    for i in range(num_steps):

        print("Step {:03d}: Loss: {:.3f}: Rate: {:.3f}".format())

    return train_loss_results, train_rate_results
```

```python
# Get some data and train

```

```python
# Predicted value for the rate parameter

pred_value = exp_train.rate.numpy()
exact_value = exponential.rate.numpy()

print("Exact rate: ", exact_value)
print("Pred rate:  ", pred_value)
```

```python
# Plot to see the convergence of the estimated and true parameters

tensor_exact_value = tf.constant(exact_value, shape=[len(train_rate_results)])

fig, axes = plt.subplots(2, sharex=True, figsize=(12, 8))
fig.suptitle('Convergence')

axes[0].set_ylabel("Loss", fontsize=14)
axes[0].plot(train_loss_results)

axes[1].set_ylabel("Rate", fontsize=14)
axes[1].set_xlabel("Epoch", fontsize=14)
axes[1].plot(train_rate_results, label='trainable rate variable')
axes[1].plot(tensor_exact_value, label='exact rate')
axes[1].legend()
plt.show()
```

```python
# Making a function get_data which:
#   1) Fetches the 20 newsgroup dataset
#   2) Performs a word count on the articles and binarizes the result
#   3) Returns the data as a numpy matrix with the labels

def get_data(categories):

    newsgroups_train_data = fetch_20newsgroups(data_home='/path/to/20_Newsgroup_Data/',
                                               subset='train', categories=categories)
    newsgroups_test_data = fetch_20newsgroups(data_home='/path/to/20_Newsgroup_Data/',
                                              subset='test', categories=categories)

    n_documents = len(newsgroups_test_data['data'])
    count_vectorizer = CountVectorizer(input='content', binary=True,max_df=0.25, min_df=1.01/n_documents)
    train_binary_bag_of_words = count_vectorizer.fit_transform(newsgroups_train_data['data'])
    test_binary_bag_of_words = count_vectorizer.transform(newsgroups_test_data['data'])

    return (train_binary_bag_of_words.todense(), newsgroups_train_data['target']),  (test_binary_bag_of_words.todense(), newsgroups_test_data['target'])

```

```python
# Defining a function to conduct laplace smoothing. This adds a base level of probability for a given feature
# to occur in every class.

def laplace_smoothing(labels, binary_data, n_classes):
    # Compute the parameter estimates (adjusted fraction of documents in class that contain word)
    n_words = binary_data.shape[1]
    alpha = 1 # parameters for Laplace smoothing
    theta = np.zeros([n_classes, n_words]) # stores parameter values - prob. word given class
    for c_k in range(n_classes): # 0, 1, ..., 19
        class_mask = (labels == c_k)
        N = class_mask.sum() # number of articles in class
        theta[c_k, :] = (binary_data[class_mask, :].sum(axis=0) + alpha)/(N + alpha*2)

    return theta
```

```python
# Now we will do a function that given the feature occurence counts returns a Bernoulli distribution of
# batch_shape=number of classes and event_shape=number of features.

def make_distributions(probs):
    batch_of_bernoullis = tfd.Bernoulli(probs=probs) # shape (n_classes, n_words)
    dist = tfd.Independent(batch_of_bernoullis, reinterpreted_batch_ndims=1)
    return dist
```

```python
# Function which computes the prior probability of every class based on frequency of occurence in
# the dataset

def class_priors(n_classes, labels):
    counts = np.zeros(n_classes)
    for c_k in range(n_classes):
        counts[c_k] = np.sum(np.where(labels==c_k, 1, 0))
    priors = counts / np.sum(counts)
    print('The class priors are {}'.format(priors))
    return priors
```

```python
# The final function predict_sample which given the distribution, a test sample, and the class priors:
#   1) Computes the class conditional probabilities given the sample
#   2) Forms the joint likelihood
#   3) Normalises the joint likelihood and returns the log prob

def predict_sample(dist, sample, priors):
    cond_probs = dist.log_prob(sample)
    joint_likelihood = tf.add(np.log(priors), cond_probs)
    norm_factor = tf.math.reduce_logsumexp(joint_likelihood, axis=-1, keepdims=True)
    log_prob = joint_likelihood - norm_factor

    return log_prob
```

```python
# Now we learn the distribution using gradient tape

def make_distribution_withGT(data, labels, nb_classes):

    class_data = []
    train_vars = []
    distributions = []
    for c in range(nb_classes):
        train_vars.append(tf.Variable(initial_value=np.random.uniform(low=0.01, high =0.1, size=data.shape[-1])))
        distributions.append(tfd.Bernoulli(probs=train_vars[c]))
        class_mask = (labels == c)
        class_data.append(data[class_mask, :])

    for c_num in range(0,nb_classes):
        optimizer = tf.keras.optimizers.Adam()
        print('\n%-------------------%')
        print('Class ', c_num)
        print('%-------------------%')

        for i in range(0,100):

    dist = tfd.Bernoulli(probs=train_vars)
    dist = tfd.Independent(dist,reinterpreted_batch_ndims=1)

    print(dist)

    return dist

```

```python
# Make the same Naive Bayes classifier we did last tutorial

categories = ['alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space']

(train_data, train_labels), (test_data, test_labels) = get_data(categories)

smoothed_counts = laplace_smoothing(labels=train_labels, binary_data=train_data, n_classes=len(categories))

priors = class_priors(n_classes=len(categories), labels=train_labels)
tf_dist = make_distributions(smoothed_counts)
```

```python
# Now train the distributions with gradient tape

GT_dist = make_distribution_withGT(data=train_data, labels=train_labels, nb_classes=4)
```

```python
# Compare the two results

for dist in [GT_dist,tf_dist]:
    probabilities = []
    for sample, label in zip(test_data, test_labels):
        probabilities.append(predict_sample(dist, sample, priors))

    probabilities = np.asarray(probabilities)
    predicted_classes = np.argmax(probabilities, axis =-1)
    print('f1 ', f1_score(test_labels, predicted_classes, average='macro'))
```


Parametric non Parametric inference
===================

Suppose you have a physical model of an output variable, which takes the form of a parametric model. You now want to model the random effects of the data by a non-parametric (better: infinite parametric) model, such as a Gaussian Process as described in [BayesianLinearRegression](../background/BayesianLinearRegression.ipynb). We can do inference in both worlds, the parameteric and infinite parametric one, by extending the features to a mix between

\begin{align}
p(\mathbf{y}|\boldsymbol{\Phi}, \alpha, \sigma) &= \int p(\mathbf{y}|\boldsymbol{\Phi}, \mathbf{w}, \sigma)p(\mathbf{w}|\alpha) \,\mathrm{d}\mathbf{w}\\
&= \langle\mathcal{N}(\mathbf{y}|\boldsymbol{\Phi}\mathbf{w}, \sigma^2\mathbf{I})\rangle_{\mathcal{N}(\mathbf{0}, \alpha\mathbf{I})}\\
&= \mathcal{N}(\mathbf{y}|\mathbf{0}, \alpha\boldsymbol{\Phi}\boldsymbol{\Phi}^\top + \sigma^2\mathbf{I})
\end{align}

Thus, we can maximize this marginal likelihood w.r.t. the hyperparameters $\alpha, \sigma$ by log transforming and maximizing:

\begin{align}
\hat\alpha, \hat\sigma = \mathop{\arg\max}_{\alpha, \sigma}\log p(\mathbf{y}|\boldsymbol{\Phi}, \alpha, \sigma)
\end{align}

So we will define a mixed inference model mixing parametric and non-parametric models together. One part is described by a paramtric feature space mapping $\boldsymbol{\Phi}\mathbf{w}$ and the other part is a non-parametric function $\mathbf{f}_\text{n}$. For this we define the underlying function $\mathbf{f}$ as

$$
\begin{align}
p(\mathbf{f}) &= p\left(
  \underbrace{
  \begin{bmatrix}
   \delta(t-T)\\
   \boldsymbol{\Phi}
  \end{bmatrix}
  }_{=:\mathbf{A}}
  \left.
  \begin{bmatrix}
   \mathbf{f}_{\text{n}}\\
   \mathbf{w}
  \end{bmatrix}
  \right|
  \mathbf{0},
  \mathbf{A}
  \underbrace{
  \begin{bmatrix}
   \mathbf{K}_{\mathbf{f}} & \\
   & \mathbf{K}_{\mathbf{w}}
  \end{bmatrix}
  }_{=:\boldsymbol{\Sigma}}
  \mathbf{A}^\top
  \right)\enspace,
\end{align}
$$

where $\mathbf{K}_{\mathbf{f}}$ is the covariance describing the non-parametric part $\mathbf{f}_\text{n}\sim\mathcal{N}(\mathbf{0}, \mathbf{K}_\mathbf{f})$ and $\mathbf{K}_{\mathbf{w}}$ is the covariance of the prior over $\mathbf{w}\sim\mathcal{N}(\mathbf{w}|\mathbf{0}, \mathbf{K}_{\mathbf{w}})$.

Thus we can now predict the different parts and even the paramters $\mathbf{w}$ themselves using (Note: If someone is willing to write down the proper path to this, here a welcome and thank you very much. Thanks to Philipp Hennig for his ideas in this.)

$$
\begin{align}
p(\mathbf{f}|\mathbf{y}) &=
 \mathcal{N}(\mathbf{f} |
 \boldsymbol{\Sigma}\mathbf{A}^\top
  \underbrace{
   (\mathbf{A}\boldsymbol{\Sigma}\mathbf{A}^\top + \sigma^2\mathbf{I})^{-1}}_{=:\mathbf{K}^{-1}}\mathbf{y}, \boldsymbol{\Sigma}-\boldsymbol{\Sigma}\mathbf{A}^\top\mathbf{K}^{-1}\mathbf{A}\boldsymbol{\Sigma})
 \\
p(\mathbf{w}|\mathbf{y}) &= \mathcal{N}(\mathbf{w} | \mathbf{K}_\mathbf{w}\boldsymbol{\Phi}^\top\mathbf{K}^{-1}\mathbf{y},
\mathbf{K}_{\mathbf{w}}-\mathbf{K}_{\mathbf{w}}\boldsymbol{\Phi}^\top\mathbf{K}^{-1}\boldsymbol{\Phi}\mathbf{K}_{\mathbf{w}}))
 \\
p(\mathbf{f}_\text{n}|\mathbf{y}) &= \mathcal{N}(\mathbf{f}_\text{n}| \mathbf{K}_\mathbf{f}\mathbf{K}^{-1}\mathbf{y},
\mathbf{K}_{\mathbf{f}}-\mathbf{K}_{\mathbf{f}}\mathbf{K}^{-1}\mathbf{K}_{\mathbf{f}}))
\end{align}
$$

```python
import GPy, numpy as np, pandas as pd
from GPy.kern import LinearSlopeBasisFuncKernel, DomainKernel, ChangePointBasisFuncKernel
%matplotlib inline
from matplotlib import pyplot as plt
```

We will create some data with a non-linear function, strongly driven by piecewise linear trends:

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

```python
x = np.random.uniform(0, 10, 40)[:,None]
x.sort(0)
starts, stops = np.arange(0, 10, 3), np.arange(1, 11, 3)

k_lin = LinearSlopeBasisFuncKernel(1, starts, stops, variance=1., ARD=1)
Phi = k_lin.phi(x)
_ = plt.plot(x, Phi)
```

We will assume the prior over $w_i\sim\mathcal{N}(0, 3)$ and a Matern32 structure in the non-parametric part. Additionally, we add a half parametric part, which is a periodic effect only active between $x\in[3,8]$:

```python
k = GPy.kern.Matern32(1, .3)
Kf = k.K(x)

k_per = GPy.kern.PeriodicMatern32(1, variance=100, period=1)
k_per.period.fix()
k_dom = DomainKernel(1, 1., 5.)
k_perdom = k_per * k_dom
Kpd = k_perdom.K(x)
```

```python
np.random.seed(1234)
alpha = np.random.gamma(3, 1, Phi.shape[1])
w = np.random.normal(0, alpha)[:,None]

f_SE = np.random.multivariate_normal(np.zeros(x.shape[0]), Kf)[:, None]
f_perdom = np.random.multivariate_normal(np.zeros(x.shape[0]), Kpd)[:, None]
f_w = Phi.dot(w)

f = f_SE + f_w + f_perdom

y = f + np.random.normal(0, .1, f.shape)
```

```python
plt.plot(x, f_w)
_ = plt.plot(x, y)
# Make sure the function is driven by the linear trend, as there can be a difficulty in identifiability.
```

With this data, we can fit a model using the basis functions as paramtric part. If you want to implement your own basis function kernel, see GPy.kern._src.basis_funcs.BasisFuncKernel and implement the necessary parts. Usually it is enough to implement the phi(X) method, returning the higher dimensional mapping of inputs X.

```python
k = (GPy.kern.Bias(1)
     + GPy.kern.Matern52(1)
     + LinearSlopeBasisFuncKernel(1, ARD=1, start=starts, stop=stops, variance=.1, name='linear_slopes')
     + k_perdom.copy()
    )

k.randomize()
m = GPy.models.GPRegression(x, y, k)
```

```python
m.checkgrad()
```

    True

```python
m.optimize()
```

    <paramz.optimization.optimization.opt_lbfgsb at 0x7f6fd1b09c10>

```python
m.plot()
```

```python
x_pred = np.linspace(0, 10, 500)[:,None]
pred_SE, var_SE = m._raw_predict(x_pred, kern=m.kern.Mat52)
pred_per, var_per = m._raw_predict(x_pred, kern=m.kern.mul)
pred_bias, var_bias = m._raw_predict(x_pred, kern=m.kern.bias)
pred_lin, var_lin = m._raw_predict(x_pred, kern=m.kern.linear_slopes)
```

```python
m.plot_f(resolution=500, predict_kw=dict(kern=m.kern.Mat52), plot_data=False)
plt.plot(x, f_SE)
```

```python
m.plot_f(resolution=500, predict_kw=dict(kern=m.kern.mul), plot_data=False)
plt.plot(x, f_perdom)
```

```python
m.plot_f(resolution=500, predict_kw=dict(kern=m.kern.linear_slopes), plot_data=False)
plt.plot(x, f_w)
```

```python
w_pred, w_var = m.kern.linear_slopes.posterior_inf()
```

```python
df = pd.DataFrame(w, columns=['truth'], index=np.arange(Phi.shape[1]))
df['mean'] = w_pred
df['std'] = np.sqrt(w_var.diagonal())
```

```python
np.round(df, 2)
```

      truth
      mean
      std

      0
      0.06
      0.00
      0.00

      1
      -2.59
      -2.34
      0.63

      2
      1.90
      1.42
      0.51

      3
      4.36
      4.46
      0.51

```python

```


```python
import numpy as np
import pandas as pd
import mpmath as mp
import sympy as sym
```

```python
from sympy.abc import x
from sympy import Poly, Symbol
from sympy.solvers.inequalities import solve_rational_inequalities
```

```python
a=1
```

```python
solve_rational_inequalities([[((Poly(-x + a), Poly(1, x)), '>='),((Poly(-x + 1), Poly(1, x)), '<=')]])

```

$\displaystyle \left\{1\right\}$

```python
from sympy import *
x, y=symbols('x y')
```

```python
plot_implicit((x**2+y*sqrt(x**(x+y)))<0)
```

```python

```


# Calculating Chern number

```python
import numpy as np
from numpy import linalg as la

O = 0.
M = 1. # g
C = 1. # N/m
w0 = np.sqrt(C / M)
l = 1. # distance between masses
x = np.array([[1.], [0.]]) # x hat
y = np.array([[0.], [1.]]) # y hat
a1 = np.sqrt(3) * l * x
a2 = (np.sqrt(3) * x + 3 * y) * l / 2.
R1 = 1 / 3 * (a1 + a2)
R2 = 1 / 3 * (-2 * a1 + a2)
R3 = 1 / 3 * (a1 - 2 * a2)
R11 = R1 * R1.conj().T
R22 = R2 * R2.conj().T
R33 = R3 * R3.conj().T
```

```python
import numpy as np
precision = 0.1
kxs = np.arange(-np.pi, np.pi, precision)
kys = np.arange(-np.pi, np.pi, precision)

R0_ = lambda k: (1 / 2 * (3 - np.exp(1.j * k.dot(R1)) - np.exp(1.j * k.dot(R2)) - np.exp(1.j * k.dot(R3)))).item()
R1_ = lambda k: (np.sqrt(3) / 4 * (np.exp(1.j * k.dot(R2)) - np.exp(1.j * k.dot(R1)))).item()
R2_ = lambda k: (2 * O / w0**2).item()
R3_ = lambda k: (1 / 4 * (1 - np.exp(1.j * k.dot(R1)) - np.exp(1.j * k.dot(R2)) + np.exp(1.j * k.dot(R3)))).item()
R = lambda k: np.sqrt(R1_(k)**2 + R2_(k)**2 + R3_(k)**2)

ws = np.zeros((len(kys), len(kxs), 2))

for y, ky in enumerate(kys):
    for x, kx in enumerate(kxs):
        k = np.array([kx, ky])
        ws[y, x] = np.sqrt(np.array([R0_(k) - R(k), R0_(k) + R(k)])).real
        # ws[y, x] = np.array([R0_(k) - R(k), R0_(k) + R(k)]).real

# 3D dispersion
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
X, Y = np.meshgrid(kxs, kys)

vmax = np.amax(ws[:, :, 1])
ax.plot_surface(X, Y, ws[:, :, 0], cmap='gist_rainbow_r')
p = ax.plot_surface(X, Y, ws[:, :, 1], cmap='gist_rainbow_r')

# ax.set_xlabel(r"$k_x$ [/m]", fontsize=14)
# ax.set_xlim(-xw, xw)
# ax.set_ylabel(r"$k_y$ [/m]", fontsize=14)
# ax.set_ylim(-yw, yw)
# ax.set_zlabel(r"$\omega$ [/s]", fontsize=14)
# ax.set_zlim(0)

fig.colorbar(p, pad=0.15)
# fig.savefig("2d-graphene-dispersion-3d.png")
plt.show()
```

```python
evals_all = np.zeros((len(kys), len(kxs), 2))
for y, ky in enumerate(kys):
    for x, kx in enumerate(kxs):
        k = np.array([kx, ky])
        H = np.array([
            [R0_(k) + R3_(k), R1_(k) - 1.j * R2_(k)],
            [R1_(k) + 1.j * R2_(k), R0_(k) - R3_(k)],
        ])
        evals, _ = la.eigh(H)
        evals = evals[np.argsort(evals)]
        evals_all[y, x] = evals
evals_all = np.array(evals_all)

# 3D dispersion
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
X, Y = np.meshgrid(kxs, kys)

vmax = np.amax(np.sqrt(evals_all[:, :, 1]))
ax.plot_surface(X, Y, np.sqrt(evals_all[:, :, 0]), vmin=0., vmax=vmax, cmap='gist_rainbow_r')
p = ax.plot_surface(X, Y, np.sqrt(evals_all[:, :, 1]), vmin=0., vmax=vmax, cmap='gist_rainbow_r')

# ax.set_xlabel(r"$k_x$ [/m]", fontsize=14)
# ax.set_xlim(-xw, xw)
# ax.set_ylabel(r"$k_y$ [/m]", fontsize=14)
# ax.set_ylim(-yw, yw)
# ax.set_zlabel(r"$\omega$ [/s]", fontsize=14)
# ax.set_zlim(0)

fig.colorbar(p, pad=0.15)
# fig.savefig("2d-graphene-dispersion-3d.png")
plt.show()

```

# Using library

```python
import z2pack
import numpy as np

O = 13
M = .01 # kg
C = 4. # N/m
w0 = np.sqrt(C / M)
l = 1. # distance between masses
x = np.array([[1.], [0.]]) # x hat
y = np.array([[0.], [1.]]) # y hat
a1 = np.sqrt(3) * l * x
a2 = (np.sqrt(3) * x + 3 * y) * l / 2.
R1 = 1 / 3 * (a1 + a2)
R2 = 1 / 3 * (-2 * a1 + a2)
R3 = 1 / 3 * (a1 - 2 * a2)
R1h = R1 / la.norm(R1)
R2h = R2 / la.norm(R2)
R3h = R3 / la.norm(R3)
R11 = R1h * R1h.conj().T
R22 = R2h * R2h.conj().T
R33 = R3h * R3h.conj().T
Rw = np.array([[0., -1.], [1., 0.]])

R0_ = lambda k: (1 / 2 * (3 - np.exp(1.j * k.dot(R1)) - np.exp(1.j * k.dot(R2)) - np.exp(1.j * k.dot(R3)))).item()
R1_ = lambda k: (np.sqrt(3) / 4 * (np.exp(1.j * k.dot(R2)) - np.exp(1.j * k.dot(R1)))).item()
R2_ = lambda k: (2 * O / w0**2).item()
R3_ = lambda k: (1 / 4 * (1 - np.exp(1.j * k.dot(R1)) - np.exp(1.j * k.dot(R2)) + np.exp(1.j * k.dot(R3)))).item()
R = lambda k: np.sqrt(R1_(k)**2 + R2_(k)**2 + R3_(k)**2)

mu = -5.

R0_ = lambda k: 0
R1_ = lambda k: (-2 * np.sin(k[0])).item()
R2_ = lambda k: (-2 * np.sin(k[1])).item()
R3_ = lambda k: (2 * (np.cos(k[0]) + np.cos(k[1])) + mu).item()

Os = np.arange(0, 100, 0.1)

def hamiltonian(k):
    kx, ky, kz = k
    k_ = np.array([kx, ky])
    p = np.exp(1.j * k_.dot(a1))
    s = np.exp(1.j * k_.dot(a2))
    # p = k_.dot(a1)
    # s = k_.dot(a2)

    H = np.array([
        [R0_(k_) + R3_(k_), R1_(k_) - 1.j * R2_(k_)],
        [R1_(k_) + 1.j * R2_(k_), R0_(k_) - R3_(k_)],
    ])

    # H = w0**2 * np.vstack([
    #     np.hstack([R11 + R22 + R33 - 2.j * O * Rw / w0**2, -(R11 + R22 * p.conj() + R33 * s.conj())]),
    #     np.hstack([-(R11 + R22 * p + R33 * s) ,R11 + R22 + R33 - 2.j * O * Rw / w0**2]),
    # ])

    return H
system = z2pack.hm.System(hamiltonian)

result = z2pack.surface.run(
    system=system,
    surface=lambda t1, t2: [t1, t2, 0]
)

print(z2pack.invariant.chern(result))   # Prints the Chern number
# print(z2pack.invariant.z2(result))   # Prints the Chern numbe
```

    +----------------------------------------------------------------------+
    | ===================                                                  |
    | SURFACE CALCULATION                                                  |
    | ===================                                                  |
    | starting at 2022-01-26 08:47:02,460                                  |
    | running Z2Pack version 2.2.0                                         |
    |                                                                      |
    | gap_tol:            0.3                                              |
    | init_result:        None                                             |
    | iterator:           range(8, 27, 2)                                  |
    | load:               False                                            |
    | load_quiet:         True                                             |
    | min_neighbour_dist: 0.01                                             |
    | move_tol:           0.3                                              |
    | num_lines:          11                                               |
    | pos_tol:            0.01                                             |
    | save_file:          None                                             |
    | serializer:         auto                                             |
    | surface:            <function <lambda> at 0x000001F41A408310>        |
    | system:             <z2pack.hm.System object at 0x000001F4167FCA00>  |
    +----------------------------------------------------------------------+

    INFO: Adding lines required by 'num_lines'.
    INFO: Adding line at t = 0.0
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.1
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.2
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.30000000000000004
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.4
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.5
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.6000000000000001
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.7000000000000001
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.8
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 0.9
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Adding line at t = 1.0
    INFO:       Calculating line for N = 8
    INFO:       Calculating line for N = 10
    INFO: Convergence criteria fulfilled for 10 of 10 neighbouring lines.

    +----------------------------------------------------------------------+
    |                   Calculation finished in 0h 0m 0s                   |
    +----------------------------------------------------------------------+
    +----------------------------------------------------------------------+
    |                         ==================                           |
    |                         CONVERGENCE REPORT                           |
    |                         ==================                           |
    |                                                                      |
    |                         Line Convergence                             |
    |                         ================                             |
    |                                                                      |
    |                             PosCheck                                 |
    |                             --------                                 |
    |                             PASSED: 11 of 11                         |
    |                                                                      |
    |                         Surface Convergence                          |
    |                         ===================                          |
    |                                                                      |
    |                             GapCheck                                 |
    |                             --------                                 |
    |                             PASSED: 10 of 10                         |
    |                                                                      |
    |                             MoveCheck                                |
    |                             ---------                                |
    |                             PASSED: 10 of 10                         |
    +----------------------------------------------------------------------+
    0.001234504501752204

```python
import numpy as np
import numpy.linalg as la
from sympy import symbols, Matrix, BlockMatrix, conjugate
from sympy.functions import exp

kx, ky = symbols('kx ky')

O = 0.
M = .01 # kg
C = 4. # N/m
w0 = np.sqrt(C / M)
l = 1. # distance between masses
x = np.array([[1.], [0.]]) # x hat
y = np.array([[0.], [1.]]) # y hat
a1 = np.sqrt(3) * l * x
a2 = (np.sqrt(3) * x + 3 * y) * l / 2.
R1 = 1 / 3 * (a1 + a2)
R2 = 1 / 3 * (-2 * a1 + a2)
R3 = 1 / 3 * (a1 - 2 * a2)
R1h = R1 / la.norm(R1)
R2h = R2 / la.norm(R2)
R3h = R3 / la.norm(R3)
R11 = R1h * R1h.conj().T
R22 = R2h * R2h.conj().T
R33 = R3h * R3h.conj().T
Rw = np.array([[0., -1.], [1., 0.]])

p = exp(1.j * np.sqrt(3) * kx)
s = exp(1.j * (np.sqrt(3) * kx + 3 * ky) / 2)

H = BlockMatrix([
    [Matrix(R11 + R22 + R33 - 2.j * O * Rw / w0**2), Matrix(-(R11 + R22 * conjugate(p) + R33 * conjugate(s)))],
    [Matrix(-(R11 + R22 * p + R33 * s)) ,Matrix(R11 + R22 + R33 - 2.j * O * Rw / w0**2)]
])
H = w0 ** 2 * Matrix(H)
# H.eigenvects()
print(R11, R22, R33)

```

    [[0.75      0.4330127]
     [0.4330127 0.25     ]] [[ 0.75      -0.4330127]
     [-0.4330127  0.25     ]] [[ 0. -0.]
     [-0.  1.]]

# Chern with extended eigen vecs

```python
import numpy as np
import numpy.linalg as la

O = 0.
C = 1.
M = 1.
w0 = np.sqrt(C / M)
a = 1. # distance between masses
x = np.array([[1.], [0.]]) # x hat
y = np.array([[0.], [1.]]) # y hat
a1 = np.sqrt(3) * a * x
a2 = (np.sqrt(3) * x + 3 * y) * a / 2.
R1 = 1 / 3 * (a1 + a2)
R2 = 1 / 3 * (-2 * a1 + a2)
R3 = 1 / 3 * (a1 - 2 * a2)
R1h = R1 / la.norm(R1)
R2h = R2 / la.norm(R2)
R3h = R3 / la.norm(R3)
R11 = R1h * R1h.conj().T
R22 = R2h * R2h.conj().T
R33 = R3h * R3h.conj().T
Rw = np.array([[0., -1.], [1., 0.]])
precision = 0.01
xw = np.pi / np.sqrt(3) * 2 # width of x
yw = np.pi / 3 * 4 # width of y
kxs = np.arange(-xw, xw, precision)
kys = np.arange(-yw, yw, precision)

evals_all = np.zeros((len(kys), len(kxs), 8), dtype=np.complex128)
evecs_all = np.zeros((len(kys), len(kxs), 8, 8), dtype=np.complex128)
for y, ky in enumerate(kys):
    for x, kx in enumerate(kxs):
        k = np.array([kx, ky])
        p = np.exp(1.j * k.dot(a1))
        s = np.exp(1.j * k.dot(a2))
        L11 = np.vstack([
            np.hstack([R11 + R22 + R33, -(R11 + p.conj() * R22 + s.conj() * R33)]),
            np.hstack([-(R11 + p * R22 + s * R33), R11 + R22 + R33])
        ])
        L12 = np.vstack([
            np.hstack([-Rw * 2.j * O, np.zeros((2, 2))]),
            np.hstack([np.zeros((2, 2)), -Rw * 2.j * O])
        ])
        L = w0 ** 2 * np.vstack([
            np.hstack([L11, L12]),
            np.hstack([np.zeros((4, 4)), np.eye(4)])
        ])
        M = np.vstack([
            np.hstack([np.zeros((4, 4)), np.eye(4)]),
            np.hstack([np.eye(4), np.zeros((4, 4))])
        ])

        H = np.linalg.inv(M).dot(L)
        evals, evecs = la.eig(H)
        idcs = np.argsort(evals)
        evals, evecs = evals[idcs], evecs[idcs]
        evals_all[y, x] = evals
        evecs_all[y, x] = evecs
evals_all = np.array(evals_all)
evecs_all = np.array(evecs_all)
print(evals_all.shape, evecs_all.shape)

```

    (838, 726, 8) (838, 726, 8, 8)

```python
from shapely.geometry import Point
from shapely.geometry.polygon import Polygon

x = np.array([[1.], [0.]]) # x hat
y = np.array([[0.], [1.]]) # y hat
a1_ = 2 * np.pi / np.sqrt(3) / a * (x - y / np.sqrt(3))
a2_ = 4 * np.pi / 3 / a * y
K = (a1_ / 2 + a2_ / 4) / np.cos(np.pi / 6)**2
b1 = 2 * np.pi / np.sqrt(3) / a * (x - y / np.sqrt(3))
b2 = 4 * np.pi / 3 / a * y

h = precision
bz= Polygon([K, -K + b1 + b2, K - b1, -K, K - b1 - b2, -K + b1])

print(h)
print(bz.contains(Point(-4,0)))

C = 0
band_number = 6

# xi = evecs_all[:, :, band_number, :2]
# eta = evecs_all[:, :, band_number, 2:4]
xi = evecs_all[:, :, :2, band_number]
eta = evecs_all[:, :, 2:4, band_number]
for y, ky in enumerate(kys):
    for x, kx in enumerate(kxs):
        p = Point(kx, ky)
        if not bz.contains(p):
            continue
        dxi_dkx = (xi[y, x+1] - xi[y, x-1]) / (2 * h)
        dxi_dky = (xi[y+1, x] - xi[y-1, x]) / (2 * h)
        C += 1.j * dxi_dkx.conj().T.dot(dxi_dky) - dxi_dky.conj().T.dot(dxi_dkx)

        deta_dkx = (eta[y, x+1] - eta[y, x-1]) / (2 * h)
        deta_dky = (eta[y+1, x] - eta[y-1, x]) / (2 * h)
        C += 1.j * deta_dkx.conj().T.dot(deta_dky) - deta_dky.conj().T.dot(deta_dkx)

C /= 2 * np.pi
# print(evecs_all[0, 0, 0, :4])
# print(evecs_all[0, 0, 0, 4:])

print(C)

```

    0.01
    False
    (101933.18741767509-101933.18741767509j)

```python

def calc_chern(hVec):
    """Calculate 1st Chern number given eigen-vectors. Using Japanese algorithm
    (2005).
    :param hVec: eigen-vectors of all band (assuming gapless).
    :return: list of Chern number of all bands. Should summed to 0.
    """
    hVec = np.array(hVec)
    dimy, dimx, nlevels, _ = hVec.shape
    cnlist = np.zeros(nlevels)

    for iy in range(dimy-1):
        for ix in range(dimx-1):
            u12 = hVec[iy, ix + 1].conjugate().T @ hVec[iy, ix]
            u23 = hVec[iy + 1, ix + 1].conjugate().T @ hVec[iy, ix + 1]
            u34 = hVec[iy + 1, ix].conjugate().T @ hVec[iy + 1, ix + 1]
            u41 = hVec[iy, ix].conjugate().T @ hVec[iy + 1, ix]
            t12 = np.diag(u12.diagonal())
            t23 = np.diag(u23.diagonal())
            t34 = np.diag(u34.diagonal())
            t41 = np.diag(u41.diagonal())
            tplaquet = t41 @ t34 @ t23 @ t12
            cnlist += np.angle(tplaquet.diagonal())

    cnlist /= 2 * np.pi
    cnlist = chop(cnlist)
    return cnlist

def chop(array, tol=1e-7):
    """Realize Mathematica Chop[].
    :param array: 1D array.
    :param tol: tolerance to be chopped. default to 1e-7
    :return: chopped array. (original array alse modified.)
    """
    for i in range(len(array)):
        a = array[i]
        if np.abs(a-round(a)) < tol:
            array[i] = round(a)
    return array

calc_chern(evecs_all[:, :, :4, 4:])
```

    array([-37.36966799,   1.81470808,  -6.64135569,  -1.35299878])

# Chern with perturbation

```python
import numpy as np
import numpy.linalg as la

O = 4
C = 4
M = 0.01
w0 = np.sqrt(C / M)
a = 1. # distance between masses
x = np.array([[1.], [0.]]) # x hat
y = np.array([[0.], [1.]]) # y hat
a1 = np.sqrt(3) * a * x
a2 = (np.sqrt(3) * x + 3 * y) * a / 2.
R1 = 1 / 3 * (a1 + a2)
R2 = 1 / 3 * (-2 * a1 + a2)
R3 = 1 / 3 * (a1 - 2 * a2)
R1h = R1 / la.norm(R1)
R2h = R2 / la.norm(R2)
R3h = R3 / la.norm(R3)
R11 = R1h * R1h.conj().T
R22 = R2h * R2h.conj().T
R33 = R3h * R3h.conj().T
Rw = np.array([[0., -1.], [1., 0.]])
precision = 1e-2
xw = np.pi / np.sqrt(3) * 2 # width of x
yw = np.pi / 3 * 4 # width of y
kxs = np.arange(-xw, xw, precision)
kys = np.arange(-yw, yw, precision)

evals_all = np.zeros((len(kys), len(kxs), 4), dtype=np.complex128)
evecs_all = np.zeros((len(kys), len(kxs), 4, 4), dtype=np.complex128)
for y, ky in enumerate(kys):
    for x, kx in enumerate(kxs):
        k = np.array([kx, ky])
        p = np.exp(1.j * k.dot(a1))
        s = np.exp(1.j * k.dot(a2))
        H = w0**2 * np.vstack([
            np.hstack([R11 + R22 + R33 - 2.j * O * Rw / w0**2, -(R11 + R22 * p.conj() + R33 * s.conj())]),
            np.hstack([-(R11 + R22 * p + R33 * s) ,R11 + R22 + R33 - 2.j * O * Rw / w0**2]),
        ])
        evals, evecs = la.eigh(H)
        idcs = np.argsort(evals)
        evals, evecs = evals[idcs], evecs[idcs]
        evals_all[y, x] = evals
        evecs_all[y, x] = evecs
evals_all = np.array(evals_all)
evecs_all = np.array(evecs_all)
print(evals_all.shape, evecs_all.shape)
```

    (838, 726, 4) (838, 726, 4, 4)

```python
from conut import MechanicalGraphene, HamiltonianType, MechanicalGrapheneLattice
from conut.data import WaveNumber, WaveNumberType
from torch.utils.data import DataLoader
import torch
import numpy as np
from shapely.geometry import Polygon, Point

dev = 'cuda' if torch.cuda.is_available() else 'cpu'
dev = 'cpu'
κ = 1.
α = 1.
m = 1.
l = 1.
params = {'batch_size': 512,
          'shuffle': False,
          'num_workers': 4}

for Ω in [0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6]:

    # Setup Lattice
    lattice = MechanicalGrapheneLattice(l, α, device=dev)

    # Construct wavenumber dataset
    wn = WaveNumber(WaveNumberType.XY, lattice, precision=3e-3)
    wn_gkm = WaveNumber(WaveNumberType.GKM, lattice, precision=1e-1)
    wn_generator = DataLoader(wn, **params)
    wn_gkm_generator = DataLoader(wn_gkm, **params)

    # Build mechanical graphene model
    mg = MechanicalGraphene(κ=κ, m=m, α=α, lattice=lattice, h_type=HamiltonianType.Bulk, shape=wn.shape, Ω=Ω, perturbation=True).to(dev)
    # mg_gkm = MechanicalGraphene(κ=κ, m=m, α=α, lattice=lattice, h_type=HamiltonianType.Bulk, shape=wn_gkm.shape, Ω=Ω, GKM=True, perturbation=False).to(dev)

    for idcs_batch, ks_batch in wn_generator:
        idcs_batch, ks_batch = idcs_batch.to(dev), ks_batch.to(dev)
        mg(idcs_batch, ks_batch)
    evecs = mg.evecs.detach().numpy()

    def calc_chern(hVec):
        """Calculate 1st Chern number given eigen-vectors. Using Japanese algorithm
        (2005).
        :param hVec: eigen-vectors of all band (assuming gapless).
        :return: list of Chern number of all bands. Should summed to 0.
        """
        hVec = np.array(hVec)
        dimy, dimx, nlevels, _ = hVec.shape
        cnlist = np.zeros(nlevels)

        for iy in range(dimy-1):
            for ix in range(dimx-1):
                u12 = hVec[iy, ix + 1].conjugate().T @ hVec[iy, ix]
                u23 = hVec[iy + 1, ix + 1].conjugate().T @ hVec[iy, ix + 1]
                u34 = hVec[iy + 1, ix].conjugate().T @ hVec[iy + 1, ix + 1]
                u41 = hVec[iy, ix].conjugate().T @ hVec[iy + 1, ix]
                t12 = np.diag(u12.diagonal())
                t23 = np.diag(u23.diagonal())
                t34 = np.diag(u34.diagonal())
                t41 = np.diag(u41.diagonal())
                tplaquet = t41 @ t34 @ t23 @ t12
                cnlist += np.angle(tplaquet.diagonal())

        cnlist /= 2 * np.pi
        cnlist = chop(cnlist)
        return cnlist

    def chop(array, tol=1e-7):
        """Realize Mathematica Chop[].
        :param array: 1D array.
        :param tol: tolerance to be chopped. default to 1e-7
        :return: chopped array. (original array alse modified.)
        """
        for i in range(len(array)):
            a = array[i]
            if np.abs(a-round(a)) < tol:
                array[i] = round(a)
        return array

    x = np.array([[1.], [0.]]) # x hat
    y = np.array([[0.], [1.]]) # y hat
    a1_ = 2 * np.pi / np.sqrt(3) / l * (x - y / np.sqrt(3))
    a2_ = 4 * np.pi / 3 / l * y
    K = (a1_ / 2 + a2_ / 4) / np.cos(np.pi / 6)**2
    b1 = 2 * np.pi / np.sqrt(3) / l * (x - y / np.sqrt(3))
    b2 = 4 * np.pi / 3 / l * y

    bz= Polygon([K, -K + b1 + b2, K - b1, -K, K - b1 - b2, -K + b1])

    evecs_all_bz = np.zeros((len(wn.kys), len(wn.kxs), 4, 4), dtype=np.complex128)

    for y, ky in enumerate(wn.kys):
        for x, kx in enumerate(wn.kxs):
            p = Point(kx, ky)
            if not bz.contains(p):
                continue
            evecs_all_bz[y, x] = evecs[y, x]

    print(Ω, calc_chern(evecs_all_bz))
```


# Semi-Monocoque Theory

```python
from pint import UnitRegistry
import sympy
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import sys
%matplotlib inline
from IPython.display import display
```

Import **Section** class, which contains all calculations

```python
from Section import Section
```

Initialization of **sympy** symbolic tool and **pint** for dimension analysis (not really implemented rn as not directly compatible with sympy)

```python
ureg = UnitRegistry()
sympy.init_printing()
```

Define **sympy** parameters used for geometric description of sections

```python
A, A0, t, t0, a, b, h, L = sympy.symbols('A A_0 t t_0 a b h L', positive=True)
```

We also define numerical values for each **symbol** in order to plot scaled section and perform calculations

```python
values = [(A, 450 * ureg.millimeter**2),(A0, 250  * ureg.millimeter**2),(a, 130 * ureg.millimeter), \
          (b, 300 * ureg.millimeter),(h, 150 * ureg.millimeter),(L, 650 * ureg.millimeter)]
datav = [(v[0],v[1].magnitude) for v in values]
```

# Multiconnected Section

Define graph describing the section:

1) **stringers** are **nodes** with parameters:
- **x** coordinate
- **y** coordinate
- **Area**

2) **panels** are **oriented edges** with parameters:
- **thickness**
- **lenght** which is automatically calculated

```python
stringers = {1:[(sympy.Integer(0),h),A],
             2:[(sympy.Integer(0),sympy.Integer(0)),A],
             3:[(a,sympy.Integer(0)),A],
             4:[(a+b,sympy.Integer(0)),A],
             5:[(a+b,h),A],
             6:[(a,h),A]}

panels = {(1,2):t,
          (2,3):t,
          (3,4):t,
          (4,5):t,
          (5,6):t,
          (6,1):t,
          (3,6):t}
```

Define section and perform first calculations

```python
S3 = Section(stringers, panels)
```

As we need to compute $x_{sc}$, we have to perform

$$A \cdot q_{ext} = T$$

where:
- **A** is a matrix with **number of nodes + number of loops** rows and **number of edges +1** columns (it is square)
- **q** is a column vector of unknowns: **#edges fluxes** and **shear center** coordinate
- **T** is the vector of known terms: $-\frac{T_y}{J_x} \cdot S_{x_i}$ or $-\frac{T_x}{J_y} \cdot S_{y_i}$ for *n-1* nodes and the rest are 0

**Expression of A**

```python
sympy.simplify(S3.A)
```

**Expression of T**

```python
sympy.simplify(S3.T)
```

**Resulting fluxes and coordinate**

```python
sympy.simplify(S3.tempq)
```

## Plot of **S3** section in original reference frame

```python
start_pos={ii: [float(S3.g.node[ii]['ip'][i].subs(datav)) for i in range(2)] for ii in S3.g.nodes() }
```

```python
plt.figure(figsize=(12,8),dpi=300)
nx.draw(S3.g,with_labels=True, arrows= True, pos=start_pos)
plt.arrow(0,0,20,0)
plt.arrow(0,0,0,20)
#plt.text(0,0, 'CG', fontsize=24)
plt.axis('equal')
plt.title("Section in starting reference Frame",fontsize=16);
```

Expression of **Inertial properties** wrt Center of Gravity in with original rotation

```python
S3.Ixx0, S3.Iyy0, S3.Ixy0, S3.α0
```

## Plot of **S3** section in inertial reference Frame

Section is plotted wrt **center of gravity** and rotated (if necessary) so that *x* and *y* are principal axes.
**Center of Gravity** and **Shear Center** are drawn

```python
positions={ii: [float(S3.g.node[ii]['pos'][i].subs(datav)) for i in range(2)] for ii in S3.g.nodes() }
```

```python
x_ct, y_ct = S3.ct.subs(datav)

plt.figure(figsize=(12,8),dpi=300)
nx.draw(S3.g,with_labels=True, pos=positions)
plt.plot([0],[0],'o',ms=12,label='CG')
plt.plot([x_ct],[y_ct],'^',ms=12, label='SC')
#plt.text(0,0, 'CG', fontsize=24)
#plt.text(x_ct,y_ct, 'SC', fontsize=24)
plt.legend(loc='lower right', shadow=True)
plt.axis('equal')
plt.title("Section in pricipal reference Frame",fontsize=16);
```

Expression of **inertial properties** in *principal reference frame*

```python
S3.Ixx, S3.Iyy, S3.Ixy, S3.θ
```

## **Shear center** expression

```python
S3.ct
```

## Loops detection

```python
S3.cycles
```

## Compute axial loads in Stringers in **S3**

Set **loads** on the section:

**Example 1**: _shear_ in **y** direction and _bending moment_ in **x** direction

```python
Tx, Ty, Nz, Mx, My, Mz, F, ry, ry, mz = sympy.symbols('T_x T_y N_z M_x M_y M_z F r_y r_x m_z')
S3.set_loads(_Tx=0, _Ty=Ty, _Nz=0, _Mx=Mx, _My=0, _Mz=0)
```

Compute _axial loads_ in **stringers** and _shear flows_ in **panels**

```python
S3.compute_stringer_actions()
S3.compute_panel_fluxes();
```

Expression of matrix **A**:

- second to last row: Mz equilibrium
- last row: equivalence of rotations in the form: $\dot{\theta_1} - \dot{\theta_2} = 0$

```python
sympy.simplify(S3.A)
```

Expression of **T**

```python
sympy.simplify(S3.T)
```

## Axial loads

```python
S3.N
```

## Shear flows

```python
sympy.simplify(S3.q)
```

**Example 2**: _twisting moment_ in **z** direction

```python
S3.set_loads(_Tx=0, _Ty=0, _Nz=0, _Mx=0, _My=0, _Mz=Mz)
S3.compute_stringer_actions()
S3.compute_panel_fluxes();
```

```python
S3.N
```

```python
S3.q
```

Set **loads** on the section:

**Example 3**: _shear_ in **x** direction and _bending moment_ in **y** direction

```python
S3.set_loads(_Tx=Tx, _Ty=0, _Nz=0, _Mx=0, _My=My, _Mz=0)
S3.compute_stringer_actions()
S3.compute_panel_fluxes();
```

```python
S3.N
```

```python
S3.q
```

Verify that $$q_i \cdot l_i = T_x$$

```python
sympy.simplify(S3.q[(2,3)]*a+S3.q[(3,4)]*b-S3.q[(5,6)]*b-S3.q[(6,1)]*a)
```

## Torsional moment of inertia

```python
S3.compute_Jt()
S3.Jt
```

```python

```