#  Diagrammatic Differentiation in Practice

Slides from the Oxford quantum group lunch talk on February 18th 2021.

## Implementing automatic differentiation in discopy

```python
from discopy import *
from discopy.quantum import *
from discopy.quantum.zx import Functor, Diagram

from sympy.abc import theta, phi, symbols
from matplotlib import pyplot as plt
```

**Derivatives are compositional**: if you have the derivative of each box, then you have the derivative of the diagrams made from those boxes.

```python
x = symbols('x')

f_array = [[1, 0], [0, x]]
g_array = [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,x*x]]
h_array = [[1, 1], [1,-1]]

f = QuantumGate('f(x)', n_qubits=1, data=x, array=f_array)
g = QuantumGate('g(x)', n_qubits=2, data=x, array=g_array)
h = QuantumGate('h', n_qubits=1, data=None, array=h_array)

d = {f: [[0, 0], [0, 1]], g: [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,2*x]]}

circuit = (f @ h >> g)
circuit.draw(figsize=(3,3))
```

Given a commutative rig $\mathbb{S}$, a derivation $\partial: \mathbb{S} \rightarrow \mathbb{S}$ is defined as any operation that satisfies the linearity and product rules:

$$ \partial(f + g) = \partial f + \partial g \quad \text{and} \quad \partial(f \times g) = \partial f \times g + f \times \partial g $$

A related notation is dual numbers $D[\mathbb{S}]$, numbers of the form $a + b \epsilon$ for $a, b \in \mathbb{S}$ such that $\epsilon^2 = 0$. Sum and product are given by:

\begin{aligned}
\left(a+a^{\prime} \epsilon\right)+\left(b+b^{\prime} \epsilon\right) &=(a+b)+\left(a+b^{\prime}\right) \epsilon \\
\left(a+a^{\prime} \epsilon\right) \times\left(b+b^{\prime} \epsilon\right) &=(a \times b)+\left(a \times b^{\prime}+a^{\prime} \times b\right) \epsilon
\end{aligned}

Write $\pi_0, \pi_1 : D[\mathbb{S}] \to \mathbb{S}$ for the projections along the real and epsilon component resp.

**Lemma:** Every derivation defines a rig homomorphism $\mathbb{S} \to D[\mathbb{S}]$ with $f \mapsto f + \partial f$. The other way around, every a rig homomorphism $\partial : \mathbb{S} \to D[\mathbb{S}]$ with $\pi_0 \circ \partial = \text{id}_\mathbb{S}$ defines a derivation $\pi_1 \circ \partial : \mathbb{S} \to \mathbb{S}$.

For example, in the rig of smooth functions we can lift any
smooth function $f : \mathbb{R} \rightarrow \mathbb{R}$ to a function $f: D[\mathbb{R}] \rightarrow D[\mathbb{R}]$ over the dual numbers defined by:

$$f\left(a+a^{\prime} \epsilon\right)=f(a)+a^{\prime} \times(\partial f)(a) \epsilon$$

Then we can derive the following linearity, product and chain rules:

\begin{aligned}
(f+g)\left(a+a^{\prime} \epsilon\right) &=(f+g)(a)+a^{\prime} \times(\partial f+\partial g)(a) \epsilon \\
(f \times g)\left(a+a^{\prime} \epsilon\right) &=(f \times g)(a)+a^{\prime} \times(f \times \partial g+\partial f \times g)(a) \epsilon \\
(f \circ g)\left(a+a^{\prime} \epsilon\right) &=(f \circ g)(a)+a^{\prime} \times(\partial g \times \partial f \circ g)(a) \epsilon
\end{aligned}

```python
eps = QuantumGate('eps', n_qubits=0, array=[1e-10])

def DualFunctorAr(box):
    if x in box.free_symbols:
        d_box = QuantumGate(f'd{box.name}', n_qubits=len(box.cod), data=x, array=d[box])
        return box + d_box @ eps
    else:
        return box

dual_functor = CircuitFunctor(ob=lambda x: x,  ar=DualFunctorAr)
test = dual_functor(circuit)
test.draw(figsize=(15, 5))
```

```python
def project_in_eps(diagram):
    eps_terms = []
    for term in diagram.terms:
        if [box.name for box in term.boxes].count('eps') == 1:
            # remove epsilon
            remove_eps_functor = CircuitFunctor(ob=lambda x: x,
                                                ar=lambda x: Id(0) if x.name == 'eps' else x)
            eps_term = remove_eps_functor(term)
            eps_terms.append(eps_term)
    return Sum(eps_terms, cod=diagram.cod, dom=diagram.dom)

drawing.equation(circuit,
                  project_in_eps(test), figsize=(18, 3), symbol="--dual--> --project-->")
```

$$\tiny
(f(x) \otimes h) \circ g =
\left(\begin{pmatrix}1 & 0 \\ 0 & x\end{pmatrix} \otimes
\begin{pmatrix}1 & 1 \\ 1 & -1\end{pmatrix}\right) \circ
\begin{pmatrix}1&&&\\&1&&\\&&1&\\&&&x^2\end{pmatrix} =
\begin{pmatrix}1&1&&\\1&-1&&\\&&x&x^3\\&&x&-x^3\end{pmatrix}
$$

```python
project_in_eps(test).eval().array.reshape(4, 4)
```

    array([[0, 0, 0, 0],
           [0, 0, 0, 0],
           [0, 0, 1.00000000000000, 3.0*x**2],
           [0, 0, 1.00000000000000, -3.0*x**2]], dtype=object)

## Rules for diffentiating diagrams

```python
circuit = (Rx(x) @ Id(1)) >> CX >> (Rx(2*x) @ Id(1))
drawing.equation(circuit, circuit.grad(x, mixed=False), symbol='|--->', figsize=(15, 4))
```

## Rules for differentiating ZX diagrams

```python
XC = QuantumGate('XC', n_qubits=2, array=[[0,1,0,0], [1,0,0,0],[0,0,1,0], [0,0,0,1]])

def gate2zx_new(box):
    from discopy.quantum.zx import gate2zx, PRO, Z, X, Id
    if box == XC:
        return Id(1) @ Z(1, 2) >> X(2, 1) @ Id(1)
    else:
        return gate2zx(box)
circuit2zx = Functor(ob={qubit: PRO(1)}, ar=gate2zx_new, ob_factory=PRO, ar_factory=Diagram)
```

```python
circuit = (Rx(x) @ Id(1)) >> CX
drawing.equation(circuit, circuit2zx(circuit), circuit2zx(circuit).grad(x), figsize=(9, 2), symbol="---->")
```

## Doubling via the CPM construction

```python
swaps = Id(2) @ SWAP >> Id(1) @ SWAP @ Id(1)
doubled_circuit = swaps[::-1] >> Id(1) @ Rx(-x) @ Rx(x) @ Id(1) >> XC @ CX >> swaps
drawing.equation(doubled_circuit, circuit2zx(doubled_circuit), symbol="--- ZX -->", figsize=(15, 4))
```

Both `Circuit`s and `zx.Diagram`s can be differentiated.

```python
doubled_circuit.grad(x, mixed=False).draw(figsize=(12, 4))
```

```python
circuit2zx(doubled_circuit).grad(x).draw(figsize=(12, 3))
```

Differentiating a circuit as doubled diagram can give you an asymmetric, undoubled diagram due to the product rule.
These diagrams cannot be executed on quantum hardware.

## Differntiating Circuits
Similar to how we defined the derivatives of the ZX `Spider`s in terms of `Spiders`, the generators of ZX, we need to define the derivatives of the parameterised `QuantumGate`s in terms of `QuantumGate`s

From Schuld et al. the parameter-shift rule for `Rz` is given by
$\partial R_z(\theta) = \frac{1}{2} [R_z(\theta + \frac{\pi}{2}) - R_z(\theta - \frac{\pi}{2})]$

```python
drawing.equation(Rz(x).bubble(drawing_name="circ ∂"),
                 Rz(x).grad(x), figsize=(12, 4))
```

```python
drawing.equation(Rz(x).bubble(drawing_name="double").bubble(drawing_name="diag ∂"),
                 (Rz(-x) @ Rz(x)).grad(x, mixed=False), figsize=(12, 4))
```

Bear in mind that the previous equation is an equation on circuits, and the this one is an equation on linear maps.

## Checks for Diagrams
we checked that the diagrammatic derivatives equals the derivatives of sympy.

```python
import numpy as np
def _to_square_mat(m):
    m = np.asarray(m).flatten()
    return m.reshape(2 * (int(np.sqrt(len(m))), ))

def test_rot_grad():
    from sympy.abc import phi
    import sympy as sy
    for gate in (Rx, Ry, Rz, CU1, CRx, CRz):
        # Compare the grad discopy vs sympy
        op = gate(phi)
        d_op_sym = sy.Matrix(_to_square_mat(op.eval().array)).diff(phi)
        d_op_disco = sy.Matrix(
            _to_square_mat(op.grad(phi, mixed=False).eval().array))
        diff = sy.simplify(d_op_disco - d_op_sym).evalf()
        assert np.isclose(float(diff.norm()), 0.)

test_rot_grad()
```

## Checks for Circuits
we checked that the circuit derivative on the circuit is equal to the diagrammatic derivative of the doubled diagram.

```python
def test_rot_grad_mixed():
    from sympy.abc import symbols
    from sympy import Matrix

    z = symbols('z', real=True)
    random_values = [0., 1., 0.123, 0.321, 1.234]

    for gate in (Rx, Ry, Rz):
        cq_shape = (4, 4)
        v1 = Matrix((gate(z).eval().conjugate() @ gate(z).eval())
                    .array.reshape(*cq_shape)).diff(z)
        v2 = Matrix(gate(z).grad(z).eval(mixed=True).array.reshape(*cq_shape))

        for random_value in random_values:
            v1_sub = v1.subs(z, random_value).evalf()
            v2_sub = v2.subs(z, random_value).evalf()

            difference = (v1_sub - v2_sub).norm()
            assert np.isclose(float(difference), 0.)

test_rot_grad_mixed()
```

```python
circuit = Ket(0, 0) >> H @ Rx(phi) >> CX >> Bra(0, 1)
gradient = (circuit >> circuit[::-1]).grad(phi, mixed=False)
drawing.equation(circuit, gradient, symbol="|-->", figsize=(15, 4))
```

```python
x = np.arange(0, 1, 0.05)
y = np.array([circuit.lambdify(phi)(i).eval(mixed=True).array.imag for i in x])
dy = np.array([gradient.lambdify(phi)(i).eval(mixed=False).array.real for i in x])
plt.subplot(2, 1, 1)
plt.plot(x, y)
plt.ylabel("Amplitude")
plt.subplot(2, 1, 2)
plt.plot(x, dy)
plt.ylabel("Gradient")
```

## Bonus: Finding the exponent of a gate using Stone's theorem

A one-parameter unitary group is a unitary matrix $U: n \rightarrow n$ in $\operatorname{Mat}_{\mathrm{R} \rightarrow \mathrm{C}}$ with $U(0)=\mathrm{id}_{n}$ and $U(t) U(s)=U(s+t)$ for all $s, t \in \mathbb{R}$. It is strongly continuous when $\lim _{t \rightarrow t_{0}} U(t)=U\left(t_{0}\right)$ for all $t_{0} \in \mathbb{R}$ A one-parameter diagram $d: x^{\otimes n} \rightarrow x^{\otimes n}$ is said to be a unitary group when its interpretation $[[d]]$ is.

**Stone's Theorem**: There is a one-to-one correspondance between strongly continuous one-parameter unitary groups and self-adjoint matrices. The bijection is given explicitly by
$$H \mapsto \exp (i t H)\quad \text{ and } \quad \mapsto-i(\partial U)(0)$$

## Future Work

* completing discopy codebase for QML
* solving differential equations
* Keeping derivatives of ZX in ZX, rather than sum of ZX
* Formulate diag diff for Boolean circuits


```python
# This cell is for the Google Colaboratory
# https://stackoverflow.com/a/63519730
if 'google.colab' in str(get_ipython()):
  # https://colab.research.google.com/notebooks/io.ipynb
  import google.colab.drive as gcdrive
  # may need to visit a link for the Google Colab authorization code
  gcdrive.mount("/content/drive/")
  import sys
  sys.path.insert(0,"/content/drive/My Drive/Colab Notebooks/nmisp/45_sympy")

```

# `sympy`

[`sympy`](https://www.sympy.org)는 *기호 처리기*로 숫자 대신 기호 연산을 지원한다..

[`sympy`](https://www.sympy.org), a *symbolic processor* supports operations in symbols instead of numbers.

2006년 이후 2019 까지 800명이 넘는 개발자가 작성한 코드를 제공하였다.

Since 2006, more than 800 developers contributed so far in 2019.

## 기호 연산 예
Examples of symbolic processing

`sympy` 모듈을 `sym` 라는 이름으로 불러온다.
Import `sympy` module in the name of `sym`.

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

```

비교를 위해 `numpy` 모듈도 불러온다.

Import `numpy` module to compare.

```python
import numpy as np

```

```python
np.pi

```

```python
sym.pi

```

#### 오일러 공식
Euler formula

$$
e ^ {\pi i} + 1 = 0
$$

```python
np.exp(np.pi * 1j) + 1

```

```python
sym.exp(sym.pi * 1j) + 1

```

```python
sym.simplify(sym.exp(sym.pi * 1j) + 1)

```

#### 무한대
Infinity

```python
np.inf, np.inf > 999999

```

```python
sym.oo, sym.oo > 999999

```

### 제곱근
Square root

10의 제곱근을 구해보자.
Let't find the square root of ten.

```python
np.sqrt(10)

```

```python
sym.sqrt(10)

```

결과를 숫자로 살펴보려면 `evalf()` 메소드를 사용한다.

Use `evalf()` method to check the result in digits.

```python
sym.sqrt(10).evalf()

```

```python
sym.sqrt(10).evalf(30)

```

10의 제곱근을 제곱해보자.
Let't square the square root of ten.

```python
np.sqrt(10) ** 2

```

```python
sym.sqrt(10) ** 2

```

위 결과의 차이에 대해 어떻게 생각하는가?

What do you think about the differences of the results above?

### 분수
Fractions

15 / 11 을 생각해보자.
Let't think about 15/11.

```python
num = 15
den = 11

```

```python
division = num / den

```

```python
division

```

```python
division * den

```

```python
import fractions

```

```python
fr_division = fractions.Fraction(num, den)

```

```python
fr_division

```

```python
fr_division * den

```

```python
sym_division = sym.Rational(num, den)

```

```python
sym_division

```

```python
sym_division * den

```

위 결과의 차이에 대해 어떻게 생각하는가?

What do you think about the differences of the results above?

### 변수를 포함하는 수식
Expressions with variables

사용할 변수를 정의.
Define variables to use.

```python
a, b, c, x = sym.symbols('a b c x')
theta, phi = sym.symbols('theta phi')

```

변수들을 한번 살펴보자.
Let's take a look at the variables

```python
a, b, c, x

```

```python
theta, phi

```

변수를 조합하여 새로운 수식을 만들어 보자.

Let's make equations using variables.

```python
y = a * x + b

```

```python
y

```

```python
z = a * x * x + b * x + c

```

```python
z

```

```python
w = a * sym.sin(theta) ** 2 + b

```

```python
w

```

```python
p = (x - a) * (x  - b) * (x - c)

```

```python
p

```

```python
sym.expand(p, x)

```

```python
sym.collect(_, x)

```

$$
\frac{x + xy}{x}
$$

```python
sym.simplify((x + x * y) / x)

```

### 그래프
Plot

```python
import sympy.plotting as splot

```

```python
splot.plot(sym.sin(x));

```

```python
import mpmath
splot.plot(sym.sin(mpmath.radians(x)), (x, -360, 360));

```

```python
splot.plot_parametric((sym.cos(theta), sym.sin(theta)), (theta, -sym.pi, sym.pi));

```

```python
splot.plot_parametric(
    16 * (sym.sin(theta)**3),
    13 * sym.cos(theta) - 5 * sym.cos(2*theta) - 2 * sym.cos(3*theta) - sym.cos(4*theta),
    (theta, -sym.pi, sym.pi)
);

```

#### 3차원 그래프
3D Plot

```python
x, y = sym.symbols('x y')
splot.plot3d(sym.cos(x) + sym.sin(y), (x, -5, 5), (y, -5, 5));

```

```python
splot.plot3d_parametric_line(x, 25-x**2, 25-x**2, (x, -5, 5));

```

```python
u, v = sym.symbols('u v')
splot.plot3d_parametric_surface(u + v, sym.sin(u), sym.cos(u), (u, -1, 1), (v, -1, 1));

```

### 극한
Limits

```python
sym.limit(x ** x, x, 0)

```

$$
\lim_{x \to 0} \frac{sin x}{x}
$$

```python
sym.limit(sym.sin(x) / x, x, 0)

```

$$
\lim_{x \to \infty} x
$$

```python
sym.limit(x, x, sym.oo)

```

$$
\lim_{x \to \infty} \frac{1}{x}
$$

```python
sym.limit(1 / x, x, sym.oo)

```

$$
\lim_{x \to 0} x^x
$$

```python
sym.limit(x ** x, x, 0)

```

### 미적분
Calculus

$$
\frac{dz}{dx} =\frac{d}{dx} \left( a x^2 + bx + c \right)
$$

```python
z.diff(x)

```

$$
\int{z}{dx} =\int{\left(a x^2 + bx + c \right)}{dx}
$$

```python
sym.integrate(z, x)

```

```python
w

```

```python
w.diff(theta)

```

```python
sym.integrate(w, theta)

```

#### 정적분
Definite integral

```python
sym.integrate(w, (theta,0, sym.pi))

```

### 근
Root

```python
z_sol_list = sym.solve(z, x)

```

```python
z_sol_list

```

```python
sym.solve(2* sym.sin(theta) ** 2 - 1, theta)

```

### 코드 생성
Code generation

```python
print(sym.python(z_sol_list[0]))

```

```python
import sympy.utilities.codegen as sc

```

```python
[(c_name, c_code), (h_name, c_header)] = sc.codegen(
    ("z_sol", z_sol_list[0]),
    "C89",
    "test"
)

```

```python
c_name

```

```python
print(c_code)

```

```python
h_name

```

```python
print(c_header)

```

### 방정식
Equation solving

$$
x^4=1
$$

```python
sym.solve(x ** 4 - 1, x)

```

```python
sym.solveset(x ** 4 - 1, x)

```

$$
e^x=-1
$$

```python
sym.solve(sym.exp(x) + 1, x)

```

$$
x^4 - 3x^2 +1
$$

```python
f = x ** 4 - 3 * x ** 2 + 1
sym.factor(f)

```

```python
sym.factor(f, modulus=5)

```

Boolean equations

```python
sym.satisfiable(a & b)

```

```python
sym.satisfiable(a ^ b)

```

### 연립방정식
System of equations

```python
a1, a2, a3 = sym.symbols('a1:4')
b1, b2, b3 = sym.symbols('b1:4')
c1, c2 = sym.symbols('c1:3')
x1, x2 = sym.symbols('x1:3')

```

```python
eq1 = sym.Eq(
    a1 * x1 + a2 * x2,
    c1,
)

```

```python
eq1

```

```python
eq2 = sym.Eq(
    b1 * x1 + b2 * x2,
    c2,
)

```

```python
eq_list = [eq1, eq2]

```

```python
eq_list

```

```python
sym.solve(eq_list, (x1, x2))

```

### 행렬
Matrix

```python
identity = sym.Matrix([[1, 0], [0, 1]])
identity

```

```python
A = sym.Matrix([[1, a], [b, 1]])
A

```

```python
A * identity

```

```python
A * A

```

```python
A ** 2

```

### 미분방정식
Differential Equations

$$
\frac{d^2}{dx^2}f(x) + f(x)
$$

```python
f = sym.Function('f', real=True)
(f(x).diff(x, x) + f(x))

```

```python
sym.dsolve(f(x).diff(x, x) + f(x))

```

기계진동
Mechanical Vibration

$$
m \frac{d^2x(t)}{dt^2} +c \frac{dx(t)}{dt} + x(t) = 0
$$

```python
m, c, k, t = sym.symbols('m c k t')
x = sym.Function('x', real=True)
vib_eq = m * x(t).diff(t, t) + c * x(t).diff(t) + k * x(t)
vib_eq

```

```python
result = sym.dsolve(vib_eq)
result

```

```python
sym.simplify(result)

```

강제진동
Forced Vibration

$$
m \frac{d^2x(t)}{dt^2} +c \frac{dx(t)}{dt} + x(t) = sin(t)
$$

```python
forced_vib_eq = m * x(t).diff(t, t) + c * x(t).diff(t) + k * x(t) - sym.sin(t)
forced_vib_eq

```

```python
result = sym.dsolve(forced_vib_eq)
result

```

```python
sym.simplify(result)

```

## 참고문헌
References

* SymPy Development Team, SymPy 1.4 documentation, sympy.org, 2019 04 10. [Online] Available : https://docs/sympy.org/latest/index.html.
* SymPy Development Team, SymPy Tutorial, SymPy 1.4 documentation, sympy.org, 2019 04 10. [Online] Available : https://docs/sympy.org/latest/tutorial/index.html.
* d84_n1nj4, "How to keep fractions in your equation output", Stackoverflow.com, 2017 08 12. [Online] Available : https://stackoverflow.com/a/45651175.
* Python developers, "Fractions", Python documentation, 2019 10 12. [Online] Available : https://docs.python.org/3.7/library/fractions.html.
* SymPy Development Team, codegen, SymPy 1.4 documentation, sympy.org, 2019 04 10. [Online] Available : https://docs/sympy.org/latest/modules/utilities/codegen.html.
* Pedregosa, F., Sympy : Symbolic Mathematics in Python, Scipy Lecture Notes, 2019 March,[Online] Available : http://www.scipy-lectures.org/packages/sympy.html [Accessed 2019 10 28]
* MIT, Twitter, 2021 Feb. [Online] Available : https://twitter.com/MIT/status/1360971008325406721.

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```


```matlab
% ignore - internal setup
path('../scripts/refrig_line', path)
set(0, 'DefaultLineLineWidth', 2)
set(0, 'DefaultAxesFontSize', 14);
```

# Bracketing Methods (Bisection example)

Consider the refrigeration tank example from Belegundu and Chandrupatla [1].
We want to minimize the cost of a cylindrical refrigeration tank that must have
a volume of 50 m$^3$.
The costs of the tank are
- Circular ends cost \$10 per m$^2$
- Cylindrical walls cost \$6 per m$^2$
- Refrigerator costs \$80 per m$^2$ over its life

Let $d$ be the tank diameter, and $L$ the height.

\begin{align}
  f &= 10  \left(\frac{2 \pi d^2}{4}\right) + 6 (\pi d L) + 80 \left( \frac{2
\pi d^2}{4} + \pi d L \right)\\
  &= 45 \pi d^2 + 86 \pi d L
\end{align}

However, $L$ is a function of $d$ because the volume is constrained.  We could
add a constraint to the problem
\begin{align}
  \frac{\pi d^2}{4} L = V
\end{align}
but it is easier to express $V$ as a function of $d$ and make the problem
unconstrained.
\begin{align}
  L &= \frac{4 V}{\pi d^2}\\
  &= \frac{200}{\pi d^2}
\end{align}
Thus the optimization can be expressed as
\begin{align}
\textrm{minimize} &\quad 45 \pi d^2 + \frac{17200}{d}\\
\textrm{with respect to} &\quad d \\
\textrm{subject to} &\quad d \ge 0
\end{align}

One-dimensional optimization problems are silly of course, we can just find the
minimum by looking at a plot.  However, we use a one-dimensional example to
illustrate line searches.  A line search seeks an approximate minimum to a one-dimensional optimization problem within a N-dimensional space.

We will use bisection to find the minimum of this function.  This is a recursive
function.

```matlab
function [x, history] = bisection(x1, x2, f1, f2, fh, history)
%{
This function finds the minimum of a function using bisection.
It can also easily be used to find the root of a function,
but for simplicity we've not added that second case.
Inputs:
    x1: lower bound
    x2: upper bound
    f1: function (or gradient) at lower bound
    f2: function (or gradient) at upper bound
        f1 * f2 must be < 0.  Currently left up to the user to check.
    fh: function handle.  should be of form [f, g] = fh(x)
        where f is the funciton value and g = df/dx
    history: input an empty array and the iteration history for the
             mean value of x will be appended at each iteration
Outputs:
    x: new value for x
    history: appended history array
%}

% new point at midpoint
x = 0.5*(x1 + x2);

% save iteration history
history = [history, x];

% check for convergence
if (abs(x1 - x2) < 1e-6)  % simplistic check
    return;
end

% evaluate function at new point
% change to [f, ~] = fh(x) to make this a root solver (and input function
% values instead of gradients
[~, f] = fh(x);   % get new gradient value

% determine which bracket to keep
if (f*f1 < 0)
    x2 = x;
    f2 = f;
else
    x1 = x;
    f1 = f;
end

% perform bisection on new bracket (recursive)
[x, history] = bisection(x1, x2, f1, f2, fh, history);

end
```

We are interseted in optimization, so we don't want to find the root of our
function, but rather the "root" of the derivative as a potential minimum point.
Let's define our objectve function, its derivative, and solve for the minium.

```matlab
function [f, g] = refrig(d)
%{
This function computes the cost of the cylindrical refrigeration tank
as a function of diameter.
Inputs:
    d: diameter (m)
Outputs:
    f: cost ($)
    g: gradient df/dd
%}

% function value
f = 45*pi*d.^2 + 17200./d;

% gradient
g = 90*pi*d - 17200./d.^2;

end
```

```matlab
clear; close all;

% pick intial bracket on diameters
d1 = 1;
d2 = 10;

% evalaute function
[f1, g1] = refrig(d1);
[f2, g2] = refrig(d2);

% check that our bracket is ok
assert(g1*g2 < 0);

% find optimal point
dhist = [];
[dopt, dhist] = bisection(d1, d2, g1, g2, @refrig, dhist);

% plot function
dvec = linspace(d1, d2, 200);
figure(); hold on;
plot(dvec, refrig(dvec)/1e3);
plot(dopt, refrig(dopt)/1e3, 'r*');
xlabel('diameter (m)');
ylabel('cost (thousands of dollars)');
```

```matlab
% plot convergence history
error = abs(dhist(1:end-1) - dhist(2:end))./(abs(dhist(2:end)));

figure();
semilogy(error);  % note use of semilog
xlabel('iteration');
ylabel('normalized error');
box('off')
```

Note the linear convergence behavior.

[1] Belegundu, A. D. and Chandrupatla, T. R., Optimization Concepts and Applications in Engineering, Cambridge University Press, Mar 2011.

```matlab

```


```python
from decodes.core import *
from decodes.io.jupyter_out import JupyterOut
out = JupyterOut.unit_square( )
```

# Dot Product
The dot product of two vectors is such a commonly used function, it is naturally incorporated into the Decod.es `Vec` class as a method.

Notice that ***the dot product operation simply produces a scalar value***.

```python
"""
Dot Product
"""
def dot(self,other):
    return float(self.x * other.x + self.y * other.y + self.z * other.z)

```

## Applications of the Dot Product

Gaining an intuition for the dot product is most easily grasped by illustrating what it does, rather than what it is. Dot prodcuts are most often used in the calculation of ***projections***, and in operations dealing with ***angular measurement and comparison***.

### Dot Product in Angle Calculation

The dot product is directly related to the angle between two vectors.

Look at the sign of the dot product of two vectors, and we may learn something about their relationship.

* If the dot product is a **positive number**, the angle is **acute**.
* If the dot product is **negative**, the angle is **obtuse**.
* If the dot product is **zero**, then the two given vectors are **perpendicular**.

A more precise way to capture this relationship can be done by invoking another law of trigonometry – the law of cosines – which gives a relationship between the lengths of the sides of the triangle determined by two vectors $\vec{v_{1}}$ and $\vec{v_{2}}$ and $\theta$, the smaller of the angles between the two vectors:

\begin{align}
\lvert \vec{v_{1}}\rvert^2 + \lvert \vec{v_{2}}\rvert^2 - 2 \lvert \vec{v_{1}}\rvert \lvert \vec{v_{2}}\rvert cos \theta = \lvert \vec{v_{1}} - \vec{v_{2}}\rvert^2
\end{align}

Notice that when the vectors are perpendicular to each other, this expression reduces to Pythagoras’ theorem.

Expanding out in coordinates as before, and then consolidating terms, we arrive at the following simplified identity that links the dot product to the cosine of the angle (in radians):

\begin{align}
\vec{v_{1}} \bullet \vec{v_{2}} = \lvert \vec{v_{1}}\rvert \lvert \vec{v_{2}}\rvert cos \theta
\end{align}

```python
"""
Angle Between Two Vectors
Returns the angle in radians, value constrained to the range [-PI,PI].
"""
def angle(self,other):
    vdot = self.dot(other) / (self.length * other.length)
    # ensures that vdot is between -1.0 and 1.0
    vdot = Interval(-1.0,1.0).limit_val(vdot)
    return math.acos(vdot)

```

### Dot Product in Vector Projections

Two useful quantities that can be calculated using the dot product are:
* **the projected length**: the length measured along one vector at which the head of another falls.
* **the projected vector**: a vector in the direction of the second vector scaled to the projected length

Imagine two vectors, $\vec{v_{1}}$ and $\vec{v_{2}}$, and an operation which projects the head of $\vec{v_{1}}$ along a line coincident with $\vec{v_{2}}$. This operation is termed an **orthogonal projection**, meaning the projection line lies at a 90˚ angle to $\vec{v_{2}}$.

In our projection scenario, the projected length is the length measured along $\vec{v_{2}}$ at which the head of $\vec{v_{1}}$ falls when projected orthogonally. Using basic trigonometry, this length is given by $\lvert \vec{v_{1}} \rvert cos \theta$.

Using the relationship between the angle and the dot product, we can see that when $\vec{v_{2}}$ is normalized to unit length, this length may be reduced down to a dot product.

\begin{align}
L = \vec{v_{1}} \bullet \frac{\vec{v_{2}}}{\lvert \vec{v_{2}} \rvert} = \vec{v_{1}} \bullet \vec{u_{2}}
\end{align}

This combination of terms is now manifest as something concrete, and ***the dot product is often defined as the projected length of one vector upon another.***

Associated to the projected length is the **projected vector**, defined as a vector in the direction of $\vec{v_{2}}$ that has been scaled to the projected length. This may be calculated as the product of the projected length and the unit vector in the direction of $\vec{v_{2}}$:

\begin{align}
L\vec{u_{2}} = (\vec{v_{1}} \bullet \vec{u_{2}}) \vec{u_{2}}
\end{align}

These mathematical formulae translate readily into the object methods `vec.projected_length()` and `vec.projected()`:

```python
"""

Projected Length Method
Returns the length of the vector which results from projecting this vector
onto a destination vector.
"""
def projected_length(self,other):
    return self.dot(other.normalized())
```

```python
"""
Projected  Method
Returns a new vector which results from projecting this vector onto a
destination vector.
"""
def projected(self,other):
    return other * ( self.dot(other) / other.dot(other) )
```

### Dot Product in Nearest Point Projection

For the case of a line segment, the projection of a nearest point is exactly given by the projected vector of $\vec{v}$ in the direction defined by the line.

Notice that the operation below assumes that the given line is infinite, and the projection location may lie outside of the bounds described by

```python
"""
Nearest Point on a Line Segment
Given a line segment (with start-point, end-point, and vector members),
finds the point on this line which is nearest to a given point
"""
vec = Vec(seg.spt, pt)
pt_near = seg.spt + vec.projected(seg.ept)

```

```python
"""
Mirror Reflection Across Line
"""
pt_mirror = pt_near + Vec(pt, pt_near)

```

103 - Points, Vectors, and Coordinate Systems/088 - Dot Product.ipynb


# Sinais pares (even) e ímpares (odd)

Neste notebook avaliaremos o que são sinais pares (even) e ímpares (odd) e como decompor um sinal que não é par ou ímpar em suas partes par e ímpar. Esta propriedade dos sinais está ligada a ***reflexão no tempo***, uma transformação da variável independente.

## Sinal par (even)

Um sinal é dito ter simetria par se:

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

ou no caso de um sinal discreto

\begin{equation}
x[-n] = x[n]
\end{equation}

## Sinal ímpar (odd)

Um sinal é dito ter simetria ímpar se:

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

ou no caso de um sinal discreto

\begin{equation}
x[-n] = -x[n]
\end{equation}
o que implica que $x(0) = 0$ e  $x[0] = 0$.

Se temos um sinal $x(t)$ que não é par ou ímpar, é possível decompor tal sinal em: um sinal par e um sinal ímpar. As partes par (even) e ímpar (odd) são dadas por:

\begin{equation}
\epsilon v\left\{x(t)\right\}= \frac{1}{2}\left[x(t) + x(-t)\right]
\end{equation}

\begin{equation}
\mathcal{O}d\left\{x(t)\right\}= \frac{1}{2}\left[x(t) - x(-t)\right]
\end{equation}

```python
# importar as bibliotecas necessárias
import numpy as np
import matplotlib.pyplot as plt
```

# Exemplos de sinais pares

1. A exponencial do módulo

\begin{equation}
x_1(t) = \mathrm{e}^{-|t|}
\end{equation}

2. A função cosseno

\begin{equation}
x_2(t) = \mathrm{cos}(2 \pi f t)
\end{equation}

```python
t = np.linspace(-5, 5, 1000) # vetor temporal

# Exp do módulo
x1t = np.exp(-np.abs(t))

# Cosseno
x2t = np.cos(2*np.pi*0.25*t)

# Figura
plt.figure()
plt.title('Sinal par')
plt.plot(t, x1t, '-b', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.xlabel('Tempo [s]')
plt.ylabel('Amplitude [-]')
plt.tight_layout()
plt.show()

# Figura
plt.figure()
plt.title('Sinal par')
plt.plot(t, x2t, '-b', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.xlabel('Tempo [s]')
plt.ylabel('Amplitude [-]')
plt.tight_layout()
plt.show()
```

# Exemplos de sinais ímpares

1. A função sigmoide

\begin{equation}
x_1(t) = \frac{1}{1+\mathrm{e}^{-t}}-0.5
\end{equation}

2. A função seno

\begin{equation}
x_2(t) = \mathrm{sin}(2 \pi f t)
\end{equation}

```python
t = np.linspace(-5, 5, 1000) # vetor temporal

# Sigmoide
x1t = 1/(1+np.exp(-t))-0.5

# Seno
x2t = np.sin(2*np.pi*0.25*t)

# Figura
plt.figure()
plt.title('Sinal ímpar')
plt.plot(t, x1t, '-r', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.xlabel('Tempo [s]')
plt.ylabel('Amplitude [-]')
plt.tight_layout()
plt.show()

# Figura
plt.figure()
plt.title('Sinal ímpar')
plt.plot(t, x2t, '-r', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.xlabel('Tempo [s]')
plt.ylabel('Amplitude [-]')
plt.tight_layout()
plt.show()
```

# Decomposição do sinal nas partes par e ímpar

```python
t = np.linspace(-1, 1, 20) # vetor temporal
xt = np.random.randn(len(t)) # Um sinal aleatório

# parte par
xt_ev = 0.5*(xt + np.flip(xt))

# parte par
xt_odd = 0.5*(xt - np.flip(xt))

# Figura
plt.figure()
plt.title('Sinal nem par nem ímpar')
plt.plot(t, xt, '-k', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.xlabel('Tempo [s]')
plt.ylabel('Amplitude [-]')
plt.tight_layout()
plt.show()

plt.figure()
plt.subplot(2,1,1)
plt.title('Parte par')
plt.plot(t, xt_ev, '-b', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.ylabel('Amplitude [-]')
plt.subplot(2,1,2)
plt.title('Parte ímpar')
plt.plot(t, xt_odd, '-r', linewidth = 2)
plt.grid(linestyle = '--', which='both')
plt.xlabel('Tempo [s]')
plt.ylabel('Amplitude [-]')
plt.tight_layout()
plt.show()
```

Aula 4 - Sinais pares e impares/Sinais pares e impares.ipynb


##### Periodic homogenization of linear elastic materials

This notebook is developed based on the tutorial **Numerical Tours of Computational Mechanics with FEniCS** to solve a microscopic boundary value problem arising the computational homogenization of linear elastic materials.

In this notebook, I try to give one example that illustrates inconsistencies between the results obtained in this notebook and those obtained in the tutorial. I think there might be some minor bug or mistakes in the tutorial due to three reasons.

1. The macroscopic stiffness is not symmetric. Theoretically, this property should be (largely) independent of the mesh discretization in the finite element method as it holds true for the continuous setting (exact solution).
2. The homogenized with the circular inclusion should be isotropic so that the macroscopic tangent can be defined by $\overline{\lambda}$ and $\overline{\mu}$ which are the homogenized Lame parameters.
3. The components of the macroscopic tangent should be non-negative.

For the above three reasons, I am afraid that there could be some minor bugs in the implementation. This notebook provides a another implementation that produces numerical results that respect the properties mentioned in the above bullets.

```python
from __future__ import print_function
from dolfin import *
import numpy as np
import mshr
import matplotlib.pyplot as plt
# %matplotlib notebook
%matplotlib inline
```

The mesh provided by the class **mshr** and its method *generate_mesh* automatically produces periodic mesh. This can be justified by examining the nodes of boundaries and easily seen for the simple case of UnitSquareMesh. Therefore, we will use the class **mshr** for our mesh generation and do not need to rely on the **gmsh** at this point.

```python
# Create domain, mark subdomains and generate mesh
vertices = np.array([[0, 0], [1, 0.25], [1.25, 1.25], [0.25, 1]])
domain = mshr.Polygon([Point(vertices[0,:]), Point(vertices[1,:]),
                       Point(vertices[2,:]), Point(vertices[3,:])])
inclusion = mshr.Circle(Point(1.25/2, 1.25/2), 0.2)
domain.set_subdomain(1, inclusion)

mesh = mshr.generate_mesh(domain, 24)
subdomain_marker = MeshFunction('size_t', mesh, mesh.topology().dim(),
                               mesh.domains())

# Compute volume of RVE
a = np.zeros((2, 2))      # direction vectors that define RVE.
a[0,:] = vertices[1,:] - vertices[0,:]
a[1,:] = vertices[3,:] - vertices[0,:]
vol = np.linalg.det(a); print("volume = ", vol)

# Vectors normal to direction vectors a.
n = np.zeros((2, 2))
n[0,:] = [a[0, 1], -a[0, 0]]
n[1,:] = [a[1, 1], -a[1, 0]]

assert np.dot(a[0,:], n[0,:]) < 1E-15
assert np.dot(a[1,:], n[1,:]) < 1E-15

# Plot the mesh and subdomains
plt.figure(figsize=(10, 4), dpi = 80)
plt.subplot(1,2,1)
plot(mesh); plt.grid()
plt.subplot(1,2,2)
plot(subdomain_marker); plt.grid()
```

By default, the **mshr** class can give us the periodic mesh. Therefore, we don't rely on **gmsh** in this notebook to create a mesh satisfying the periodicity.

```python
# Check if the nodes on the left and the right boundaries are symmetric (periodic).
# left boundary is detected by the condition: the vector (x - v[0,:]) must be orthogonal to n[1,:].
xx = mesh.coordinates()[:,0]
yy = mesh.coordinates()[:,1]
coords = mesh.coordinates()
x_vector = coords - vertices[0,:]
left_idx = np.argwhere(abs(np.dot(n[1,:], x_vector.T)) < 1E-10).ravel()
bottom_idx = np.argwhere(abs(np.dot(n[0,:], x_vector.T)) < 1E-10).ravel()
# right boundary is detected by the condition: the vector (x - v[1,:]) is orthogonal to n[1,:]
x_vector = coords - vertices[1,:]
right_idx = np.argwhere(abs(np.dot(n[1,:], x_vector.T)) < 1E-10).ravel()
top_idx = np.argwhere(abs(np.dot(n[0,:], (coords - vertices[3,:]).T)) < 1E-10).ravel()
# we sort the nodes in the x-coordinates
left_idx = left_idx[np.argsort(xx[left_idx])]
right_idx = right_idx[np.argsort(xx[right_idx])]
bottom_idx = bottom_idx[np.argsort(xx[bottom_idx])]
top_idx = top_idx[np.argsort(xx[top_idx])]
# check periodicity
print("left-right periodic: ", np.allclose(coords[right_idx] - coords[left_idx], a[0,:]))
print("bottom-top periodic: ", np.allclose(coords[top_idx] - coords[bottom_idx], a[1,:]))
```

    left-right periodic:  True
    bottom-top periodic:  True

```python
# class used to define the periodic boundary map
class PeriodicBoundary(SubDomain):
    # Constructor
    def __init__(self, vertices, TOL=DOLFIN_EPS_LARGE):
        super().__init__()
        self.TOL = TOL
        self.vertices = vertices
        # vectors generating periodicity
        self.a = np.zeros((2, 2))
        self.a[0,:] = vertices[1,:] - vertices[0,:]
        self.a[1,:] = vertices[3,:] - vertices[0,:]

        # vectors normal to vectors a[0,:] and a[1,:]
        self.n = np.zeros((2,2))
        self.n[0,:] = [ -self.a[0,1], self.a[0,0] ]
        self.n[1,:] = [ -self.a[1,1], self.a[1,0] ]

        assert np.dot(self.n[0,:], self.a[0,:]) < self.TOL
        assert np.dot(self.n[1,:], self.a[1,:]) < self.TOL

    def inside(self, x, on_boundary):
        # return True if x is on the left or bottom boundaries
        # AND NOT on one of the bottom-right or top-left vertices
        top_left = np.allclose(x, self.vertices[3,:], self.TOL)
        bottom_right = np.allclose(x, self.vertices[1,:], self.TOL)
        x_vector = x - self.vertices[0,:]
        bottom = (np.abs(np.dot(self.n[0,:], x_vector)) < self.TOL)
        left = (np.abs(np.dot(self.n[1,:], x_vector)) < self.TOL)

        return (left or bottom) and \
            (not (top_left or bottom_right)) and on_boundary
    # Map right boundary (R) to the left boundary (L)
    def map(self, x, y):
        # top right node
        if np.allclose(x, self.vertices[2,:]):
            y[0] = x[0] - (self.a[0, 0] + self.a[1,0])
            y[1] = x[1] - (self.a[0, 1] + self.a[1,1])
        # right boundary
        elif np.abs(np.dot(self.n[1,:], x - self.vertices[1,:])) < self.TOL:
            y[0] = x[0] - self.a[0,0]
            y[1] = x[1] - self.a[0,1]
        # top boundary
        else:
            y[0] = x[0] - self.a[1,0]
            y[1] = x[1] - self.a[1,1]
# Create an instance of periodic boundary condition
vertices = np.array([[0, 0], [1, 0.25], [1.25, 1.25], [0.25, 1]])

pbc = PeriodicBoundary(vertices)
```

We now define the constitutive law for both phases. We employ the energy density for linear isotropic elasicity
\begin{equation}
    \psi = \frac{\lambda}{2} \mathrm{trace}(\boldsymbol{\epsilon})^2 + \mu\,{} \mathrm{trace}(\boldsymbol{\epsilon}^2).
\end{equation}
The Lame parameters $\lambda$ and $\mu$ are computed in terms of the Young's modulus and Poisson ratio as follows:
\begin{equation}
    \lambda = \frac{E \nu}{(1 + \nu) (1 - 2\nu}, \quad \mu = \frac{E}{2(1 + \nu)}.
\end{equation}

The stress tensor $\boldsymbol{\sigma}$ is derived as
\begin{equation}
    \boldsymbol{\sigma} = \lambda \mathrm{trace}(\boldsymbol{\epsilon})\mathbf{I} + 2\mu \boldsymbol{\epsilon}
\end{equation}
The tangent stiffness, which is linear in this case, is given by
\begin{equation}
    C_{ijkl} = \lambda \delta_{ij}\delta_{kl} + \mu(\delta_{il}\delta_{jk} + \delta_{ik}\delta_{jl})
\end{equation}
Due to the symmetries of the $\boldsymbol{\delta}$, the stiffness is (1) majorly symmetric: $C_{ijkl} = C_{klij}$ and (2) minorly symmetric: $C_{ijkl} = C_{jikl} = C_{ijlk}$.

Since both $\lambda$ and $\mu$ can be scaled by a multiplicative factor $E$, we can define the normalized energy density as
\begin{equation}
    \overline{\psi} = \frac{1}{E} \psi = \frac{ \overline{\lambda}}{2} \mathrm{trace}(\boldsymbol{\epsilon})^2 + \overline{\mu}\,{} \mathrm{trace}(\boldsymbol{\epsilon}^2), \qquad \overline{\lambda} = \frac{\lambda}{E}, \overline{\mu} = \frac{\mu}{E}
\end{equation}
By using the normalized energy density, we can focus on the phase contrast between the matrix and inclusion materials. Using the normalized energy density, the numerical results are normally more reliable. This technique is not necessary for this simple problem but it is done in this notebook.

```python
# material parameters for the matrix and inclusion
E = np.array([50e3, 210e3])
E_ratio = E/np.min(E)
nu = np.array([0.2, 0.3])
num_phases = len(E)
lambda_ = E_ratio * nu /(1 + nu)/(1 - 2*nu)
mu_ = 0.5 * E_ratio / (1 + nu)
# define strain and stress tensors
def epsilon(v):
    return sym(grad(v))

def sigma(v, i, MacroStrain):
    return lambda_[i]*tr(sym(grad(v)) + MacroStrain)*Identity(2) \
            + 2*mu_[i]*(sym(grad(v)) + MacroStrain)
```

```python
Ve = VectorElement("CG", mesh.ufl_cell(), 2)
Re = VectorElement("R", mesh.ufl_cell(), 0)
W = FunctionSpace(mesh, MixedElement([Ve, Re]), constrained_domain=pbc)
V = FunctionSpace(mesh, Ve)

u, c = TestFunctions(W)
v, d = TrialFunctions(W)
w = Function(W)
dx = Measure('dx', subdomain_data=subdomain_marker)
```

```python
I = np.eye(3, 3)

C_Macro = np.zeros((3, 3))
for irun in range(3):
    MacroStrain = Constant(np.array([ [I[0, irun], I[2, irun]/2],
                                    [I[2, irun]/2, I[1, irun] ]]))

    F = sum([inner(sigma(u, i, MacroStrain), epsilon(v))*dx(i) for i in range(2)]) + dot(c, v)*dx + dot(u, d)*dx
    a, L = lhs(F), rhs(F)
    solve(a == L, w, [])

    (u_Fluc, lagrange_mul) = w.split()

    stress_tensor = np.array([[assemble(
                        sigma(u_Fluc, 0, MacroStrain)[i,j]*dx(0) \
                        + sigma(u_Fluc, 1, MacroStrain)[i,j]*dx(1)) \
                        for i in range(2)] for j in range(2)])
    C_Macro[:, irun] = np.array([stress_tensor[0,0],
                                 stress_tensor[1,1],
                                 stress_tensor[0,1]])/vol

print(C_Macro)
```

    Calling FFC just-in-time (JIT) compiler, this may take some time.
    Calling FFC just-in-time (JIT) compiler, this may take some time.
    Calling FFC just-in-time (JIT) compiler, this may take some time.
    Calling FFC just-in-time (JIT) compiler, this may take some time.
    Calling FFC just-in-time (JIT) compiler, this may take some time.
    [[1.71098072 0.56106521 0.        ]
     [0.56106521 1.71098072 0.        ]
     [0.         0.         0.57495775]]

```python
## Validate isotropic materials:
# Symmetric.
print("Symmetric =", np.allclose(C_Macro, C_Macro.T))

# C_Macro[0, 0] = Lambda + 2*Mu
# C_Macro[0, 1] = Lambda
# C_Macro[2, 2] = Mu
print("C_{11} == C_{12} + C_{33}:", np.allclose(C_Macro[0,0], C_Macro[0,1] + 2*C_Macro[2,2]))

print(C_Macro[0, 0])
print(C_Macro[0,1] + 2*C_Macro[2,2])
```

    Symmetric = True
    C_{11} == C_{12} + C_{33}: True
    1.7109807187122172
    1.7109807187122161

```python
# plotting deformed unit cell with total displacement u = MacroStrain*y + v
y = SpatialCoordinate(mesh)
plt.figure(figsize=(6, 6), dpi=80)

plot(mesh); plt.colorbar( plot(0.5*(dot(MacroStrain, y)+ u_Fluc), mode="displacement") ); plt.grid()
plt.axis([-0.2, 1.8, -0.2, 1.8])
```

##### Extra codes for backup (if something goes wrong)

```python
# class used to define the periodic boundary map
class PeriodicBoundary(SubDomain):
    # Constructor
    def __init__(self, vertices, TOL):
        super().__init__()
        self.vertices = vertices
        self.TOL = TOL
    # Left boundary is "tangent domain" G
    def inside(self, x, on_boundary):
        # return True if x is on the left or bottom boundaries
        # AND NOT on one of the bottom-right or top-left vertices
        left = np.dot(np.array([-1, 0]), x - self.vertices[0,:]) < self.TOL
        bottom = np.dot(np.array([0, 1]), x - self.vertices[0,:]) < self.TOL
        top_left = np.allclose(x, self.vertices[3,:], self.TOL)
        bottom_right = np.allclose(x, self.vertices[1,:], self.TOL)
        return (left or bottom) and \
            (not (top_left or bottom_right)) and on_boundary
    # Map right boundary (R) to the left boundary (L)
    def map(self, x, y):
        # top right
        a1 = self.vertices[1,:] - self.vertices[0,:]
        a2 = self.vertices[3,:] - self.vertices[0,:]
        n1 = np.array([-a1[1], a1[0]])
        n2 = np.array([-a2[1], a2[0]])
        if np.allclose(x, self.vertices[2,:], self.TOL):
            y[0] = x[0] - (a1 + a2)
        # right boundary
        elif np.abs(np.dot(n2, x - self.vertices[2,:])) < self.TOL:
            y[0] = x[0] - a1[0]
            y[1] = x[1] - a1[1]
            # y[0] = x[0] - (self.vertices[1,0] - self.vertices[0,0])
            # y[1] = x[1]
        else:    # top boundary
            y[0] = x[0]
            y[1] = x[1] - (self.vertices[1,1] - self.vertices[0,1])

# Create an instance of periodic boundary condition
vertices = np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]])
pbc = PeriodicBoundary(vertices, 1E-8)
```

```python
MacroStrain = Constant(np.array([[1, 0], [0, 0]]))

# bilinear form (left-hand side)
a = sum([inner(lambda_[i]*tr(epsilon(u))*Identity(2) + 2*mu_[i]*epsilon(u), epsilon(v))*dx(i) for i in range(2)]) \
    + dot(c, v)*dx + dot(u, d)*dx
# linear form (right-hand side)
L = -sum([inner(lambda_[i]*tr(MacroStrain)*Identity(2) + 2*mu_[i]*MacroStrain, epsilon(v))*dx(i) for i in range(2)])
solve(a == L, w, [])

(u_Fluc, lagrange_mul) = w.split()
np.array([[assemble(sigma(u_Fluc, 0, MacroStrain)[i,j]*dx(0) + sigma(u_Fluc, 1, MacroStrain)[i,j]*dx(1)) \
  for i in range(2)] for j in range(2)])
```

    array([[6.06704403e+04, 5.43500255e+00],
           [5.43500255e+00, 1.60408364e+04]])

```python
# Create domain, mark subdomains and generate mesh

vertices = np.array([[-1, -1], [1, 1]])
domain = mshr.Rectangle(Point(-1, -1), Point(1, 1))
inclusion = mshr.Circle(Point(0, 0), 0.5)
domain.set_subdomain(1, inclusion)
mesh = mshr.generate_mesh(domain, 48)
# Volume of the RVE.
vol = (vertices[1,0] - vertices[0,0])*(vertices[1,1] - vertices[0,1])

domain_marker = MeshFunction('size_t', mesh, mesh.topology().dim(),
                             mesh.domains())
p = plot(domain_marker); plt.colorbar(p)
```


# Семинар №1

# **Работа с растровыми изображенями**
**Растровое изображение** $-$ изображение, представляющее собой сетку пикселей — цветных точек.
Важными характеристиками изображения являются: размер, глубина цвета и цветовое пространство.

Самый распространённый способ выделить объект $-$ это цвет.

**Цвет** $-$ это свойство тел отражать или испускать видимое излучение определенного спектрального состава и интенсивности.

Трихроматическая теория (сетчатка глаза имеет 3 вида рецепторов света, ответственных за цветное зрение) полагает, что достаточно всего трёх чисел, чтобы описать цвет (красный, синий, зелёный). Т.е. используя три значения **R, G, B**

Цветовые пространства бывают линейные и нелинейные.
К линейным относится **RGB**. Изучим его подробней.

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

# отображение графиков в ноутбуке
%matplotlib inline
```

```python
# считаем изображение
image = cv2.imread('img/RGB_cube.png')
```

```python
# посмотрим, какой тип объекта
type(image), image.dtype
```

```python
# размерность
image.shape
```

```python
# преобразуем RGB в BGR для корректного отображения
# это необходимо из-за особенностей matplotlib
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# отобразим объект
plt.imshow(image)
plt.axis('off');
```

## Grayscale

**Grayscale** - цветовой режим изображений, которые отображаются в оттенках серого цвета, размещённые в виде таблицы в качестве эталонов яркости белого цвета.

В компьютерном представлении широко распространённая серая шкала использует на каждый пиксел изображения один байт ($8$ бит) информации. Такая шкала передаёт $256$ оттенков (градаций) серого цвета, или яркости (значение $0$ представляет чёрный цвет, а значение $255$ $-$ белый).

Преобразование цветного изображения в оттенки серого из **RGB** пересчитывают по формуле:

\begin{align}
\ Y' & = 0.2126R + 0.7152G + 0.0722B \\
\end{align}

В **OpenCV_** сменить цветовой режим изображения можно с помощью функции **cv2.cvtColor(img, code)**

* **img** $-$ исходное изображение
* **code** $-$ кодировка для смены цвета.

Все кодировки есть в [документации](https://docs.opencv.org/3.4/d8/d01/group__imgproc__color__conversions.html#ga397ae87e1288a81d2363b61574eb8cab). В рамках занятий мы рассмотрим лишь их часть.

```python
# создадим grayscale изображение с помощью openCV
gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)

fig, axs = plt.subplots(1, 2, figsize = (12, 8))
ax1, ax2 = axs

ax1.set_title('RGB изображение', fontsize=15)
ax1.imshow(image)
ax2.set_title('Grayscale изображение', fontsize=15)
ax2.imshow(gray_image, cmap='gray')

axs = [ax.axis('off') for ax in axs.flatten()]
```

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

```python
# R scale
r = image[:, :, 2].copy()
r[(r >= 250) & (r <= 250)] = 0

# G scale
g = image[:, :, 1].copy()
g[(g >= 250) & (g <= 250)] = 0

# B scale
b = image[:, :, 0].copy()
b[(b >= 250) & (b <= 250)] = 0
```

```python
## посмотрим на результат
fig, axs = plt.subplots(1, 4, figsize = (20, 9))
ax1, ax2, ax3, ax4 = axs

ax1.set_title('R канал', fontsize=15)
ax1.imshow(r, cmap='gray')
ax2.set_title('G канал', fontsize=15)
ax2.imshow(g, cmap='gray')
ax3.set_title('B канал', fontsize=15)
ax3.imshow(b, cmap='gray')
ax4.set_title('RGB', fontsize=15)
ax4.imshow(image)

axs = [ax.axis('off') for ax in axs.flatten()]
```

***Контрольные вопросы:***

1. Почему так получилось?

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

```python
tr_val = 155  # пороговое значение

# R scale
r = image[:, :, 2].copy()
r[r < tr_val] = 0

# G scale
g = image[:, :, 1].copy()
g[g < tr_val] = 0

# B scale
b = image[:, :, 0].copy()
b[b < tr_val] = 0
```

```python
## посмотрим на результат
fig, axs = plt.subplots(1, 4, figsize = (20, 9))
ax1, ax2, ax3, ax4 = axs

ax1.set_title('R канал', fontsize=15)
ax1.imshow(r, cmap='gray')
ax2.set_title('G канал', fontsize=15)
ax2.imshow(g, cmap='gray')
ax3.set_title('B канал', fontsize=15)
ax3.imshow(b, cmap='gray')
ax4.set_title('RGB', fontsize=15)
ax4.imshow(image)

axs = [ax.axis('off') for ax in axs.flatten()]
```

### ***Контрольные вопросы:***

1. Какой вывод можно сделать из этих примеров?

2. Чем неудобна **RGB** кодировка цвета?

## Поиск по цвету

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

Как мы можем в этом найти кота? Для начала мы можем оценить глазами, что цвет кота отличается от цвета окружения – он ярко рыжий и отлично виден в траве. Как мы можем выделить этот цвет в OpenCV? Для этого существует специальная функция ```cv2.inRange(image, low_color, max_color)```, она принимает на вход изображение и диапазон цвета, который мы хотим выделить. **На выходе мы получаем черно-белое изображение**, где белым выделены пиксели, цвета которых попадали в диапазон, а черным $-$ все другое. Дальше мы попробуем найти кота.

### Sliding Windows

В классическом CV есть устоявшееся идея про скользящие окна, к которой еще не раз будем ссылаться в курсе. Идея очень простая и понятна из названия - будем пробегать неким окном по изображению и выполнять какие-то функции в каждом окне. Что конкретно делать и когда уже зависит от конкретной задачи и алгоритма. Пока сделаем само скользящее окно, чтобы использовать его дальше.

```python
# создадим генератор для скользящего окна
def sliding_window(image, stepSize, windowSize):
    # скользим по каждому индексу с заданным шагом
    for y in range(0, image.shape[0], stepSize):
        for x in range(0, image.shape[1], stepSize):
            # возвращаем текущее окно
            yield (x, y, image[y:y + windowSize[1], x:x + windowSize[0]])
```

```python
import time

# размеры скользящего окна
winW, winH = 64, 64
step = 32

# loop over the sliding window for each layer of the pyramid
for (x, y, window) in sliding_window(image, stepSize=step, windowSize=(winW, winH)):
    # if the window does not meet our desired window size, ignore it
    if window.shape[0] != winH or window.shape[1] != winW:
        continue

    # в этом месте идет какая-то фугкция обработки окна

    # отрисуем окно для наглядности
    clone = image.copy()
    cv2.rectangle(clone, (x, y), (x + winW, y + winH), (0, 255, 0), 2)
    cv2.imshow("Sliding Windows", clone)
    cv2.waitKey(1)
    time.sleep(0.025)

cv2.destroyAllWindows();
```

### Найди кота

Теперь попробуем найти кота на изображении с помощью реализованного поиска цвета выше. Одако вот так искать цвет для конкретного объекта в цветом пространстве RGB трудно. Рассмотрим пример для наглядности.

```python
# посмторим на кота

cat_image  = cv2.imread('img/cat.jpg')

while True:
    cv2.imshow('cat', cat_image)

    key = cv2.waitKey(1)
    if key == ord('q'):
        break

cv2.destroyAllWindows()
```

```python
cat_image  = cv2.imread('img/cat.jpg')

# значения цвета можно посмотреть в gimp или paint
pix = 256
low_red = (0.0 * pix, 0.11 * pix, 0.37 * pix)
high_red = (0.35 * pix, 0.4 * pix, 0.99 * pix)

cat_area = cv2.inRange(cat_image, low_red, high_red)

# посмотрим на результат
fig, m_axs = plt.subplots(1, 2, figsize = (12, 9))
ax1, ax2 = m_axs

ax1.set_title('Исходная картинка', fontsize=15)
cat_image = cv2.cvtColor(cat_image, cv2.COLOR_BGR2RGB)  # преобразуем цвет для plt
ax1.imshow(cat_image, cmap='gray')
ax1.axis('off')

ax2.set_title('Только кот', fontsize=15)
ax2.imshow(cat_area, cmap='gray')
ax2.axis('off');
```

**Кот в RGB $-$ не кот. Выбираем цветовое пространство.**

С данным изображением мы уже можем работать, но что-то в нем не то. Кот выделен не целиком, и есть много лишних точек. На самом деле, мы научимся даже из такого получать много полезной информации, но в данный момент мы сделаем одно очень важное и правильное улучшение. Как вы могли заметить $-$ мы работаем с изображением в пространстве **RGB**. У данного пространства, кроме не очевидной для человеческого восприятия записи цвета, есть еще один недостаток в нашем случае. Чтобы его понять, рассмотрим что из себя представляет **RGB**:

Пространство представляет из себя куб с длинной стороны $256$, где любой цвет задается координатами соответствующей точки на данном кубе. Данная концепция получила широкое распространение из-за того, что в случае формирования цвета на экране устройства, такого как телефон или монитор компьютера, изображение формируется за счет точек трех цветов $-$ синего, зеленого, и красного. Теперь перейдем к нашей функции ```cv2.inRange()```. Она принимает набор из двух цветов – точек на данном кубе. Что в таком случае мы будем считать за цвет, который удовлетворяет нашим критериям? Для ответа на данный вопрос нарисуем схематично тот же самый куб RGB:

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

Мы не первые,кто сталкивается с такой задачей и такой проблемой. И одно из существующих решений $-$ переход в другое цветовое пространство. Если поставить задачей, что во главе угла у нас фиксируется оттенок цвета или соотношение между RGB, то мы приходим к следующей схеме:

В данной модели мы переходим к следующим координатам $-$ **оттенок, насыщенность и значение**. Именно в данных понятиях люди обычно описывают цвета.

Под цветовым тоном (**Hue**) имеют в виду именно цвет (длину волны).

Насыщенность (**Saturation**) характеризует близость цвета к белому (розовый ближе к белому чем красный).

Значение (**Value**) описывается всех сложнее и в модели HS ее можно описать как общую яркость точки или цвета.

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

В OpenCV диапазоны разные. S, V находятся в $[0,255]$, а H – в $[0,180]$. Обычно H находится в диапазоне $[0,360]$ (полный круг), но для того, чтобы поместиться в байте ($256$ различных значений), его значение уменьшается вдвое.

В пространстве HSV проще отделить один цвет, так как вы можете просто установить правильный диапазон для H и просто позаботиться о том, чтобы S не был слишком маленьким (он будет почти белым), а V не слишком мал (это будет темно).

Например, если вам нужны почти синие цвета, вам нужно, чтобы H находилось вокруг значения $120$ (например, в $[110,130]$), а S, V не слишком мало (скажем, в $[100, 255]$).

Белый цвет не является оттенком (радуга не имеет белого цвета в нем), но представляет собой комбинацию цвета.

В HSV вам нужно взять весь диапазон H (H в $[0, 180]$), очень малые значения S (например, S в $[0, 25]$) и очень высокие значения V (например, V в $[230, 255]$). Это в основном соответствует верхней части центральной оси конуса.

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

В OpenCV данное цветовое пространство имеет обозначение **HSV**. Переведем наше изображение в него и снова найдем котейку:

```python
# меняем цветовое пространство
cat_image_hsv = cv2.cvtColor(cat_image, cv2.COLOR_RGB2HSV)

# осталось только подобрать нужные цвета
# воспользуйтесь функциями, которые реализовали выше
hsv_low = (0, 0, 0)
hsv_high = (0, 0, 0)

cat_area = cv2.inRange(cat_image_hsv, hsv_low, hsv_high)

## посмотрим на результат
fig, m_axs = plt.subplots(1, 2, figsize = (12, 9))
ax1, ax2 = m_axs

ax1.set_title('Исходная картинка', fontsize=15)
ax1.imshow(cat_image, cmap='gray')
ax2.set_title('Только кот', fontsize=15)
ax2.imshow(cat_area, cmap='gray');
```

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

# **Краткое ревью элементов из линейной алгебры**

## Связанные определения

**Квадратной** называется матрица, у которой число строк равно числу столбцов.

**Нулевой** ($Θ$) называется матрица, у которой все элементы равны 0.

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

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

**Единичной** ($E$) называется квадратная матрица, у которой на главной диагонали стоят единицы, а все остальные равны нулю.

**Симметричной** называется такая квадратная матрица, у которой для всех элементов выполняется условие $a_{i,j} = a_{j,i}$.

**Диагональной** называется такая матрица, у которой элементы не лежащие на главной диагонали равны нулю.

## Транспонирование матриц

Матрица $A^T$ называется **транспонированной** матрицей $A$, если для любого элемента $A^T_{i,j} = A_{j,i}$. Иными словами, отперация транспонирования -- это отражение матрицы относительно ее главной диагонали.

Для симметричной матрицы $S = S^T$.

## Сложение матриц
Складывать можно только матрицы одинакового размера.

Сложение матриц $A+B$ есть операция нахождения матрицы $C$, все элементы которой равны попарной сумме всех соответствующих элементов матриц $A$ и $B$, то есть каждый элемент матрицы $C$ равен: $$\displaystyle c_{i,j} = a_{i,j} + b_{i,j}$$

### Свойства сложения матриц:

* коммутативность: $A+B = B+A$
* ассоциативность: $(A+B)+C =A+(B+C)$;
* сложение с нулевой матрицей: $A + Θ = A$;
* существование противоположной матрицы: $A + (-A) = Θ$.

## Умножение матриц

Умножение матриц (обозначение:  $AB$, реже со знаком умножения  $A\times B$) — есть операция вычисления матрицы $C$, каждый элемент которой равен сумме произведений элементов в соответствующей строке первого множителя и столбце второго.

$c_{ij}=\sum _{k=1}^{n}a_{ik}b_{kj}$
Количество столбцов в матрице $A$ должно совпадать с количеством строк в матрице $B$, иными словами, матрица $A$ обязана быть согласованной с матрицей $B$. Если матрица $A$ имеет размерность $m\times n$, $B$ — $n \times k$, то размерность их произведения $A B = C$ есть $m \times k$.

### Свойства умножения матриц:

* ассоциативность: $(AB)C = A(BC)$;
* антикоммутативность (в общем случае): $AB$ $\neq$  $BA$;
* произведение коммутативно в случае умножения с единичной матрицей: $AE = EA$;
* дистрибутивность: $(A+B)C = AC + BC, \: A(B+C) = AB + AC$;
* ассоциативность и коммутативность относительно умножения на число: $(\lambda A)B = \lambda (AB) = A(\lambda B)$.

Подробное объяснение умножения матриц вместе с примерами можно найти [тут](http://mathprofi.ru/deistviya_s_matricami.html).

## Афинные преобразования

Под преобразованием понимается отображение множества на себя. Другими словами, преобразование $-$ это правило, в соответствии с которым каждому элементу множества ставится в соответствие элемент этого же множества.

Преобразование плоскости (пространства) называется аффинным, если существуют такие две аффинные системы координат, что координаты любой точки в первой системе координат совпадают с координатами ее образа во второй системе координат.

Аффинное преобразование можно рассматривать как последовательное применение (композицию) двух отображений:

Точке ставится в соответствие координаты относительно первой системы координат;
Полученным координатам ставится в соответствие точка относительно второй системы координат.
Пусть $f$ $-$ аффинное преобразование. Рассмотрим вектор ${\overrightarrow{AB}}$ в первой системе координат и ${\overrightarrow{f(A)f(B)}}$ во второй. Так как координаты вектора определяются как разность координат конца и начала, а координаты точек ${A}$ и ${f(A)}$, $B$ и ${f(B)}$ равны в соответствующих системах координат, то вектор ${\overrightarrow{f(A)f(B)}}$ имеет те же координаты относительно второй системы координат, что и вектор ${\overrightarrow{AB}}$ относительно первой.

### Аффинные преобразования плоскости

Преобразование плоскости задается двумя скалярными функциями двух переменных:

${\begin{cases}\hat{x}=f_{1}(x,y)\\ \hat{y}=f_{2}(x,y)\end{cases}}$ или аналогично через вектор-функцию ${\begin{pmatrix}f_1(x);\ f_2(x)\end{pmatrix}^T}$

#### Формула аффинного преобразования плоскости

Преобразование $A$ плоскости называется аффинным, если координаты $(\hat{x},\hat{y})$ образа $Y$ выражаются через координаты $(x,y)$ прообраза $X \ (Y=A(X))$ по формулам:

$$
{\begin{cases}\hat{x}=a_{11}x+a_{12}y-x_{0}\\ \hat{y}=a_{21}x+a_{22}y-y_{0}\end{cases}} {\Leftrightarrow\quad}
{{\begin{pmatrix}\hat{x}\\\hat{y}\end{pmatrix}}=A{\begin{pmatrix}x\\y\end{pmatrix}} - {\begin{pmatrix}x_{0}\\y_{0}\end{pmatrix}},}
$$

где матрица ${\begin{equation*}A={\begin{pmatrix}a_{11}\ a_{12}\\a_{21}\ a_{22}\end{pmatrix}}\end{equation*}}$
$-$ невырожденная матрица (матрица аффинного преобразования).

#### Свойства аффинных преобразований плоскости
__1.__ Аффинное преобразование взаимно однозначное, кроме того:

* преобразование, обратное к аффинному, является также аффинным;
* композиция аффинных преобразований является также аффинным преобразованием.
***
__2.__ При аффинном преобразовании векторы преобразуются следующим образом:

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

$\hat{S}=|det|\cdot S$, где

$S$ $-$ площадь параллелограмма,

$\hat{S}$ $-$ площадь образа этого параллелограмма.

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

# **Геометрические преобразования изображений**

Теперь мы рассмотрим каким образом аффинные преобразования помогут нам изменять изображения с помощью OpenCV.

## Однородные координаты

А что же делать? Колдовать! Представьте теперь, что я допишу руками одну строчку и один столбец к нашей матрице преобразования и добавлю третью координату, которая равна единице у вектора, который мы преобразовываем:

$$
{\begin{pmatrix} a && b && e\\ c && d && f\\ 0 && 0 && 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} = \begin{pmatrix} ax + by + e \\ cx + dy + f \\ 1 \end{pmatrix}}
$$

При умножении этой 3x3 матрицы и нашего вектора, дополненного единицей, мы снова получили вектор с единицей в третьей компоненте, а остальные две имеют ровно тот вид, который мы хотели! Колдунство.

## Сдвиг изображения
**Сдвиг** $-$ это смещение местоположения объекта. Если вам известно смещение в направлении $(x,y)$, пусть оно будет $(tx,ty)$, вы можете создать матрицу преобразования $M$ следующим образом:

$$
\ M = \begin{pmatrix}
1 & \ 0 & \ t_x \\
0 & \ 1 & \ t_y \\
\end{pmatrix}
$$

Затем вы можете взять массив Numpy типа np.float32 и передать его в функцию **cv2.warpAffine()**. Ниже приведен пример сдвига:

```python
# сдвиг изображения

image  = cv2.imread('img/RGB_cube.png')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

## посмотрим на размер изображения
print('Раземер исходного изображения:', image.shape)
rows, cols, _ = image.shape

## матрица афинного преобразования
M1 = np.float32([[1, 0, 200], [0, 1, 0]])
M2 = np.float32([[1, 0, 0], [0, 1, 200]])
M3 = np.float32([[1, 0, 200], [0, 1, 200]])

## визуализация
fig, m_axs = plt.subplots(1, 3, figsize=(20,8))
ax1, ax2, ax3 = m_axs

dst1 = cv2.warpAffine(image.copy(), M1, (cols, rows))
ax1.imshow(dst1)
ax1.grid()
ax1.set_title('M1 преоразование', fontsize=15)

dst2 = cv2.warpAffine(image.copy(), M2, (cols, rows))
ax2.imshow(dst2)
ax2.grid()
ax2.set_title('M2 преоразование', fontsize=15)

dst3 = cv2.warpAffine(image.copy(), M3, (cols, rows))
ax3.imshow(dst3)
ax3.grid()
ax3.set_title('M3 преоразование', fontsize=15);
```

## Поворот изображения
Поворот изображения на угол $\theta$ достигается путем преобразования матрицы вида:

$$
\ M = \begin{pmatrix}
\cos(\theta) & \ -\sin(\theta) \\
\sin(\theta) & \ \cos(\theta) \\
\end{pmatrix}
$$

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

$$
\ M = \begin{pmatrix}
\alpha & \ -\beta & \ (1-\alpha)\cdot center.x - \beta \cdot center.y \\
\beta & \ \alpha & \ \beta \cdot center/x + (1-\alpha) \cdot center.y \\
\end{pmatrix}
$$

где:

$$
\alpha = scale \cdot \cos(\theta) \\
\beta = scale \cdot \sin(\theta) \\
$$

Результатом поворота является:

$$
\begin{pmatrix} \ x' \\ \ y' \\ \end{pmatrix} =
\begin{pmatrix}
\alpha & \ -\beta & \ (1-\alpha)\cdot center.x - \beta \cdot center.y \\
\beta & \ \alpha & \ \beta \cdot center/x + (1-\alpha) \cdot center.y \\
\end{pmatrix} \cdot
\begin{pmatrix} \ x \\ \ y \\ 1 \\ \end{pmatrix}
$$

где:

$$
\ (x', y') - \ new \ coordinates \\
\ (x, y) - \ old \ coordinates \\
$$
Чтобы найти эту матрицу преобразования, OpenCV предоставляет функцию **cv2.getRotationMatrix2D()**. Ниже приведены несколько примеров.

```python
print('Раземер исходного изображения:', image.shape)
rows, cols, _ = image.shape

M1 = cv2.getRotationMatrix2D((cols/2, rows/2), 25, scale=1.0)
M2 = cv2.getRotationMatrix2D((300, 700), -15, scale=0.75)
M3 = cv2.getRotationMatrix2D((300, 100), 45, scale=2.0)

## визуализация
fig, m_axs = plt.subplots(1, 3, figsize=(20, 8))
ax1, ax2, ax3 = m_axs

dst1 = cv2.warpAffine(image.copy(), M1, (cols, rows))
ax1.imshow(dst1)
ax1.grid()
ax1.set_title('M1 преоразование', fontsize=15)

dst2 = cv2.warpAffine(image.copy(), M2, (cols, rows))
ax2.imshow(dst2)
ax2.grid()
ax2.set_title('M2 преоразование', fontsize=15)

dst3 = cv2.warpAffine(image.copy(), M3, (cols, rows))
ax3.imshow(dst3)
ax3.grid()
ax3.set_title('M3 преоразование', fontsize=15);
```

### ***Контрольные вопросы:***
Как сделать поворт изображения без обрезаний краев?

## Трансформация точек
При аффинном преобразовании все параллельные линии в исходном изображении все еще будут параллельны в выходном изображении. Чтобы найти матрицу преобразования, нам нужны три точки из входного изображения и их соответствующие местоположения в выходном изображении. Затем **cv2.getAffineTransform()** создаст матрицу $2\times 3$, которая должна быть передана в **cv2.warpAffine()**.

```python
print('Раземер исходного изображения:', image.shape)
h, w, _ = image.shape

pts1 = np.float32([[50, 50], [400, 50], [50, 200]])
pts2 = np.float32([[100, 100], [200, 20], [100, 250]])

M = cv2.getAffineTransform(pts1, pts2)
print(f'Матрица перехода: \n{M}')

dst = cv2.warpAffine(image, M, (w, h))

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

ax1.set_title('Исходное изображение', fontsize=15)
ax1.imshow(image)
ax1.grid()
ax1.scatter(*pts1.T, color='g', s=100)

ax2.set_title('M преобразование', fontsize=15)
ax2.imshow(dst)
ax2.grid()
ax2.scatter(*pts2.T, color='g', s=100);
```

## Перспектива

На самом деле, идея очень простая: параллельный перенос не является линейной операцией в двумерном пространстве.
Поэтому мы погружаем наше двумерное пространство в трёхмерное (добавив единицу в третью компоненту). Это означает, что наше двумерное пространство это плоскость z=1 внутри трёхмерного. Затем мы делаем линейное преобразование в трёхмерном пространстве и проецируем всё трехмерное пространство обратно на нашу физическую плоскость. Параллельный перенос от этого не стал линеен, но пайплайн всё же прост.

Как именно мы проецируем трёхмерное пространство обратно в нашу плоскость? Очень просто:

$$
{\begin{pmatrix} x \\ y \\ z\end{pmatrix} \rightarrow \begin{pmatrix} x/z \\ y/z \end{pmatrix}}
$$

Итак, если мы хотим построить центральную перспективу, находящейся на оси z на расстоянии c от начала координат, то сначала мы погружаем трёхмерные точки в четырёхмерное пространство, добавив 1. Затем умножаем на следующую матрицу и проецируем результат обратно в 3D:

$$
{\begin{pmatrix} x \\ y \\ z\end{pmatrix} \rightarrow \begin{pmatrix} x \\ y \\ z \\ 1\end{pmatrix}}
$$

$$
{\begin{pmatrix} 1 && 0 && 0 && 0 \\ 0 && 1 && 0 && 0 \\ 0 && 0 && 1 && 0 \\ 0 && 0 && -1/c && 1\end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1\end{pmatrix} = \begin{pmatrix} x \\ y \\ z \\ 1 - z/c\end{pmatrix}}
$$

$$
{\begin{pmatrix} x \\ y \\ z \\ 1 - z/c\end{pmatrix} \rightarrow \begin{pmatrix} \frac{x}{1-z/c} \\ \frac{y}{1-z/c} \\ \frac{z}{1-z/c}\end{pmatrix}}
$$

Для преобразования перспективы вам понадобится матрица преобразования $3\times3$. Прямые линии останутся прямыми даже после трансформации. Чтобы найти эту матрицу преобразования, вам нужно $4$ точки на входном изображении и соответствующие точки на выходном изображении. Среди этих $4$ точек $3$ из них не должны быть коллинеарными. Тогда матрица преобразования может быть найдена функцией **cv2.getPerspectiveTransform()**. Затем примените **cv2.warpPerspective()** с этой матрицей преобразования $3\times3$.

```python
print('Раземер исходного изображения:', image.shape)
rows, cols, _ = image.shape

pts1 = np.float32([[200, 100], [450, 100],[200, 400], [450, 400]])
pts2 = np.float32([[0, 0],[cols/2, 0],[0, rows], [cols, rows]])
pts3 = np.float32([[0, 0],[cols, 0],[0, rows], [cols, rows/2]])

M1 = cv2.getPerspectiveTransform(pts1, pts2)
M2 = cv2.getPerspectiveTransform(pts1, pts3)

fig, m_axs = plt.subplots(1, 3, figsize=(20,8))
ax1, ax2, ax3 = m_axs

ax1.set_title('Исходное изображение', fontsize=15)
ax1.imshow(image)
ax1.grid()
ax1.scatter(*pts1.T, color='g', s=100)

ax2.set_title('M1 преобразование', fontsize=15)
dst2 = cv2.warpPerspective(image, M1, (cols, rows))
ax2.imshow(dst2)
ax2.grid()

ax3.set_title('M2 преобразование', fontsize=15)
dst3 = cv2.warpPerspective(image, M2, (cols, rows))
ax3.imshow(dst3)
ax3.grid();
```