Trusted Notebook" width="500 px" align="left">

## _*Comparing Classical and Quantum Finite Automata (QFA)*_

Finite Automaton has been a mathematical model for computation since its invention in the 1940s. The purpose of a Finite State Machine is to recognize patterns within an input taken from some character set and accept or reject the input based on whether the pattern defined by the machine occurs in the input. The machine requires a list of states, the initial state, and the conditions for each transition from state to state. Such classical examples are vending machines, coin-operated turnstiles, elevators, traffic lights, etc.

In the classical algorithm, the sequence begins in the start state, and will only make a transition if the next character in the input string matches the label on the transition from the current to the next state. The machine will continue making transitions on each input character until no move is possible. The string will be accepted if its final state is in the accept state and will be rejected if its final state is anywhere else.

As for Quantum Finite Automata (QFA), the machine works by accepting a finite-length string of letters from a finite alphabet and utilizing quantum properties such as superposition to assign the string a probability of being in either the accept or reject state.

***
### Contributors
Kaitlin Gili, Rudy Raymond

## Prime Divisibility Algorithm

Let's say that we have a string with $ a^i $ letters and we want to know whether the string is in the language $ L $ where $ L $ = {$ a^i $ | $ i $ is divisble by $ p $} and $ p $ is a prime number. If $ i $ is divisible by $ p $, we want to accept the string into the language, and if not, we want to reject it.
$|0\rangle $ and $ |1\rangle $ serve as our accept and reject states.

Classically, this algorithm requires a minimum of $ log(p) $ bits to store the information, whereas the quantum algorithm only requires $ log(log(p)) $ qubits. For example, using the highest known prime integer, the classical algorithm requires **a minimum of 77,232,917 bits**, whereas the quantum algorithm **only requires 27 qubits**.

## Introduction

The algorithm in this notebook follows that in [Ambainis et al. 1998](https://arxiv.org/pdf/quant-ph/9802062.pdf). We assume that we are given a string and a prime integer. If the user does not input a prime number, the output will be a ValueError. First, we demonstrate a simpler version of the quantum algorithm that uses $ log(p) $ qubits to store the information. Then, we can use this to more easily understand the quantum algorithm that requires only $ log(log(p)) $ qubits.

## The Algorithm for Log(p) Qubits

The algorithm is quite simple as follows.
1. Prepare quantum and classical registers for $ log(p) $ qubits initialized to zero.
$$ |0\ldots 0\rangle $$
2. Prepare $ log(p) $ random numbers k in the range {$ 1 $... $ p-1 $}. These numbers will be used to decrease the probability of a string getting accepted when $ i $ does not divide $ p $.
3. Perform a number of $ i $ Y-Rotations on each qubit, where $ \theta $ is initially zero and $ \Phi $ is the angle of rotation for each unitary. $$ \Phi = \frac{2 \pi k}{p} $$
4. In the final state:
$$ \cos \theta |0\rangle + \sin \theta |1\rangle $$
$$ \theta = \frac{2 \pi k} p {i} $$
5. Measure each of the qubits in the classical register. If $ i $ divides $ p $, $ \cos \theta $ will be one for every qubit and the state will collapse to $ |0\rangle $ to demonstrate an accept state with a probability of one. Otherwise, the output will consist of a small probability of accepting the string into the language and a higher probability of rejecting the string.

## The Circuit

We now implement the QFA Prime Divisibility algorithm with QISKit by first preparing the environment.

```python
# useful additional packages
import random
import math
from sympy.ntheory import isprime

# importing QISKit
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import Aer, IBMQ, execute

from qiskit.wrapper.jupyter import *
from qiskit.backends.ibmq import least_busy

from qiskit.tools.visualization import matplotlib_circuit_drawer as circuit_drawer
from qiskit.tools.visualization import plot_histogram, qx_color_scheme
```

```python
IBMQ.load_accounts()
sim_backend = Aer.get_backend('qasm_simulator')
device_backend = least_busy(IBMQ.backends(operational=True, simulator=False))
device_coupling = device_backend.configuration()['coupling_map']
```

We then use QISKit to program the algorithm.

```python
#Function that takes in a prime number and a string of letters and returns a quantum circuit
def qfa_algorithm(string, prime):
    if isprime(prime) == False:
        raise ValueError("This number is not a prime") #Raises a ValueError if the input prime number is not prime
    else:
        n = math.ceil((math.log(prime))) #Rounds up to the next integer of the log(prime)
        qr = QuantumRegister(n) #Creates a quantum register of length log(prime) for log(prime) qubits
        cr = ClassicalRegister(n) #Creates a classical register for measurement
        qfaCircuit = QuantumCircuit(qr, cr) #Defining the circuit to take in the values of qr and cr
        for x in range(n): #For each qubit, we want to apply a series of unitary operations with a random int
            random_value = random.randint(1,prime - 1) #Generates the random int for each qubit from {1, prime -1}
            for letter in string: #For each letter in the string, we want to apply the same unitary operation to each qubit
                qfaCircuit.ry((2*math.pi*random_value) / prime, qr[x]) #Applies the Y-Rotation to each qubit
            qfaCircuit.measure(qr[x], cr[x]) #Measures each qubit
        return qfaCircuit #Returns the created quantum circuit
```

The qfa_algorithm function returns the Quantum Circuit qfaCircuit.

## Experiment with Simulators

We can run the above circuit on the simulator.

```python
#A function that returns a string saying if the string is accepted into the language or rejected
def accept(parameter):
    states = list(result.get_counts(parameter))
    for s in states:
        for integer in s:
            if integer == "1":
                return "Reject: the string is not accepted into the language"
    return "Accept: the string is accepted into the language"
```

Insert your own parameters and try even larger prime numbers.

```python
range_lower = 0
range_higher = 36
prime_number = 11
```

```python
for length in range(range_lower,range_higher):
    params = qfa_algorithm("a"* length, prime_number)
    job = execute(params, sim_backend, shots=1000)
    result = job.result()
    print(accept(params), "\n", "Length:",length," " ,result.get_counts(params))
```

    Accept: the string is accepted into the language
     Length: 0   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 1   {'001': 4, '010': 2, '011': 14, '100': 32, '101': 138, '110': 124, '111': 686}
    Reject: the string is not accepted into the language
     Length: 2   {'000': 6, '001': 14, '010': 125, '011': 571, '100': 1, '101': 6, '110': 40, '111': 237}
    Reject: the string is not accepted into the language
     Length: 3   {'000': 8, '001': 453, '010': 7, '011': 194, '100': 1, '101': 229, '110': 2, '111': 106}
    Reject: the string is not accepted into the language
     Length: 4   {'001': 34, '010': 1, '011': 140, '100': 2, '101': 138, '110': 17, '111': 668}
    Reject: the string is not accepted into the language
     Length: 5   {'000': 202, '001': 92, '010': 271, '011': 143, '100': 85, '101': 35, '110': 126, '111': 46}
    Reject: the string is not accepted into the language
     Length: 6   {'000': 55, '001': 22, '010': 264, '011': 112, '100': 56, '101': 21, '110': 326, '111': 144}
    Reject: the string is not accepted into the language
     Length: 7   {'000': 1, '001': 7, '010': 2, '011': 11, '100': 79, '101': 370, '110': 103, '111': 427}
    Reject: the string is not accepted into the language
     Length: 8   {'000': 227, '001': 85, '010': 93, '011': 38, '100': 283, '101': 120, '110': 109, '111': 45}
    Reject: the string is not accepted into the language
     Length: 9   {'000': 107, '001': 38, '010': 546, '011': 214, '100': 12, '101': 6, '110': 51, '111': 26}
    Reject: the string is not accepted into the language
     Length: 10   {'000': 267, '001': 115, '010': 372, '011': 172, '100': 23, '101': 9, '110': 29, '111': 13}
    Accept: the string is accepted into the language
     Length: 11   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 12   {'000': 3, '001': 7, '011': 7, '100': 109, '101': 582, '110': 55, '111': 237}
    Reject: the string is not accepted into the language
     Length: 13   {'000': 56, '001': 259, '010': 66, '011': 325, '100': 14, '101': 108, '110': 29, '111': 143}
    Reject: the string is not accepted into the language
     Length: 14   {'000': 3, '001': 1, '010': 319, '011': 122, '100': 7, '101': 2, '110': 385, '111': 161}
    Reject: the string is not accepted into the language
     Length: 15   {'000': 1, '001': 1, '010': 170, '011': 7, '100': 8, '101': 1, '110': 752, '111': 60}
    Reject: the string is not accepted into the language
     Length: 16   {'000': 22, '001': 93, '010': 10, '011': 34, '100': 113, '101': 504, '110': 38, '111': 186}
    Reject: the string is not accepted into the language
     Length: 17   {'000': 22, '001': 85, '010': 7, '011': 32, '100': 115, '101': 474, '110': 45, '111': 220}
    Reject: the string is not accepted into the language
     Length: 18   {'000': 118, '001': 172, '010': 55, '011': 82, '100': 181, '101': 214, '110': 73, '111': 105}
    Reject: the string is not accepted into the language
     Length: 19   {'000': 255, '001': 393, '010': 108, '011': 154, '100': 25, '101': 42, '110': 8, '111': 15}
    Reject: the string is not accepted into the language
     Length: 20   {'000': 2, '001': 104, '010': 2, '011': 43, '100': 13, '101': 579, '110': 7, '111': 250}
    Reject: the string is not accepted into the language
     Length: 21   {'000': 286, '001': 24, '010': 357, '011': 35, '100': 125, '101': 9, '110': 150, '111': 14}
    Accept: the string is accepted into the language
     Length: 22   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 23   {'000': 57, '001': 10, '010': 334, '011': 34, '100': 70, '101': 9, '110': 449, '111': 37}
    Reject: the string is not accepted into the language
     Length: 24   {'000': 29, '001': 130, '010': 2, '011': 7, '100': 149, '101': 621, '110': 14, '111': 48}
    Reject: the string is not accepted into the language
     Length: 25   {'000': 131, '001': 48, '010': 190, '011': 69, '100': 169, '101': 77, '110': 237, '111': 79}
    Reject: the string is not accepted into the language
     Length: 26   {'000': 3, '001': 303, '010': 1, '011': 126, '100': 8, '101': 400, '110': 2, '111': 157}
    Reject: the string is not accepted into the language
     Length: 27   {'000': 14, '001': 487, '010': 7, '011': 196, '100': 2, '101': 211, '110': 2, '111': 81}
    Reject: the string is not accepted into the language
     Length: 28   {'000': 1, '001': 4, '010': 10, '011': 5, '100': 65, '101': 109, '110': 354, '111': 452}
    Reject: the string is not accepted into the language
     Length: 29   {'000': 189, '001': 299, '010': 77, '011': 106, '100': 100, '101': 135, '110': 45, '111': 49}
    Reject: the string is not accepted into the language
     Length: 30   {'000': 10, '001': 55, '010': 72, '011': 276, '100': 16, '101': 90, '110': 67, '111': 414}
    Reject: the string is not accepted into the language
     Length: 31   {'000': 4, '010': 170, '011': 13, '100': 19, '110': 723, '111': 71}
    Reject: the string is not accepted into the language
     Length: 32   {'000': 102, '001': 5, '010': 52, '011': 6, '100': 537, '101': 47, '110': 229, '111': 22}
    Accept: the string is accepted into the language
     Length: 33   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 34   {'001': 23, '010': 14, '011': 876, '101': 1, '111': 86}
    Reject: the string is not accepted into the language
     Length: 35   {'000': 9, '001': 3, '010': 7, '011': 2, '100': 495, '101': 184, '110': 204, '111': 96}

### Drawing the circuit of the QFA

Below is the snapshop of the QFA for reading the bitstring of length $3$. It can be seen that there are independent QFAs each of which performs $Y$ rotation for $3$ times.

```python
circuit_drawer(qfa_algorithm("a"* 3, prime_number), style=qx_color_scheme())
```

## The Algorithm for Log(Log(p)) Qubits

The algorithm is quite simple as follows.
1. Prepare a quantum register for $ log(log(p)) + 1 $ qubits initialized to zero. The $ log(log(p))$ qubits will act as your control bits and the 1 extra will act as your target bit. Also prepare a classical register for 1 bit to measure the target.
$$ |0\ldots 0\rangle |0\rangle  $$
2. Hadamard the control bits to put them in a superposition so that we can perform multiple QFA's at the same time.
3. For each of $s $ states in the superposition, we can perform an individual QFA with the control qubits acting as the random integer $ k $ from the previous algorithm. Thus, we need $ n $ values from $ 1... log(p)$ for $ k $. For each letter $ i $ in the string, we perform a controlled y-rotation on the target qubit, where $ \theta $ is initially zero and $ \Phi $ is the angle of rotation for each unitary. $$ \Phi = \frac{2 \pi k_{s}}{p} $$
4. The target qubit in the final state:
$$ \cos \theta |0\rangle + \sin \theta |1\rangle $$
$$ \theta = \sum_{s=0}^n \frac{2 \pi k_{s}} p {i} $$
5. Measure the target qubit in the classical register. If $ i $ divides $ p $, $ \cos \theta $ will be one for every QFA and the state of the target will collapse to $ |0\rangle $ to demonstrate an accept state with a probability of one. Otherwise, the output will consist of a small probability of accepting the string into the language and a higher probability of rejecting the string.

## The Circuit

We then use QISKit to program the algorithm.

```python
#Function that takes in a prime number and a string of letters and returns a quantum circuit
def qfa_controlled_algorithm(string, prime):
    if isprime(prime) == False:
        raise ValueError("This number is not a prime") #Raises a ValueError if the input prime number is not prime
    else:
        n = math.ceil((math.log(math.log(prime,2),2))) #Represents log(log(p)) control qubits
        states = 2 ** (n) #Number of states that the qubits can represent/Number of QFA's to be performed
        qr = QuantumRegister(n+1) #Creates a quantum register of log(log(prime)) control qubits + 1 target qubit
        cr = ClassicalRegister(1) #Creates a classical register of log(log(prime)) control qubits + 1 target qubit
        control_qfaCircuit = QuantumCircuit(qr, cr) #Defining the circuit to take in the values of qr and cr
        for q in range(n): #We want to take each control qubit and put them in a superposition by applying a Hadamard Gate
            control_qfaCircuit.h(qr[q])
        for letter in string: #For each letter in the string, we want to apply a series of Controlled Y-rotations
            for q in range(n):
                control_qfaCircuit.cu3(2*math.pi*(2**q)/prime, 0, 0, qr[q], qr[n]) #Controlled Y on Target qubit
        control_qfaCircuit.measure(qr[n], cr[0]) #Measure the target qubit
        return control_qfaCircuit #Returns the created quantum circuit
```

The qfa_algorithm function returns the Quantum Circuit control_qfaCircuit.

## Experiment with Simulators

We can run the above circuit on the simulator.

```python
for length in range(range_lower,range_higher):
    params = qfa_controlled_algorithm("a"* length, prime_number)
    job = execute(params, sim_backend, shots=1000)
    result = job.result()
    print(accept(params), "\n", "Length:",length," " ,result.get_counts(params))
```

    Accept: the string is accepted into the language
     Length: 0   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 1   {'0': 782, '1': 218}
    Reject: the string is not accepted into the language
     Length: 2   {'0': 488, '1': 512}
    Reject: the string is not accepted into the language
     Length: 3   {'0': 546, '1': 454}
    Reject: the string is not accepted into the language
     Length: 4   {'0': 643, '1': 357}
    Reject: the string is not accepted into the language
     Length: 5   {'0': 554, '1': 446}
    Reject: the string is not accepted into the language
     Length: 6   {'0': 527, '1': 473}
    Reject: the string is not accepted into the language
     Length: 7   {'0': 661, '1': 339}
    Reject: the string is not accepted into the language
     Length: 8   {'0': 539, '1': 461}
    Reject: the string is not accepted into the language
     Length: 9   {'0': 475, '1': 525}
    Reject: the string is not accepted into the language
     Length: 10   {'0': 758, '1': 242}
    Accept: the string is accepted into the language
     Length: 11   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 12   {'0': 747, '1': 253}
    Reject: the string is not accepted into the language
     Length: 13   {'0': 451, '1': 549}
    Reject: the string is not accepted into the language
     Length: 14   {'0': 549, '1': 451}
    Reject: the string is not accepted into the language
     Length: 15   {'0': 629, '1': 371}
    Reject: the string is not accepted into the language
     Length: 16   {'0': 554, '1': 446}
    Reject: the string is not accepted into the language
     Length: 17   {'0': 535, '1': 465}
    Reject: the string is not accepted into the language
     Length: 18   {'0': 688, '1': 312}
    Reject: the string is not accepted into the language
     Length: 19   {'0': 531, '1': 469}
    Reject: the string is not accepted into the language
     Length: 20   {'0': 473, '1': 527}
    Reject: the string is not accepted into the language
     Length: 21   {'0': 782, '1': 218}
    Accept: the string is accepted into the language
     Length: 22   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 23   {'0': 765, '1': 235}
    Reject: the string is not accepted into the language
     Length: 24   {'0': 467, '1': 533}
    Reject: the string is not accepted into the language
     Length: 25   {'0': 519, '1': 481}
    Reject: the string is not accepted into the language
     Length: 26   {'0': 632, '1': 368}
    Reject: the string is not accepted into the language
     Length: 27   {'0': 548, '1': 452}
    Reject: the string is not accepted into the language
     Length: 28   {'0': 528, '1': 472}
    Reject: the string is not accepted into the language
     Length: 29   {'0': 629, '1': 371}
    Reject: the string is not accepted into the language
     Length: 30   {'0': 544, '1': 456}
    Reject: the string is not accepted into the language
     Length: 31   {'0': 485, '1': 515}
    Reject: the string is not accepted into the language
     Length: 32   {'0': 765, '1': 235}
    Accept: the string is accepted into the language
     Length: 33   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 34   {'0': 784, '1': 216}
    Reject: the string is not accepted into the language
     Length: 35   {'0': 463, '1': 537}

### Drawing the circuit of the QFA

Below is the snapshot of the QFA for reading the bitstring of length $3$. It can be seen that there is a superposition of QFAs instead of independent QFAs.

```python
circuit_drawer(qfa_controlled_algorithm("a"* 3, prime_number), style=qx_color_scheme())
```

## Experimenting with Real Devices

Real-device backends have errors and if the above QFAs are executed on the noisy backends, errors in rejecting strings that should have been accepted can happen. Let us see how well the real-device backends can realize the QFAs.

Let us look an example when the QFA should reject the bitstring because the length of the bitstring is not divisible by the prime number.

```python
prime_number = 3
length = 2          # set the length so that it is not divisible by the prime_number
print("The length of a is", length, " while the prime number is", prime_number)
qfa1 = qfa_controlled_algorithm("a"* length, prime_number)
```

    The length of a is 2  while the prime number is 3

```python
%%qiskit_job_status
HTMLProgressBar()
job = execute(qfa1, backend=device_backend, coupling_map=device_coupling, shots=100)
```

    VBox(children=(HTML(value="
Job Status : job is being initialized
"),))

```python
result = job.result()
plot_histogram(result.get_counts())
```

In the above, we can see that the probability of observing "1" is quite significant. Let us see how the circuit looks like.

```python
circuit_drawer(qfa1, style=qx_color_scheme())
```

Now, let us see what happens when the QFAs should accept the input string.

```python
print_number = length = 3 # set the length so that it is divisible by the prime_number
print("The length of a is", length, " while the prime number is", prime_number)
qfa2 = qfa_controlled_algorithm("a"* length, prime_number)
```

    The length of a is 3  while the prime number is 3

```python
%%qiskit_job_status
HTMLProgressBar()
job = execute(qfa2, backend=device_backend, coupling_map=device_coupling, shots=100)
```

    VBox(children=(HTML(value="
Job Status : job is being initialized
"),))

```python
result = job.result()
plot_histogram(result.get_counts())
```

The error of rejecting the bitstring is equal to the probability of observing "1" which can be checked from the above histogram. We can see that the noise of real-device backends prevents us to have a correct answer. It is left as future work on how to mitigate errors of the backends in the QFA models.

```python
circuit_drawer(qfa2, style=qx_color_scheme())
```


# 核主成分分析(Kernel Principle Component Analysis, KPCA)

## 符号定义

|符号|含义|
|:--:|:--:|
|$\pmb{x}$|样本点|
|$X$|样本点构成的矩阵|
|$\pmb{y}$|降维后样本点|
|$Y$|降维后样本点构成的矩阵|
|$k$|降维前样本点维度|
|$d$|升维后样本点维度|
|$m$|降维后样本点维度|
|$N$|样本总数|
|$\phi$|映射函数|
|$\overset{\thicksim}\phi$|中心化后的映射函数|
|$Cov()$|协方差矩阵|
|$\pmb{p}$|特征向量|
|$\pmb{\lambda}$|特征向量|

## 概念

KPCA在PCA的基础上通过引入核函数实现非线性降维。

传统的PCA仅能实现线性降维，对于本身线性不可分的数据，通过PCA降维后往往得不到有价值的结果。另一方面，对于线性不可分的数据，映射到更高维度后很可能就是线性可分的，然后进行PCA降维很可能得到有价值的结果。

因此对于一个线性不可分的数据集，通过映射到高维从而实现线性可分是可能的。显然，这里涉及到一个非常关键的问题，那就是**确定映射函数**。显然，不同的映射方法适用于不同的数据集，同时，同一个特定的映射方法不可能适用于所有的数据集。这一问题为寻找通用的映射方法带来了困难。因此若能不显式的定义映射函数，并且依然能够实现后续的降维是再好不过了。KPCA正是这样一种依靠核函数和核技巧，在不显式定义映射函数的基础上实现非线性降维的算法。

## 推导

* **低维空间的数据映射到高维空间**

对于特定的样本点$\pmb{x} \in \mathcal{R}^k$，假设存在映射函数$\phi$，使得$\phi(\pmb{x}) \in \mathcal{R}^d$。即

$$
\begin{equation}
    \phi(\pmb{x}) : \mathcal{R}^k \rightarrow \mathcal{R}^d, \ d \gg k
\end{equation}
$$

若将所有样本点均映射到高维空间则可以得到对应的映射后样本点构成的矩阵$\phi(X) \in \mathcal{R}^{d\times N}$

$$
\begin{equation}
    \phi(X) = [\phi(\pmb{x_1}), \phi(\pmb{x_2}), \cdots, \phi(\pmb{x_N})]
\end{equation}
$$

* **高维空间的降维**

由PCA的推导可知，对于降维到$m$维的问题，在数据已经经过中心化的前提下，PCA实际上就是得到协方差矩阵前$m$个最大特征值对应的特征向量构成的子空间，因此有

$$
\begin{equation}
    \begin{split}
    Cov(\phi(X))
    &= \frac{1}{N}\phi(X)\phi(X)^T \\
    &= \frac{1}{N}\sum_{i=1}^N\phi(\pmb{x_i})\phi(\pmb{x_i})^T
    \end{split}
\end{equation}
$$

接下来就是上述协方差矩阵的特征值、特征向量求解问题。

$$
\begin{equation}
    \begin{split}
    Cov(\phi(X))\pmb{p} &= \lambda\pmb{p} \\
    \Rightarrow \frac{1}{N}\phi(X)\phi(X)^T\pmb{p} &= \lambda\pmb{p} \\
    \Rightarrow \phi(X)\phi(X)^T\pmb{p} &= N\lambda\pmb{p}
    \end{split}
\end{equation}
$$

考虑到实际应用中主要关心的是特征向量，为了简化起见，这里舍去$N$（令$\lambda=N\lambda$），得到下式
$$
\begin{equation}
    \begin{split}
    \phi(X)\phi(X)^T\pmb{p} &= \lambda\pmb{p} \\
    \Rightarrow \sum_{i=1}^N\phi(\pmb{x_i})\phi(\pmb{x_i})^T\pmb{p} &= \lambda\pmb{p}
    \end{split}
\end{equation}
$$

由于并没有显式的定义映射函数$\phi$，因此上式无法直接求解。上式中映射函数以$\phi(X)\phi(X)^T$的形式出现，因此可以尝试将$\phi(X)\phi(X)^T$看作整体，若能计算得到这个整体也能够解出相应的特征向量。

假设特征值$\lambda \neq 0$，有
$$
\begin{equation}
    \pmb{p} = \frac{1}{\lambda}\sum_{i=1}^N\phi(\pmb{x_i})\phi(\pmb{x_i})^T\pmb{p}
\end{equation}
$$

上式中$\phi(\pmb{x_i})^T\pmb{p}$为常数，因此特征向量$\pmb{p}$实际上可以视为$\phi(\pmb{x_i}), i=1, 2, \cdots, N$的线性组合，假设线性组合的权重为$\alpha_i, i=1, 2, \cdots, N$，则有

$$
\begin{equation}
    \pmb{p} = \sum_{i=1}^N\alpha_i\phi(\pmb{x_i}) = \phi(X)\pmb{\alpha}, \pmb{\alpha}= [\alpha_1, \alpha_2, \cdots, \alpha_N]^T
\end{equation}
$$

对于降维到$m$维的问题，实际上就是求解前m个最大特征值对应的特征向量，对于第$j$个特征向量，样本点$\pmb{x_i}$投影到该方向上的坐标为:

$$
\begin{equation}
    \begin{split}
    y_{ij}
    &= \phi(\pmb{x_i})^T\pmb{p_j} \\
    &= \phi(\pmb{x_i})^T\phi(X)\pmb{\alpha_j}
    \end{split}
\end{equation}
$$

而所有样本点投影到该方向上的坐标为
$$
\begin{equation}
    \begin{split}
    \pmb{Y_j}
    &= \phi(X)^T\pmb{p_j} \\
    &= \phi(X)^T\phi(X)\pmb{\alpha_j}
    \end{split}
\end{equation}
$$

显然若能求得$\phi(X)^T\phi(X)$和$\pmb{\alpha_j}$就能求得降维后的结果。

将式-7回代到式-5可以得到
$$
\begin{equation}
    \begin{split}
    \phi(X)\phi(X)^T\pmb{p} &= \lambda\pmb{p} \\
    \Rightarrow \phi(X)\phi(X)^T\phi(X)\pmb{\alpha} &= \lambda\phi(X)\pmb{\alpha} \\
    \Rightarrow \phi(X)^T\phi(X)\phi(X)^T\phi(X)\pmb{\alpha} &= \lambda\phi(X)^T\phi(X)\pmb{\alpha}
    \end{split}
\end{equation}
$$

令$K=\phi(X)^T\phi(X)$，有
$$
\begin{equation}
    K \cdot K\pmb{\alpha} = \lambda K\pmb{\alpha}
\end{equation}
$$
上式实际上就是求解矩阵$K$的特征值问题。值得注意的是矩阵$K$中每一项是两个向量的内积，即$K_{ij}=\phi(\pmb{x_i}^T)\phi(\pmb{x_j})$，这与核函数不谋而合，虽然无法在没有显式定义映射函数的情况下求得映射到高维后的向量，但是能够在低维找到一个函数，使得这个函数的解恰好就是映射到高维空间后两个向量的内积。

将矩阵$K$带入式-9，得到
$$
\begin{equation}
    \begin{split}
    \pmb{Y_j}
    &= \phi(X)^T\phi(X)\pmb{\alpha_j} \\
    &= K\pmb{\alpha_j}
    \end{split}
\end{equation}
$$

上式揭示了矩阵K、矩阵K的特征向量$\alpha$以及降维后坐标之间的关系，显然，只要利用核函数得到矩阵K，就能够在不显示定义并计算映射函数$\phi(\pmb{x})$的情况下得到非线性升维并降维的结果。

上述结果并没有对主成分方向向量进行归一化，若想要得到归一化之后的结果，则想要进行如下处理。这里要注意是对主成分方向向量进行归一化，真实的投影方向是向量$\pmb{p}$，而不是向量$\pmb{\alpha}$，可以得到

$$
\begin{equation}
    \begin{split}
    \pmb{p}^T\pmb{p}
    &= 1 \\
    &= (\phi(X)\pmb{\alpha})^T\phi(X)\pmb{\alpha} \\
    &= \pmb{\alpha}^T\phi(X)^T\phi(X)\pmb{\alpha} \\
    &= \pmb{\alpha}^TK\pmb{\alpha} \\
    &= \lambda\pmb{\alpha}^T\pmb{\alpha}
    \end{split}
\end{equation}
$$

因此若要使得主成分方向向量归一化，应当在$\pmb{\alpha}$是单位向量的前提下，令$\pmb{\alpha} \leftarrow \frac{\pmb{\alpha}}{\sqrt\lambda}$

* **中心化**

上述推导是在$\phi(\pmb{x})$已经是中心化的前提下进行的（PCA的要求），但是显然，对于输入数据，不能保证经过非线性映射后是中心化的，因此需要对$\phi(\pmb{x})$进行专门的中心化。

$$
\begin{equation}
    \overset{\thicksim}\phi(\pmb{x}) = \phi(\pmb{x}) - \frac{1}{N}\sum_{i=1}^N\phi(\pmb{x_i})
\end{equation}
$$

若对每一个数据点均作中心化可以得到
$$
\begin{equation}
    \overset{\thicksim}\phi(X) = \phi(X) - \frac{1}{N}\phi(X)\pmb{1_{N\times N}}
\end{equation}
$$
其中，$\pmb{1_{N\times N}}$表示一个全为1的$N\times N$矩阵，由此可得中心化$\phi(X)$后的K矩阵

$$
\begin{equation}
    \begin{split}
    \overset{\thicksim}K
    &= \overset{\thicksim}\phi(X)^T\overset{\thicksim}\phi(X) \\
    &= (\phi(X) - \frac{1}{N}\phi(X)\pmb{1_{N\times N}})^T(\phi(X) - \frac{1}{N}\phi(X)\pmb{1_{N\times N}}) \\
    &= \phi(X)^T\phi(X) - \frac{1}{N}\pmb{1_{N\times N}}\phi(X)^T\phi(X) - \frac{1}{N}\phi(X)^T\phi(X)\pmb{1_{N\times N}} + \frac{1}{N^2}\pmb{1_{N\times N}}\phi(X)^T\phi(X)\pmb{1_{N\times N}} \\
    &= K - \frac{1}{N}\pmb{1_{N\times N}}K - \frac{1}{N}K\pmb{1_{N\times N}} + \frac{1}{N^2}\pmb{1_{N\times N}}K\pmb{1_{N\times N}}
    \end{split}
\end{equation}
$$

上式即为中心化后的矩阵K

* **测试数据的处理**

测试数据同样需要进行中心化，不同于PCA中只要直接减去均值，KPCA中需要对$\phi(\pmb{x_{test}})$进行中心化，即

$$
\begin{equation}
    \overset{\thicksim}\phi(\pmb{x_{test}}) = \phi(\pmb{x_{test}}) - \frac{1}{N}\sum_{i=1}^N\phi(\pmb{x_i})
\end{equation}
$$

若对每一个测试数据点均作中心化可以得到
$$
\begin{equation}
    \overset{\thicksim}\phi(X_{test}) = \phi(X_{test}) - \frac{1}{N}\phi(X)\pmb{1_{N\times L}}
\end{equation}
$$

对数据进行降维，类似于式-9
$$
\begin{equation}
    \begin{split}
    \pmb{Y_{test j}}
    &= \phi(X_{test})^T\pmb{p_j} \\
    &= \phi(X_{test})^T\phi(X)\pmb{\alpha_j} \\
    &= K_{test}\pmb{\alpha_j}
    \end{split}
\end{equation}
$$

同理可得

$$
\begin{equation}
    \begin{split}
    \overset{\thicksim}K_{test}
    &= \overset{\thicksim}\phi(X_{test})^T\overset{\thicksim}\phi(X) \\
    &= (\phi(X_{test}) - \frac{1}{N}\phi(X)\pmb{1_{N\times L}})^T(\phi(X) - \frac{1}{N}\phi(X)\pmb{1_{N\times N}}) \\
    &= \phi(X_{test})^T\phi(X) - \frac{1}{N}\pmb{1_{L\times N}}\phi(X)^T\phi(X) - \frac{1}{N}\phi(X_{test})^T\phi(X)\pmb{1_{N\times N}} + \frac{1}{N^2}\pmb{1_{L\times N}}\phi(X)^T\phi(X)\pmb{1_{N\times N}} \\
    &= K_{test} - \frac{1}{N}\pmb{1_{L\times N}}K - \frac{1}{N}K_{test}\pmb{1_{N\times N}} + \frac{1}{N^2}\pmb{1_{L\times N}}K\pmb{1_{N\times N}}
    \end{split}
\end{equation}
$$

## 算法流程

1. 定义原空间维度$k$，降维后维度$m$、数据集以及核函数
2. 由核函数计算矩阵$K$，其计算方式如下
$$
K_{ij}=\phi(\pmb{x_i}^T)\phi(\pmb{x_j}) = \mathcal{k}(\pmb{x_i}, \pmb{x_j})
$$
3. 由矩阵$K$计算中心化后的矩阵$\overset{\thicksim}K$，其计算方式如下
$$
    \overset{\thicksim}K = K - \frac{1}{N}\pmb{1_{N\times N}}K - \frac{1}{N}K\pmb{1_{N\times N}} + \frac{1}{N^2}\pmb{1_{N\times N}}K\pmb{1_{N\times N}}
$$
4. 对矩阵$\overset{\thicksim}K$进行谱分解得到前$m$个最大特征值$\lambda_1, \lambda_2, \cdots, \lambda_m$以及对应的特征向量$\pmb{\alpha_1}, \pmb{\alpha_2}, \cdots, \pmb{\alpha_m}$
5. 对特征向量进行归一化并使得主成分方向向量归一化
$$
    \pmb{\alpha} \leftarrow \frac{\pmb{\alpha}}{|\pmb{\alpha}|\sqrt\lambda}
$$
6. 计算降维后的坐标
$$
    \pmb{Y_j} = \overset{\thicksim}K\pmb{\alpha_{j}}, j=1, 2, \cdots, m
$$
7. 对于测试数据，则按照如下方式计算矩阵$\overset{\thicksim}K_{test}$
$$
    \begin{split}
    & K_{test} = \phi(X_{test})^T\phi(X) \\
    & K_{test\ ij} = \phi(\pmb{x_{test\ i}}^T)\phi(\pmb{x_j}) = \mathcal{k}(\pmb{x_{test\ i}}, \pmb{x_j}) \\
    & \overset{\thicksim}K_{test} = K_{test} - \frac{1}{N}\pmb{1_{L\times N}}K - \frac{1}{N}K_{test}\pmb{1_{N\times N}} + \frac{1}{N^2}\pmb{1_{L\times N}}K\pmb{1_{N\times N}}
    \end{split}
$$

## 参考资料

https://zhuanlan.zhihu.com/p/59775730

https://scikit-learn.org/stable/auto_examples/decomposition/plot_kernel_pca.html#sphx-glr-auto-examples-decomposition-plot-kernel-pca-py

```python
import numpy as np
from sklearn.decomposition import KernelPCA
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
from sklearn.utils.extmath import svd_flip
import matplotlib.pyplot as plt
```

```python
class MyKPCA(object):

    def __init__(self, n_components, kernel, gamma):

        self.n_components = n_components
        self.kernel = self.kernel_func(kernel)
        self.gamma = gamma

        self.mat_k = None
        self.mat_k_sim = None

        self._components = None
        self._lambdas = None

        self.train_data = None

    def fit_transform(self, input_data):
        self.fit(input_data)
        output_mat = self._components * self._lambdas
        return output_mat

    def fit(self, input_data):
        input_data = np.array(input_data)
        self.train_data = input_data.copy()
        samples_num, _ = input_data.shape
        self.mat_k = np.zeros((samples_num, samples_num))

        # compute matrix K
        self.mat_k = self._compute_mat_k(self.train_data)
        # compute matrix \overset{\thicksim}K
        self.mat_k_sim = self._compute_mat_k_sim(self.mat_k, self.mat_k)

        self._lambdas, self._components = self._topk_eigen(self.mat_k_sim, self.n_components)
        self._components, _ = svd_flip(self._components, np.zeros_like(self._components).T)
        for i in range(self.n_components):
            temp_component = self._components[:, i]
            temp_component = temp_component / (np.linalg.norm(temp_component) * np.sqrt(self._lambdas[i]))
            self._components[:, i] = temp_component.copy()

    def transform(self, input_data):

        mat_k_test = self._compute_mat_k(input_data)
        mat_k_test_sim = self._compute_mat_k_sim(self.mat_k, mat_k_test)

        output_mat = np.matmul(mat_k_test_sim, self._components)
        return output_mat

    def _compute_mat_k_sim(self, mat_k_train, mat_k_test):
        mat_k_sim = mat_k_test.copy()

        train_samples_num = mat_k_train.shape[0]
        ones_mat_like_test_data = np.ones_like(mat_k_test)
        ones_mat_list_train_data = np.ones_like(mat_k_train)

        mat_k_sim -= np.matmul(ones_mat_like_test_data, mat_k_train) / train_samples_num
        mat_k_sim -= np.matmul(mat_k_test, ones_mat_list_train_data) / train_samples_num
        mat_k_sim += ones_mat_like_test_data @ mat_k_train @ ones_mat_list_train_data / pow(train_samples_num, 2)

        return mat_k_sim

    def _compute_mat_k(self, input_data):
        input_data = np.array(input_data)
        samples_num = input_data.shape[0]

        k_mat = np.zeros((samples_num, self.train_data.shape[0]))

        for i in range(samples_num):
            for j in range(self.train_data.shape[0]):
                k_mat[i, j] = self.kernel(input_data[i, :], self.train_data[j, :])

        return k_mat

    def kernel_func(self, kernel_type):
        if kernel_type == "rbf":
            return self._rbf_kernel
        else:
            raise KeyError

    def _rbf_kernel(self, vec_1, vec_2):
        vec_1 = np.reshape(vec_1, (-1,))
        vec_2 = np.reshape(vec_2, (-1,))

        return np.exp(-self.gamma * np.power(np.linalg.norm(vec_1-vec_2), 2))

    def _topk_eigen(self, mat, k):
        e_vals, e_vecs = np.linalg.eigh(mat)
        sorted_ind = np.argsort(e_vals)

        topk_e_vals = e_vals[sorted_ind[:-1-k:-1]]
        topk_e_vecs = e_vecs[:, sorted_ind[:-1-k:-1]]
        return topk_e_vals, topk_e_vecs
```

```python
# 数据数量
sample_num = 1000
# 方差
std_ = 0.1

x_list, y_list = make_circles(n_samples=1000, factor=0.3, noise=0.05, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(x_list, y_list, stratify=y_list, random_state=0)
```

```python
# --------------------------- sklearn kpca ---------------------------
sklearn_kpca = KernelPCA(n_components=2, kernel="rbf", gamma=10)
sklearn_kpca_pred_x = sklearn_kpca.fit_transform(X_train)

# --------------------------- my kpca  ---------------------------
my_kpca = MyKPCA(n_components=2, kernel="rbf", gamma=10)
my_kpca_pred_x = my_kpca.fit_transform(X_train)

# -------------------------------
# draw func
plt.figure(figsize=(20, 20))

plt.subplot(221)
plt.title("Training Data", fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.scatter(x_list[:, 0], x_list[:, 1], c=y_list)
plt.subplot(222)
plt.title("Projection of training data using sklearn KPCA", fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
binwidth = 0.03
plt.scatter(sklearn_kpca_pred_x[:, 0], sklearn_kpca_pred_x[:, 1], c=y_train)

plt.subplot(223)
plt.title("Training Data", fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.scatter(x_list[:, 0], x_list[:, 1], c=y_list)
plt.subplot(224)
plt.title("Projection of training data using My KPCA", fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
binwidth = 0.03
plt.scatter(my_kpca_pred_x[:, 0], my_kpca_pred_x[:, 1], c=y_train)

plt.show()
```


# 5. Gyakorlat - 1 DoF csillapított lengő kar
2021.03.08

## Feladat:

```python
from IPython.display import Image
Image(filename='gyak_5_1.png',width=500)
```

A mellékelt ábrán egy lengőkar látható, ami két különböző tömegű és hosszúságú rúdból és a hozzá csatlakozó $R$ sugarú korongból áll. A két rúd két $k_1$, illetve $k_2$ merevségű rugón és egy $c_1$ csillapítási tényezőjű csillapító elemen keresztül csatlakozik a környezetehéz. A lengőkar csak az $A$ csukló körül tud elfordulni. A mozgást leíró általános koordináta a $\varphi$ szög, melyet a vízszintes egyenestől mérünk. A lengőkar a Föld gravitációs terében helyezkedik el, egyensúlyi helyezete a $\varphi=0$ pozícióban van, ahol a $k_2$ merevségű rugó feszítetlen.

### Adatok:
|||
|-------------------|------------------|
| $l$ = 0,2 m       | $k_1$ = 300 N/m  |
| $R$ = 0,1 m       | $k_2$ = 10 N/m   |
| $m$ = 0,12 kg     | $c_1$ = 2 Ns/m   |

### Részfeladatok:

1. Írja fel a mozgásegyenletet és számítsa ki a csillapítatlan ($\omega_n$) illetve csillapított sajátfrekvenciákat ($\omega_d$), és a realtív csillapítási tényezőt ($\zeta$)
2. Számítsa ki a kritikus csillapítási tényezőt ($c_{cr}$)!
3. Számítsa ki a $k_1$ merevségű rugóhoz tartozó maximális rugóerő értékét a következő kezdeti feltételek esetén ! ($\varphi(t=0)=\varphi_0=0,01$[rad];$\dot{\varphi}(t=0)=0$[rad/s])!

## Megoldás:

```python
from IPython.display import Image
Image(filename='gyak5_2.png',width=900)
```

### 1. Feladat:
A fenti ábrán a lengőkar szabadtest ábrája látható az egyensúgyi helyzetből kitérített helyezetében. Egy korábbi példában (3. gyakorlat) már be lett mutatva, hogy milyen egyszerűsítéséskkel lehet élni annak érdekében hogy linearizáljunk egy ilyen lengőrendszert. Röviden összefoglalva az egyszerűsítéseket:
 - a vízszintes rúdelemre ható gravitációs erőnek nincs hatása a sajátkörfrekvenciára (viszont hatása lehet a maximális rugóerőkre)
 - a rugók deformációja az egyensúlyi helyzetből mért ívhosszakkal jól közelíthetők

A mozgás egyenlet Newton II alapján:

\begin{equation}
    \dot{\mathbf{I}}=\mathbf{F}
\end{equation}

Amiből az SZTA alapján (és cos$\varphi\approx 1$, illetve a sin$\varphi\approx \varphi$ közelítéseket alkalmazva) adódik

\begin{equation}
    \Theta_A\ddot{\varphi}=-F_{r,1}3l-F_{r,2}l-F_{cs,1}3l-3mg\frac{3}{2}l-2mgl\varphi-mg(2l+R)\varphi,
\end{equation}

ahol a csillapításból adódó erő és a rugerők

\begin{equation}
    F_{cs,1}\cong c_13l\dot{\varphi}; \hspace{10pt} F_{r,1}\cong F_{r,1st}+k_13l\varphi; \hspace{10pt} F_{r,2}\cong k_2l\varphi
\end{equation}

(megjegyzés: a mozgásegyenletben szerplő $-3mg\frac{3}{2}l$ tag és a $k_1$ merevségű rugó statikus deformációjáből adódó erő ($F_{r,1st}$) kiegyenlítik egymást, így kiesenek az egyenletből). Tehát a mozgásegyenlet

\begin{equation}
    \Theta_A\ddot{\varphi}=-9l^2k_1\varphi-l^2k_2\varphi-9l^2c_1\dot{\varphi}-2mgl\varphi-mg(2l+R)\varphi
\end{equation}

A szerkezet tehetetlenségi nyomatékát a Steiner tétel segítségével lehet az $A$ pontra meghatározni

\begin{equation}
    \Theta_A=\frac{1}{3}3ml(3l)^2+\frac{1}{3}2ml(2l)^2+\frac{1}{2}mR^2+m(2l+R)^2
\end{equation}

```python
import sympy as sp
from IPython.display import display, Math

sp.init_printing()
```

```python
l, R, m, k1, k2, c1, Θ_A, g = sp.symbols("l, R, m, k1, k2, c1, Θ_A, g", real=True)

# Készítsünk behelyettesítési listát az adatok alapján, SI-ben
adatok = [(l, 0.2), (R, 0.1), (m, 0.12), (k1, 300), (k2, 10), (c1, 2), (g, 9.81)]

# Az általános koordináta definiálása az idő függvényeként
t = sp.symbols("t",real=True, positive=True)
φ_t = sp.Function('φ')(t)

# A z tengelyre számított perdület derivált az A pontban
dΠ_Az = Θ_A*φ_t.diff(t,2)

# A z tengelyre számított nyomaték az A pontban
M_Az = -9*l**2*k1*φ_t-l**2*k2*φ_t-9*l**2*c1*φ_t.diff(t)-2*m*g*l*φ_t-m*g*(2*l+R)*φ_t

# A dinamika alapegyenlete
# (nullára rendezve)

mozgegy = dΠ_Az-M_Az
mozgegy
```

```python
# Osszunk le a főegyütthatóval:
foegyutthato = mozgegy.coeff(sp.diff(φ_t,t,2))

mozgegy = (mozgegy/foegyutthato).expand().apart(φ_t)

mozgegy
```

```python
# Írjuk be a tehetetlenségi nyomatékot
# a rúdhosszakkal és tömegekkelkifejezve
mozgegy = mozgegy.subs(Θ_A, 1/3*3*m*(3*l)**2+1/3*2*m*(2*l)**2+1/2*m*R**2+m*(2*l+R)**2)
```

```python
# A mozgásegyenletnek ebben az alakjában
# d/dt(φ(t)) együtthatója 2ζω_n -nel
# a φ(t) együtthatója pedig ω_n^2-tel egyezik meg
# tehát mind a három kérdéses paraméter megadható

ω_n_num = sp.sqrt((mozgegy.coeff(φ_t)).subs(adatok)).evalf(6)

ζ_num = ((mozgegy.coeff(φ_t.diff(t))).subs(adatok)/(2*ω_n_num)).evalf(4)

ω_d_num = (ω_n_num*sp.sqrt(1-ζ_num**2)).evalf(6)

display(Math('\omega_n = {}'.format(sp.latex(ω_n_num))),Math('\zeta = {}'.format(sp.latex(ζ_num))),Math('\omega_d = {}'.format(sp.latex(ω_d_num))))
# [rad/s]
# [1]
# [rad/s]
```

$\displaystyle \omega_n = 35.5523$

$\displaystyle \zeta = 0.1169$

$\displaystyle \omega_d = 35.3085$

```python
# Később még szükség lesz a csillapított frekvenciára és periódusidőre is
T_d_num = (2*sp.pi/ω_d_num).evalf(4)

f_d_num = (ω_d_num/(2*sp.pi)).evalf(5)
display(Math('T_d = {}'.format(sp.latex(T_d_num))),Math('f_d = {}'.format(sp.latex(f_d_num))))

# [s]
# [1/s]
```

$\displaystyle T_d = 0.178$

$\displaystyle f_d = 5.6195$

## 2. Feladat
Kritikus csillapításról akkor beszélünk, amikor a relatív csillapítási tényező éppen 1.

```python
# A mozgásegyenletnek d/dt(φ(t)) együtthatóját kell vizsgálni
mozgegy.coeff(φ_t.diff(t)).evalf(5)
```

```python
# Ez az együttható pont 2ζω_n -nel egyenlő
# Az így adódó egyenlet megoldásásval kapjuk
# a kritikus csillapítshoz tartozó c1 értéket
ζ_cr = 1

# itt még nem helyettesítünk be, hanem csakkifejezzük a kritikus csillapítási együtthatót
c1_cr = sp.solve(mozgegy.coeff(φ_t.diff(t))-2*ζ_cr*ω_n_num,c1)[0]

# most már be lehet helyettesíteni
c1_cr_num = c1_cr.subs(adatok)
display(Math('c_{{1,cr}} = {:.6}'.format(sp.latex(c1_cr_num))))

# [1]
```

$\displaystyle c_{1,cr} = 17.104$

## 3. Feladat
A $k_1$ mrevségúrugóban ébredő erő az alábbi alakban adható meg

\begin{equation}
    F_{r,1}(t) = F_{r,1st}+k_13l\varphi(t).
\end{equation}

A rugó az egyensúlyi helyzetben feszített állapotban van. A statikus deformáció tehát az egyensúlyi egyenletből határozható meg

\begin{equation}
    \sum M_A=0:\hspace{20pt}     -F_{r,1st}3l-3mg\frac{3}{2}l=0,
\end{equation}

```python
Fr_1st = sp.symbols("Fr_1st")
Fr_1st_num = (sp.solve(-Fr_1st*3*l-3*m*g*3/2*l,Fr_1st)[0]).subs(adatok)
Fr_1st_num

# [N]
```

A dinamikus rugóerőnek ott lesz a maximuma ahol a legnagyobb a kitérés. Első körben tehát a mozgástörvény kell meghatározni.

```python
kezdeti_ert = {φ_t.subs(t,0): 0.01, φ_t.diff(t).subs(t,0): 0}
display(kezdeti_ert)

mozg_torv = (sp.dsolve(mozgegy.subs(adatok),φ_t,ics=kezdeti_ert)).evalf(6)
mozg_torv
```

Keressük meg a kitérés maximumát numerikus módszerek segítéségvel. Ehhez először célszerű kirajzoltatni a függvényünket. (Analatikus megoldáshoz lásd 4. gyakorlat hasoló példa megoldása!)

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

```python
# A matplotlib plottere az általunk megadott pontokat fogja egyenes vonalakkal összekötni.
# Elegendően kis lépésközt választva az így kapott görbe simának fog tűnni.
# Állítsuk elő az (x,y) koordinátákat!

t_val = np.linspace(0,0.5,1000) # lista létrehozása a [0 ; 0,5] intervallum 1000 részre való bontásával
φ_val = np.zeros(len(t_val)) # nulla lista létrehozása (ugyanannyi elemszámmal)
# for ciklus segítségével írjuk felül a nulla listában szerplő elemelet az adott x értékhez tartozó y értékekkel
for i in range(len(t_val)):
    φ_val[i] = mozg_torv.rhs.subs(t,t_val[i])

# rajzterület létrehozása
plt.figure(figsize=(40/2.54,30/2.54))

# függvény kirajzolása az x és y kordináta értékeket tartalmazó listák megadásásval
plt.plot(t_val,φ_val,color='b',label=r'num_sim')

# tengelyek
axes = plt.gca()
axes.set_xlim([0,t_val[-1]])
axes.set_ylim([-0.01, 0.01])

# rácsozás
plt.grid()

# tengely feliratozás
plt.xlabel(r'$ t [s] $',fontsize=30)
plt.ylabel(r'$ \varphi(t) [rad] $',fontsize=30)

plt.show()
```

A statikus rugóerő negatív előjelű, ezért két szélsőérték helyet is meg kell vizsgálni, az első lokális maximumot és a minimumot. Ezeket az értékekeket könnyen ki tudjuk szedni a korábban meghatározott listából.

```python
lok_max = max(φ_val)
lok_min = min(φ_val)

# Rugóerők meghatározása
Fr_11 = (Fr_1st_num+k1*3*l*φ_t).subs(adatok).subs(φ_t,lok_max).evalf(5)
Fr_12 = (Fr_1st_num+k1*3*l*φ_t).subs(adatok).subs(φ_t,lok_min).evalf(5)

display(Math('F_{{r,1}} = {}'.format(sp.latex(Fr_11))),Math('F_{{r,2}} = {}'.format(sp.latex(Fr_12))))

# [N]
```

$\displaystyle F_{r,1} = 0.0342$

$\displaystyle F_{r,2} = -3.0093$

A 1-es rugóban ébredő maximális erő tehát 3,009 N.

Készítette:
    Juhos-Kiss Álmos (Alkalmazott Mechanika Szakosztály)
    Takács Dénes (BME MM) kidolgozása és ábrái alapján.

        Hibák, javaslatok:
        amsz.bme@gmail.com
        csuzdi02@gmail.com
        almosjuhoskiss@gmail.com

            2021.03.07

```python

```


# Consumption Equivalent Variation (CEV)

1. Use the model in the **ConsumptionSaving.pdf** slides and solve it using **egm**
2. This notebooks estimates the *cost of income risk* through the Consumption Equivalent Variation (CEV)

We will here focus on the cost of income risk, but the CEV can be used to estimate the value of many different aspects of an economy. For eaxample, [Oswald (2019)](http://qeconomics.org/ojs/index.php/qe/article/view/701 "The option value of homeownership") estimated the option value of homeownership using a similar strategy as described below.

**Goal:** To estimate the CEV by comparing the *value of life* under the baseline economy and an alternative economy with higher permanent income shock variance along with a consumption compensation.

**Value of Life:**
1. Let the *utility function* be a generalized version of the CRRA utility function with $\delta$ included as a potential consumption compensation.
\begin{equation}
{u}(c,\delta) = \frac{(c\cdot(1+\delta))^{1-\rho}}{1-\rho}
\end{equation}
2. Let the *value of life* of a synthetic consumer $s$ for a given level of permanent income shock varaince, $\sigma_{\psi}$, and $\delta$, be
\begin{equation}
{V}_{s}({\sigma}_{\psi},\delta)=\sum_{t=1}^T \beta ^{t-1}{u}({c}^{\star}_{s,t}({\sigma}_{\psi},\delta),\delta)
\end{equation}
where ${c}^{\star}_{s,t}({\sigma}_{\psi},\delta)$ is optimal consumption found using the **egm**. The value of life is calcualted in the function `value_of_life(.)` defined below.

**Consumption Equivalent Variation:**
1. Let $V=\frac{1}{S}\sum_{s=1}^SV(\sigma_{\psi},0)$ be the average value of life under the *baseline* economy with the baseline value of $\sigma_{\psi}$ and $\delta=0$.
2. Let $\tilde{V}(\delta)=\frac{1}{S}\sum_{s=1}^SV(\tilde{\sigma}_{\psi},\delta)$ be the average value of life under the *alternative* economy with $\tilde{\sigma}_{\psi} > \sigma_{\psi}$.

The CEV is the value of $\delta$ that sets $V=\tilde{V}(\delta)$ and can be estimated as
\begin{equation}
\hat{\delta} = \arg\min_\delta (V-\tilde{V}(\delta))^2
\end{equation}
where the objective function is calculated in `obj_func_cev(.)` defined below.

# Setup

```python
%load_ext autoreload
%autoreload 2

import time
import numpy as np
import scipy.optimize as optimize

import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
prop_cycle = plt.rcParams['axes.prop_cycle']
colors = prop_cycle.by_key()['color']

import sys
sys.path.append('../')

from consav import jit
import ConsumptionSavingModel as csm
from ConsumptionSavingModel import ConsumptionSavingModelClass
```

# Setup the baseline model and the alternative model

```python
par = {'simT':40}
model = ConsumptionSavingModelClass(name='baseline',par=par)

# increase the permanent income with 100 percent and allow for consumption compensation
par_cev = {'sigma_psi':0.2,'do_cev':1,'simT':40}
model_cev = ConsumptionSavingModelClass(name='cev',par=par_cev)
```

```python
model.solve()
model.simulate()
```

    model solved in 3.1 secs
    model simulated in 3.4 secs

# Average value of life

**Define Functions:** value of life and objective function used to estimate "cev"

```python
def value_of_life(model):

    # utility associated with consumption for all N and T
    with jit(model) as model_jit:
        util = csm.utility(model_jit.sim.c,model_jit.par)

    # discounted sum of utility
    disc = np.ones(model.par.simT)
    disc[1:] = np.cumprod(np.ones(model.par.simT-1)*model.par.beta)

    disc_util = np.sum(disc*util,axis=1)

    # return average of discounted sum of utility
    return np.mean(disc_util)

def obj_func_cev(theta,model_cev,value_of_life_baseline):

    # update cev-parameter
    setattr(model_cev.par,'cev',theta)

    # re-solve and simulate alternative model
    model_cev.solve(do_print=False)
    model_cev.simulate(do_print=False)

    # calculate value of life
    value_of_life_cev = value_of_life(model_cev)

    # return squared difference to baseline
    return (value_of_life_cev - value_of_life_baseline)*(value_of_life_cev - value_of_life_baseline)

```

**Baseline value of life and objective function at cev=0**

```python
value_of_life_baseline = value_of_life(model)
obj_func_cev(0.0,model_cev,value_of_life_baseline)
```

    3.46846115430638

```python
# plot the objective function
grid_cev = np.linspace(0.0,0.2,20)
grid_obj = np.empty(grid_cev.size)

for j,cev in enumerate(grid_cev):
    grid_obj[j] = obj_func_cev(cev,model_cev,value_of_life_baseline)

plt.plot(grid_cev,grid_obj);
```

# Estimate the Consumption Equivalent Variation (CEV)

```python
res = optimize.minimize_scalar(obj_func_cev, bounds=[-0.01,0.5],
                               args=(model_cev,value_of_life_baseline),method='golden')
res
```

         fun: 9.21421081920921e-18
        nfev: 48
         nit: 43
     success: True
           x: 0.0975865281411968

The estimated CEV suggests that consumers would be indifferent between the baseline economy and a 100% increase in the permanent income shock variance along with a 10% increase in consumption in all periods.

00. DynamicProgramming/extra/Consumption Equivalent Variation (CEV).ipynb


## Subsurface Data Analytics

### Isotonic Regression for Subsurface Data Analytics in Python

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

### PGE 383 Exercise: Isotonic Regression for Subsurface Modeling in Python

Here's a simple workflow, demonstration of isotonic regression for subsurface modeling workflows. This should help you get started with building subsurface models with data analytics and machine learning. Here's some basic details about isotonic regression.

#### Isotonic Regression

Isotonic regression for prediction.  Here are some key aspects of linear regression:

**Stepwise Linear Parametric Model**

* the fit model is a piece-wise linear model at specific predictor feature isotonic constraints / thresholds under the constraint that $x_1 \ge ... \ge x_k$ where the resulting model must be monotonically increasing, $\hat{f}(x_1) \ge ... \ge \hat{f}(x_k)$.

* the loss function is:

\begin{equation}
min \sum_{i=1}^{n} w_i (y_i - \hat{f}(x)_i)^2
\end{equation}

Subject to the constraint that:

\begin{equation}
\hat{f}(x_1) \ge ... \ge \hat{f}(x_n)
\end{equation}

**Assumptions**
* **Model Parameters** - parameterized by response at each isotonic constraint / threshold
* **Monotonic Increasing** - may not have a negative slope
* **Piece-wise Linear Model** - assumes linear interpolation between each point

#### Workflow Goals

Learn the basics of isotonic regression in Python for analysis, modeling and prediction of porosity from density. This includes:

* Basic Python workflows and data preparation

* Training / fitting a ridge regression model

* Checking the model and learning about the impact of hyperparameters

#### Objective

Content to support the PGE 383: Subsurface Machine Learning class.

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

There are examples below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

#### Import Required Packages

Let's import the GeostatsPy package.

```python
import os                                                   # to set current working directory
import numpy as np                                          # arrays and matrix math
import scipy.stats as st                                    # statistical methods
import pandas as pd                                         # DataFrames
import matplotlib.pyplot as plt                             # for plotting
from sklearn.isotonic import IsotonicRegression
from sklearn.model_selection import train_test_split        # train and test split
from sklearn.metrics import explained_variance_score
```

If you get a package import error, you may have to first install some of these packages. This can usually be accomplished by opening up a command window on Windows and then typing 'python -m pip install [package-name]'. More assistance is available with the respective package docs.

#### Set the working directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).  Also, in this case make sure to place the required (see below) data file in this working directory.

```python
os.chdir("c:/PGE383")                                       # set the working directory
```

#### Loading Data

Let's load the provided dataset. 'GrainSize_por.csv' is available at https://github.com/GeostatsGuy/GeoDataSets. It is a comma delimited file with 20 grain size ($mm$) and porosity (as a percentage) measures from the subsurface. We load the data file with the pandas 'read_csv' function into a data frame we called 'df'.  We preview each with the head function from Pandas DataFrames.

```python
df = pd.read_csv("GrainSize_Por.csv")                    # read a .csv file in as a DataFrame
df
```

      Grainsize
      Porosity

      0
      2.689284
      5.400000

      1
      6.683638
      10.647582

      2
      9.877106
      7.926271

      3
      12.239344
      11.278953

      4
      27.623312
      10.508753

      5
      27.941670
      14.960621

      6
      28.096457
      10.619494

      7
      29.962596
      11.896658

      8
      30.602685
      12.479068

      9
      31.788502
      16.865819

      10
      35.977007
      16.611485

      11
      42.246780
      16.717406

      12
      44.865601
      15.023351

      13
      54.495121
      15.601486

      14
      59.434456
      16.785793

      15
      71.215562
      17.486703

      16
      73.862756
      19.641645

      17
      76.989911
      22.983510

      18
      88.401523
      20.464334

      19
      97.022766
      23.431172

#### Test and Train Split

Let's separate the data into test and train and visualize it.

```python
X_train, X_test, y_train, y_test = train_test_split(df.iloc[:,0], df.iloc[:,1], test_size=0.25, random_state=73073)
plt.subplot(111)
plt.scatter(X_train, y_train,  color='black', s = 20, alpha = 0.5, label='train')
plt.scatter(X_test, y_test,  color='blue', s = 20, alpha = 0.5, label='test')
plt.title('Porosity from Grainsize with Training Data'); plt.xlabel('Grain Size (mm)'); plt.ylabel('Porosity (%)')
plt.xlim(0,100)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()
plt.show()
```

The data is generally monotonic increasing with noise. The training and testing split looks reasonable.

Let's assign the isotonic constraints / thresholds and visualize them.

```python
X_predict = np.linspace(5,95,10)
plt.subplot(111)
plt.scatter(X_train, y_train,  color='black', s = 20, alpha = 0.5, label='train')
plt.scatter(X_test, y_test,  color='blue', s = 20, alpha = 0.5, label='test')
for threshold in X_predict:
    plt.axvline(threshold, color='red', alpha = 0.3)
plt.title('Porosity from Grainsize'); plt.xlabel('Grain Size (mm)'); plt.ylabel('Porosity (%)')
plt.xlim(0,100)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()
plt.show()
```

#### Fit the Model

Let's fit the model with these isotonic constraints / thresholds.

```python
iso_reg = IsotonicRegression().fit(X_train, y_train)
y_predict = iso_reg.predict(X_predict)
```

We can visualize the model parameters, the $f(x_1),...,f(x_k)$ estimates at the thresholds.

```python
y_predict
```

    array([ 7.64856974, 10.8938532 , 10.8938532 , 14.1199068 , 15.78074761,
           15.88347922, 17.88740369, 20.85697181, 23.1625063 , 23.38597028])

Now we can visualize the model with a regular line plot over the model parameters.

```python
# Plot model fit
plt.subplot(111)
plt.scatter(X_train, y_train,  color='black', s = 20, alpha = 0.5, label='train')
plt.scatter(X_test, y_test,  color='blue', s = 20, alpha = 0.5, label='test')
plt.plot(X_predict,y_predict, color='red', linewidth=1, label='model')
for threshold in X_predict:
    plt.axvline(threshold, color='red', alpha = 0.2)
plt.title('Isotonic Regression Porosity from Grainsize with Training Data'); plt.xlabel('Grain Size (mm)'); plt.ylabel('Porosity (%)')
plt.xlim(0,100)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()
plt.show()
```

#### Make Predictions and Check the Model Goodness

Now let's make predictions at the testing data locations and check the model goodness

* let's use the variance explained

We will plot the estimates for the training data as 'predicted test'

* note the iso_reg.transform() function does not seem to work properly

* we will just use a linear interpolation function from numpy to make estimates of the response feature with the model at new predictor feature values

```python
# The transform function should calculate the result for new observations, but doesn't seem to work
# let's just use piece-wise linear interpolation from NumPy.
#y_predict_test = iso_reg.transform(X_test)
y_predict_test = np.interp(X_test, X_predict, y_predict, left=None, right=None, period=None)
r2 = explained_variance_score(y_test,y_predict_test)
print('Variance Explained = ' + str(round(r2,3)))

# Plot test predictions
plt.subplot(111)
plt.scatter(X_train, y_train,  color='black', s = 20, alpha = 0.5, label='train')
plt.scatter(X_test, y_test,  color='blue', s = 20, alpha = 0.5, label='test')
plt.scatter(X_test, y_predict_test,  color='green', s = 20, alpha = 0.5, label='predicted test')
plt.plot(X_predict,y_predict, color='red', linewidth=1, label='model')
for threshold in X_predict:
    plt.axvline(threshold, color='red', alpha = 0.2)
plt.title('Isotonic Regression Porosity from Grainsize with Training Data'); plt.xlabel('Grain Size (mm)'); plt.ylabel('Porosity (%)')
plt.xlim(0,100)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()
plt.show()
```

Here's another model with very few isotonic constraints.

```python
# Let's try varying the number of isotonic constraints, thresholds
number_isotonic_constraints = 2
X_predict = np.linspace(10,90,number_isotonic_constraints)
iso_reg = IsotonicRegression().fit(X_train, y_train)
y_predict = iso_reg.predict(X_predict)

# Make predictions at testing data locations and calculate the model goodness
y_predict_test = np.interp(X_test, X_predict, y_predict, left=None, right=None, period=None)
r2 = explained_variance_score(y_test,y_predict_test)
print('Variance Explained = ' + str(round(r2,3)))

# Plot test predictions
plt.subplot(111)
plt.scatter(X_train, y_train,  color='black', s = 20, alpha = 0.5, label='train')
plt.scatter(X_test, y_test,  color='blue', s = 20, alpha = 0.5, label='test')
plt.scatter(X_test, y_predict_test,  color='green', s = 20, alpha = 0.5, label='predicted_test')
plt.plot(X_predict,y_predict, color='red', linewidth=1, label='model')
for threshold in X_predict:
    plt.axvline(threshold, color='red', alpha = 0.2)
plt.title('Isotonic Regression Porosity from Grainsize with Training Data'); plt.xlabel('Grain Size (mm)'); plt.ylabel('Porosity (%)')
plt.xlim(0,100)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()
plt.show()
```

Now let's run multiple models with variable number of isotonic constraints to perform a rough hyperparameter tuning.

* simple test and train split. cross validation

* model goodness by variance explained

```python

isotonic_constraint_list = np.linspace(2,35,100)
r2 = []

for k in isotonic_constraint_list:
    X_predict = np.linspace(3,97,k)
    iso_reg = IsotonicRegression().fit(X_train, y_train)
    y_predict = iso_reg.predict(X_predict)
    y_predict_test = np.interp(X_test, X_predict, y_predict, left=None, right=None, period=None)
    r2.append(explained_variance_score(y_test,y_predict_test))

plt.subplot(111)
plt.plot(isotonic_constraint_list, r2, color='red', linewidth=1, label='model')
plt.title('Isotonic Regression Tuning Number of Isotonic Constraints'); plt.xlabel('Number of Isotonic Constraints'); plt.ylabel('Variance Explained in Testing')
plt.xlim(0,40); plt.ylim(0,0.6)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
```

#### Comments

Isotonic regression provides a simple, piece-wise linear model with specified isotonic and monotonic constraint. The hyperparameter is the number of isotonic constraints. I hope this was helpful,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin
On twitter I'm the @GeostatsGuy.

***

#### More on Michael Pyrcz and the Texas Center for Geostatistics:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)


```python
import imp

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sympy as sym
import axelrod as axl
import axelrod.interaction_utils as iu

import testzd as zd

C, D = axl.Action.C, axl.Action.D
```

```python
parameters = imp.load_source('parameters', 'data/raw/parameters.py')
```

# Extortionate zero determinant.

In [1], given a match between 2 memory one strategies the concept of Zero Determinant strategies is introduced. It was showed that a player $p\in\mathbb{R}^4$ against a player $q\in\mathbb{R}^4$ could force a linear relationship between the scores.

Assuming the following:

- The utilities for player $p$: $S_x = (R, S, T, P)$ and for player $q$: $S_y = (R, T, S, P)$.
- The normalised long run score for player $p$: $s_x$ and for player $q$: $s_y$.
- Given $p=(p_1, p_2, p_3, p_4)$ a transformed (but equivalent) vector: $\tilde p=(p_1 - 1, p_2 - 1, p_3, p_4)$, similarly: $\tilde q=(1 - q_1, 1 - q_2, q_3, q_4)$

The main result of [1] is that:

if $\tilde p = \alpha S_x + \beta S_y + \gamma 1$ **or** if $\tilde q = \alpha S_x + \beta S_y + \gamma 1$ then:

$$
\alpha s_x + \beta s_y + \gamma 1 = 0
$$

where $\alpha, \beta, \gamma \in \mathbb{R}$

As an example consider the `extort-2` strategy defined in [2]. This is given by:

$$p=(8/9, 1/2, 1/3, 0)$$

Let us use the `Axelrod` library [4, 5] to simulate some matches, here it is against some of the best strategies in the Axelrod library:

```python
extort2 = axl.ZDExtort2()
players = (extort2, axl.EvolvedFSM16())
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
scores = match.final_score_per_turn()
np.round((scores[0] - 1) / (scores[1] - 1), 3)
```

    1.998

```python
players = (extort2, axl.EvolvedANN5())
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
scores = match.final_score_per_turn()
np.round((scores[0] - 1) / (scores[1] - 1), 3)
```

    2.0

```python
players = (extort2, axl.PSOGamblerMem1())
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
scores = match.final_score_per_turn()
np.round((scores[0] - 1) / (scores[1] - 1), 3)
```

    2.051

```python
players = (extort2, extort2)
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
scores = match.final_score_per_turn()
(scores[0] - 1) / (scores[1] - 1)
```

    1.0

We see that `extort2` beats all these strategies but gets a low score against itself.

In [1], in fact a specific type of Zero determinant strategy is considered, indeed if: $\gamma=-(\alpha + \beta)P$ then the relationship $\chi = S_X / S_Y$ holds where $\chi = \frac{-\beta}{\alpha}$ so that the $S_X - P$ will be at $\chi$ times bigger than $S_Y - P$ as long as $\chi > 1$. We can obtain a simple linear equation and an inequality that checks if a strategy is of this form:

```python
p = np.array([8 / 9, 1 / 2, 1 / 3, 0])
zd.is_ZD(p)
```

    True

```python
np.round(p, 3)
```

    array([0.889, 0.5  , 0.333, 0.   ])

Note however that even if there is a slight measurement error then these equations will fail:

```python
np.random.seed(0)
approximate_p = p + 10 ** -5 * np.random.random(4)
np.round(np.max(np.abs(p - approximate_p)), 3)
```

    0.0

```python
zd.is_ZD(approximate_p)
```

    False

Thus, this work proposes a statistical approach for recognising extortionate behaviour. This uses a least squares minimisation approach for the underlying linear algebraic problem being solved.

```python
x, SSError = zd.compute_least_squares(approximate_p)
alpha, beta = x
chi = -beta / alpha
np.round(chi, 3)
```

    2.0

In the paper, exact algebraic expressions for these measure have also been obtained:

```python
x, SSError = zd.get_least_squares(approximate_p)
alpha, beta = x
chi = -beta / alpha
np.round(chi, 3)
```

    2.0

Using the large data set of collected matches we can confirm the obtained formulae:

```python
try:
    df = pd.read_csv("./data/processed/full/std/overall/main.csv")
    assert (np.all(np.isclose(df["residual"], df["computed_residual"])) and
            np.all(np.isclose(df["alpha"], df["computed_alpha"])) and
            np.all(np.isclose(df["beta"], df["computed_beta"])))
except FileNotFoundError:
    pass
```

We see that in the case of an approximation of `extort2` we recover the value of $\chi=2$ (to the third decimal place).

The value that is in fact being minimised is called: $\text{SSError}$. This in fact gives us a measure of how far from being an extortionate strategy a given strategy vector $p$ is.

While all strategies are not necessarily memory one: so do not necessarily have a representation as a 4 dimensional vector. Their transition rates from all states to any action can still be measured.

Let us see how this works, using the 3 strategies above:

```python
def get_p_from_interactions(interactions):
    vectors = []
    cooperations = iu.compute_cooperations(interactions)
    for player, (coop_count, state_counter) in enumerate(zip(
        cooperations,
        iu.compute_state_to_action_distribution(interactions)
    )):
        p = []
        for state in ((C, C), (C, D), (D, C), (D, D)):
            if player == 1:
                state = state[::-1]
            try:
                p.append(state_counter[(state, C)] / (state_counter[(state, C)]  + state_counter[(state, D)] ) )
            except ZeroDivisionError:
                p.append(coop_count / len(interactions))
        vectors.append(p)
    return np.array(vectors)
```

```python
players = (extort2, axl.EvolvedFSM16())
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
p = get_p_from_interactions(interactions=interactions)[1]
```

```python
np.round(p, 3)
```

    array([0.375, 0.544, 0.472, 0.517])

```python
x, SSError = zd.get_least_squares(p)
np.round(SSError, 3)
```

    0.482

```python
players = (extort2, axl.EvolvedANN5())
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
p = get_p_from_interactions(interactions=interactions)[1]
x, SSError = zd.get_least_squares(p)
SSError
```

    0.008494117647058819

This particular strategy in fact does not visit all states:

```python
iu.compute_normalised_state_distribution(interactions=interactions)
```

    Counter({(C, C): 0.0075, (D, C): 0.0025, (D, D): 0.99})

but the overall cooperation rate is used for the missing values:

```python
iu.compute_normalised_cooperation(interactions=interactions)[1]
```

    0.01

```python
p
```

    array([1.  , 0.8 , 0.01, 0.  ])

```python
players = (extort2, axl.PSOGambler2_2_2())
axl.seed(0)
match = axl.Match(players, turns=parameters.TURNS)
interactions = match.play()
p = get_p_from_interactions(interactions=interactions)[1]
x, SSError = zd.get_least_squares(p)
np.round(SSError, 3)
```

    0.43

So it seems that the `PSOGambler2_2_2` is "less" extortionate than `EvolvedANN5`. Note: it is certainly not an extortionate strategy as $p_4 > 0$:

```python
np.round(p, 3)
```

    array([0.105, 0.002, 0.518, 0.505])

We can actually classify all potential extortionate strategies which is Figure 1 of the paper.

The paper extends this work to consider a LARGE number of strategies, and identifies if and when strategies actually exhibit extortionate behaviour.

We note that the strategies that exhibit strong evolutionary fitness are ones that are able to adapt their behaviour: they do not extort strong strategies (thus cooperation evolves) but they do extort weaker ones. For example, here is a list of strategies against which `EvolvedANN5` is close to being ZD (\\(\text{SS}_{\text{error}} < 0.05\\)):

```python
for opponent in parameters.PLAYER_GROUPS["full"]:
    players = (axl.EvolvedANN5(), opponent)
    axl.seed(0)
    match = axl.Match(players, turns=parameters.TURNS)
    interactions = match.play()
    p = get_p_from_interactions(interactions=interactions)[0]
    x, SSError = zd.compute_least_squares(p)
    if SSError < 0.05:
        alpha, beta = x
        scores = match.final_score_per_turn()
        print(f"vs {opponent}, chi={round(-beta / alpha, 2)}, (S_X - 1)/(S_Y - 1)={round((scores[0] - 1) / (scores[1] - 1), 2)}")
```

    vs Adaptive, chi=-6.22, (S_X - 1)/(S_Y - 1)=-4.06
    vs AntiCycler, chi=-5.28, (S_X - 1)/(S_Y - 1)=-4.04
    vs Anti Tit For Tat, chi=-3.64, (S_X - 1)/(S_Y - 1)=-4.04
    vs Bully, chi=-4.03, (S_X - 1)/(S_Y - 1)=-4.06
    vs Calculator, chi=7.64, (S_X - 1)/(S_Y - 1)=0.48
    vs Cooperator Hunter, chi=-5.28, (S_X - 1)/(S_Y - 1)=-4.05
    vs Cycler CCCCCD, chi=16.0, (S_X - 1)/(S_Y - 1)=-4.44
    vs Cycler CCCD, chi=15.58, (S_X - 1)/(S_Y - 1)=-4.15
    vs Cycler CCD, chi=15.53, (S_X - 1)/(S_Y - 1)=-4.13
    vs Cycler DC, chi=14.74, (S_X - 1)/(S_Y - 1)=-4.18
    vs Cycler CCCDCD, chi=15.53, (S_X - 1)/(S_Y - 1)=-4.14
    vs DoubleResurrection, chi=16.0, (S_X - 1)/(S_Y - 1)=-7.53
    vs Feld: 1.0, 0.5, 200, chi=0.32, (S_X - 1)/(S_Y - 1)=0.7
    vs $\phi$, chi=-2.9, (S_X - 1)/(S_Y - 1)=-4.04
    vs Hard Prober, chi=13.43, (S_X - 1)/(S_Y - 1)=0.0
    vs Harrington, chi=16.0, (S_X - 1)/(S_Y - 1)=0.8
    vs Hopeless, chi=15.56, (S_X - 1)/(S_Y - 1)=-4.08
    vs Knowledgeable Worse and Worse, chi=16.0, (S_X - 1)/(S_Y - 1)=4.89
    vs Negation, chi=-4.03, (S_X - 1)/(S_Y - 1)=-4.06
    vs $\pi$, chi=-2.9, (S_X - 1)/(S_Y - 1)=-4.04
    vs Predator, chi=1.29, (S_X - 1)/(S_Y - 1)=0.58
    vs Prober, chi=11.58, (S_X - 1)/(S_Y - 1)=-0.07
    vs Prober 3, chi=11.99, (S_X - 1)/(S_Y - 1)=-0.15
    vs Prober 4, chi=2.12, (S_X - 1)/(S_Y - 1)=1.24
    vs Random Tit for Tat: 0.5, chi=14.83, (S_X - 1)/(S_Y - 1)=-4.5
    vs RichardHufford, chi=7.24, (S_X - 1)/(S_Y - 1)=4.06
    vs SelfSteem, chi=14.44, (S_X - 1)/(S_Y - 1)=-4.49
    vs Sneaky Tit For Tat, chi=-2.57, (S_X - 1)/(S_Y - 1)=-4.1
    vs Stochastic Cooperator, chi=16.0, (S_X - 1)/(S_Y - 1)=-4.42
    vs Stochastic WSLS: 0.05, chi=16.0, (S_X - 1)/(S_Y - 1)=-4.29
    vs TF1, chi=1.17, (S_X - 1)/(S_Y - 1)=0.79
    vs ThueMorse, chi=15.15, (S_X - 1)/(S_Y - 1)=-4.16
    vs ThueMorseInverse, chi=-6.05, (S_X - 1)/(S_Y - 1)=-4.09
    vs Tranquilizer, chi=3.62, (S_X - 1)/(S_Y - 1)=1.6
    vs Tricky Cooperator, chi=-4.42, (S_X - 1)/(S_Y - 1)=-4.05
    vs Tricky Defector, chi=-2.49, (S_X - 1)/(S_Y - 1)=-4.05
    vs Win-Shift Lose-Stay: D, chi=15.56, (S_X - 1)/(S_Y - 1)=-4.08
    vs Worse and Worse, chi=16.0, (S_X - 1)/(S_Y - 1)=5.35
    vs Worse and Worse 2, chi=16.0, (S_X - 1)/(S_Y - 1)=25.33
    vs ZD-Extort-2: 0.1111111111111111, 0.5, chi=8.92, (S_X - 1)/(S_Y - 1)=0.5
    vs ZD-Extort3: 0.11538461538461539, 0.3333333333333333, 1, chi=16.0, (S_X - 1)/(S_Y - 1)=0.51
    vs ZD-Extort-2 v2: 0.125, 0.5, 1, chi=8.92, (S_X - 1)/(S_Y - 1)=0.5
    vs ZD-Mem2, chi=12.3, (S_X - 1)/(S_Y - 1)=-9.79
    vs $e$, chi=-2.9, (S_X - 1)/(S_Y - 1)=-4.04
    vs Meta Hunter Aggressive: 7 players, chi=-5.09, (S_X - 1)/(S_Y - 1)=-5.9

This work shows here that not only is there a mathematical basis for suspicion: the calculation of $\text{SSError}$ but that some high performing strategies seem to exhibit suspicious behaviour that allows them to adapt.

## References

[1] Press, William H., and Freeman J. Dyson. "Iterated Prisoner’s Dilemma contains strategies that dominate any evolutionary opponent." Proceedings of the National Academy of Sciences 109.26 (2012): 10409-10413

[2] Stewart, Alexander J., and Joshua B. Plotkin. "Extortion and cooperation in the Prisoner’s Dilemma." Proceedings of the National Academy of Sciences 109.26 (2012): 10134-10135.

[3] Golub, Gene H., and Charles F. Van Loan. Matrix computations. Vol. 3. JHU Press, 2012.

[4] The Axelrod project developers. Axelrod: v4.2.0. 2016. http://doi.org/10.5281/zenodo.1252994

[5] Knight, Vincent, et al. "An Open Framework for the Reproducible Study of the Iterated Prisoner’s Dilemma." Journal of Open Research Software 4.1 (2016).

```python

```

```python

```

```python

```


# Course 3 Week 3 lecture notebook 01
# Permutation Method

In this lecture notebook, we'll demonstrate the permutation method on a dataset you worked on in Week 1 of Course 2. By going through this short exercise, you'll hopefully develop some intuition on how to do the related task in this week's programming assignment.

## Setup

First, let's import the packages we'll be using:

```python
from google.colab import drive

drive.mount('/content/drive')
```

```python
import os

os.chdir('drive/My Drive/AI for Health/Treatment/Week 3')
```

```python
import pandas as pd
import pickle
from util import cindex
```

Next, we load the model we trained during the said assignment in Course 2. This accepts a patient's age, systolic blood pressure, diastolic blood pressure, and cholesterol to output a risk score (i.e. probability of an event).

```python
model_X = pickle.load(open('C2M1_model.model', 'rb'))
```

We will also use the same test dataset from that programming assignment:

```python
# patient data (test set in the previous assignment)
X_baseline = pd.read_csv('./lecture_nb_permutation_data/X_data_normalized.csv',index_col=0)

# corresponding patient outcome
y = pd.read_csv('./lecture_nb_permutation_data/y_data_test.csv',index_col=0)
```

Let's print the first 10 cells to peek into the dataset. Take note that these are already standardized and normalized as you did previously.

```python
X_baseline.head(10)
```

      Age
      Systolic_BP
      Diastolic_BP
      Cholesterol

      4320
      0.747640
      -1.211321
      -1.003548
      -1.072461

      2006
      -0.758669
      -1.476605
      -1.541427
      -0.434487

      5689
      0.067833
      0.399011
      0.904060
      0.694137

      472
      -0.257881
      0.655984
      1.157183
      0.700589

      1370
      0.800621
      -0.664058
      0.228552
      1.366445

      1457
      -0.988683
      0.935349
      0.752583
      -0.709739

      5783
      0.145403
      0.759190
      0.155382
      1.790223

      3484
      0.149429
      -0.852439
      0.671971
      0.477288

      1145
      -0.250499
      -0.173969
      0.006586
      -0.360930

      130
      -0.757791
      -0.781030
      0.431331
      -0.617740

## C-index and Baseline Performance

Now let's get the c-index of the trained model using our test dataset. Just to refresh:

* The c-index measures the discriminatory power of a risk score.
* Intuitively, a higher c-index indicates that the model's prediction is in agreement with the actual outcomes of a pair of patients.
* The formula for the c-index is

$$ \mbox{cindex} = \frac{\mbox{concordant} + 0.5 \times \mbox{ties}}{\mbox{permissible}} $$

* A permissible pair is a pair of patients who have different outcomes.
* A concordant pair is a permissible pair in which the patient with the higher risk score also has the worse outcome.
* A tie is a permissible pair where the patients have the same risk score.

Computing this for our test dataset will give us our baseline performance. For efficiency, we will be using the `concordance_index()` method from the [lifelines](https://lifelines.readthedocs.io/en/latest/lifelines.utils.html) package instead of the homemade c-index function we developed in the Course 2 assignment. This is wrapped in a `cindex()` function in the `util` library we imported earlier.

```python
# get the patient's risk scores by feeding the baseline data to the trained model
scores = model_X.predict_proba(X_baseline)[:, 1]

# measure the model's cindex given the patient outcome and computed risk scores above
c_index = cindex(y.values, scores)

print(f"baseline c-index is {c_index:.4f}")
```

    baseline c-index is 0.8182

This should look familiar as this is also the result you got in the Course 2 assignment.

## Feature Importance and the Permutation Method

As discussed in the lecture videos, one way we can compute the importance of a feature in a given model is by shuffling the values of a particular column in our test set. Evaluating the performance of the model after this shuffling will allow us to quantify the feature's importance. For this exercise, we'll choose to shuffle the `Age` column. You will do this programmatically in this week's assignment but for now, you'll use a dataset that is shuffled beforehand.

```python
# patient data permuted at the Age column
X_permuted_1 = pd.read_csv('./lecture_nb_permutation_data/X_data_normalized_perm1.csv',index_col=0)

# Let's print the first 10 cells and compare with the baseline table above.
# All are in the same place except for the values in the Age column
X_permuted_1.head(10)
```

      Age
      Systolic_BP
      Diastolic_BP
      Cholesterol

      4320
      -0.494544
      -1.211321
      -1.003548
      -1.072461

      2006
      -0.289638
      -1.476605
      -1.541427
      -0.434487

      5689
      0.425693
      0.399011
      0.904060
      0.694137

      472
      -2.244324
      0.655984
      1.157183
      0.700589

      1370
      -0.078010
      -0.664058
      0.228552
      1.366445

      1457
      -1.475322
      0.935349
      0.752583
      -0.709739

      5783
      0.694167
      0.759190
      0.155382
      1.790223

      3484
      -0.695555
      -0.852439
      0.671971
      0.477288

      1145
      -0.761429
      -0.173969
      0.006586
      -0.360930

      130
      0.021294
      -0.781030
      0.431331
      -0.617740

Now, let's see how our model performs with this shuffled input:

```python
# get the patient's risk scores by feeding the baseline data with shuffled Age column
scores = model_X.predict_proba(X_permuted_1)[:, 1]

# measure the c-index given the patient outcome and newly computed risk scores
c_index = cindex(y.values, scores)
print(f"c-index for 1st permutation is {c_index:.4f}")
```

    c-index for 1st permutation is 0.6749

We see a big drop in the performance. To quantify the importance of this feature, we simply subtract this drop from the baseline performance. This results in `0.8182 - 0.6749 = 0.1433`

## Additional Permutations

The result above (i.e. `0.1433`) describes the feature importance of `Age` in our model given the shuffled dataset. However, you might deduce that the predicted risk scores are greatly affected by how the Age column is rearranged. If we arrange it differently, then the model will predict different values and this might lead to a different c-index, and consequently a different value of the feature importance. To illustrate, let's import two more datasets with different permutations of the `Age` column. Since there is a random element when doing the permutation, the resulting order of the elements in the `Age` column will be different for each shuffle. You can see that in the two datasets below.

```python
# 2nd permutation
X_permuted_2 = pd.read_csv('./lecture_nb_permutation_data/X_data_normalized_perm2.csv',index_col=0)

X_permuted_2.head(10)
```

      Age
      Systolic_BP
      Diastolic_BP
      Cholesterol

      4320
      0.728099
      -1.211321
      -1.003548
      -1.072461

      2006
      0.008176
      -1.476605
      -1.541427
      -0.434487

      5689
      0.604035
      0.399011
      0.904060
      0.694137

      472
      -1.179030
      0.655984
      1.157183
      0.700589

      1370
      0.835747
      -0.664058
      0.228552
      1.366445

      1457
      0.872547
      0.935349
      0.752583
      -0.709739

      5783
      0.498609
      0.759190
      0.155382
      1.790223

      3484
      0.324675
      -0.852439
      0.671971
      0.477288

      1145
      0.671958
      -0.173969
      0.006586
      -0.360930

      130
      -0.925788
      -0.781030
      0.431331
      -0.617740

```python
# 3rd permutation
X_permuted_3 = pd.read_csv('./lecture_nb_permutation_data/X_data_normalized_perm3.csv',index_col=0)

X_permuted_3.head(10)
```

      Age
      Systolic_BP
      Diastolic_BP
      Cholesterol

      4320
      -0.593886
      -1.211321
      -1.003548
      -1.072461

      2006
      0.303244
      -1.476605
      -1.541427
      -0.434487

      5689
      -1.755198
      0.399011
      0.904060
      0.694137

      472
      -0.500154
      0.655984
      1.157183
      0.700589

      1370
      -0.109112
      -0.664058
      0.228552
      1.366445

      1457
      0.439787
      0.935349
      0.752583
      -0.709739

      5783
      1.335356
      0.759190
      0.155382
      1.790223

      3484
      -0.428345
      -0.852439
      0.671971
      0.477288

      1145
      0.199869
      -0.173969
      0.006586
      -0.360930

      130
      0.146330
      -0.781030
      0.431331
      -0.617740

Now let's evaluate the performance of the model with these new datasets:

```python
# get the patient's risk scores by feeding a dataset with a different permutation of the Age column
scores = model_X.predict_proba(X_permuted_2)[:, 1]

# measure c-index
c_index = cindex(y.values, scores)

print(f"c-index for 2nd permutation: {c_index:.4f}")
```

    c-index for 2nd permutation: 0.6554

```python
# get the patient's risk scores by feeding a dataset with a different permutation of the Age column
scores = model_X.predict_proba(X_permuted_3)[:, 1]

# measure c-index
c_index = cindex(y.values, scores)

print(f"c-index for 3rd permutation: {c_index:.4f}")
```

    c-index for 3rd permutation: 0.6428

As you can see, there are slight differences in the resulting c-index depending on how the values are shuffled. This will then result in different values of the feature importance as well. Let's summarize our results in a table for clarity:

| Dataset      | C-index | Importance |
|:---------------|:--------:| :--------: |
|Baseline       | 0.8182| n/a |
|1st permutation| 0.6749 | 0.1433 |
|2nd permutation| 0.6554 | 0.1628 |
|3rd permutation| 0.6428 | 0.1754 |

To take these variations into account, we can take the mean of these different results to have a single value of the feature importance. More formally:

\begin{align}
I_x  = \left\lvert perf - perf_x  \right\rvert
\end{align}

where $I_x$ is the importance of feature $x$ and
\begin{align}
perf_x  = \frac{1}{n}\cdot \sum_{i=1}^{n} perf_i^{sx}
\end{align}

where $perf_i^{sx}$ is the performance with the feature $x$ shuffled in the $i$th permutation.

Applying this to our results, that would be `abs(0.8182 - mean([0.6749, 0.6554, 0.6428]) = 0.1605` . We would want to get more permutations to get a more accurate value of the feature importance. You will get the chance to work this out in the week's programming assignment.

That's it for this lecture on the permutation method! We can now measure the global importance of a feature in the predictions of a given model. In the next section of the course, you will learn how to determine the importance of a feature for an individual patient in the dataset.


Tài liệu này mang giấy phép Creative Commons Attribution (CC BY).
(c) Nguyễn Ngọc Sáng, Zhukovsky 7/2020.

[@SangVn](https://github.com/SangVn) [@VnCFD](https://vncfdgroup.wordpress.com/)

*Thực hành CFD với Python!*

**Tài liệu tham khảo**

* 1. V. V. Vlasenko, “Mathematical approach and principles of developing numerical methods for the EWT-TsAGI application package,” Trudy TsAGI, No. 2671 (2007).
* 2. F. Moukalled, L. Mangani, M. Darwish, "The Finite Volume Method in Computational Fluid Dynamics. An Advanced Introduction with OpenFOAM and Matlab"

# Bài 26. Phương pháp tính dòng khuếch tán

Các khái niệm, công thức cơ bản được sử dụng để tính dòng khuếch tán trong VnCFD_v3.

## 1. Tọa độ cong (curvilinear coordinates)

Để tính được dòng khuếch qua các mặt của ô lưới, chúng ta cần phải xác định được gradient của $(T, u, v)$ tại đó. Ví dụ, ta có công thức xấp xỉ $(T_x)_{_{i+1/2}} = \left(\frac{\partial T}{\partial x}\right)_{_{i+1/2}} = \frac{T_{i+1}-T_{i}}{x_{i+1}-x_{i}}$, tuy nhiên công thức này chỉ có thể sử dụng chính xác trong điều kiện lưới đều Cartesian($x_i=const, y_i=const$). Trường hợp tổng quát, nhiệt độ T được lưu tại các tâm ô lưới nằm trên các đường cong. Do đó để tính gradient ta sẽ chuyển từ không gian vật lý (x, y) sang không gian tọa độ cong $(\xi, \eta)$ gắn với lưới (hình dưới).

Khi đó gradient của đại lượng $\phi$ được xác định theo công thức:
\begin{align}
\begin{pmatrix} \phi_x \\ \phi_y \end{pmatrix} =
\begin{pmatrix}
\frac{\partial \xi}{\partial x} & \frac{\partial \xi}{\partial y} \\
\frac{\partial \eta}{\partial x} & \frac{\partial \eta}{\partial y}
\end{pmatrix}^T
\begin{pmatrix} \phi_{\xi} \\ \phi_{\eta} \end{pmatrix}
\end{align}

với J là ma trận Jacobi:
\begin{equation}
J=
\begin{pmatrix}
\frac{\partial \xi}{\partial x} & \frac{\partial \xi}{\partial y} \\
\frac{\partial \eta}{\partial x} & \frac{\partial \eta}{\partial y}
\end{pmatrix} =
\begin{pmatrix}
\frac{\partial x}{\partial \xi} & \frac{\partial x}{\partial \eta} \\
\frac{\partial y}{\partial \xi} & \frac{\partial y}{\partial \eta}
\end{pmatrix}^{-1}
\end{equation}

Đối với vận tốc, ta có:
\begin{align}
\begin{pmatrix} x_t \\ y_t \end{pmatrix} =
\begin{pmatrix}
x_\xi & x_\eta \\
y_\xi & y_\eta
\end{pmatrix}
\begin{pmatrix} \xi_t \\ \eta_t \end{pmatrix}
\end{align}

Hay $V_{\xi, \eta} = J \ V_{x, y}$.

## 2. Ma trận Jacobi

**Các đạo hàm riêng được tính theo công thức xấp xỉ:**

$\phi_{\xi}=\frac{\Delta\phi}{\Delta\xi}$ với gia số tọa độ dọc đường lưới $\Delta^L \xi=\sqrt{(\Delta^L x)^2 + (\Delta^L y)^2}, \Delta^R \xi=\sqrt{(\Delta^R x)^2 + (\Delta^R y)^2}, \Delta \xi = \Delta^L \xi + \Delta^R \xi $.

Xét cụ thể ô lưới (i,j) và mặt (i+1/2,j) như trên hình.

**Jacobi tại tâm ô lưới (i,j):**
$$\frac{\partial x}{\partial \xi}=\frac{x_{i+1/2,j}-x_{i-1/2,j}}{\sqrt{(x_{i+1/2,j}-x_{i-1/2,j})^2+(y_{i+1/2,j}-y_{i-1/2,j})^2}}$$
$$\frac{\partial y}{\partial \xi}=\frac{y_{i+1/2,j}-y_{i-1/2,j}}{\sqrt{(x_{i+1/2,j}-x_{i-1/2,j})^2+(y_{i+1/2,j}-y_{i-1/2,j})^2}}$$
$$\frac{\partial x}{\partial \eta}=\frac{x_{i,j+1/2}-x_{i,j-1/2}}{\sqrt{(x_{i,j+1/2}-x_{i,j-1/2})^2+(y_{i,j+1/2}-y_{i,j-1/2})^2}}$$
$$\frac{\partial y}{\partial \eta}=\frac{y_{i,j+1/2}-y_{i,j-1/2}}{\sqrt{(x_{i,j+1/2}-x_{i,j-1/2})^2+(y_{i,j+1/2}-y_{i,j-1/2})^2}}$$

Đây là các công thức xấp xỉ sai phân trung tâm (central difference).

**Jacobi tại tâm mặt (i+1/2,j):**

Theo phương tiếp tuyến với bề mặt:
$$\frac{\partial x}{\partial \eta}=\frac{x_{i+1/2,j+1/2}-x_{i+1/2,j-1/2}}{\sqrt{(x_{i+1/2,j+1/2}-x_{i+1/2,j-1/2})^2+(y_{i+1/2,j+1/2}-y_{i+1/2,j-1/2})^2}}$$
$$\frac{\partial y}{\partial \eta}=\frac{y_{i+1/2,j+1/2}-y_{i+1/2,j-1/2}}{\sqrt{(x_{i+1/2,j+1/2}-x_{i+1/2,j-1/2})^2+(y_{i+1/2,j+1/2}-y_{i+1/2,j-1/2})^2}}$$

Riêng đối với $x_\xi, y_\xi$, trong trường hợp tổng quát điểm (i+1/2,j) có thể không nằm trên đoạn nối tâm hai ô lưới. Do đó để xác định chúng ta sử dụng hàm nội suy:
$$x_\xi = interpolation(x_\xi^L, x_\xi^R, \Delta \xi^L, \Delta \xi^R)=\frac{\Delta \xi^R x_\xi^L + \Delta \xi^L x_\xi^R}{\Delta \xi^L+\Delta \xi^R}$$
$$x_\xi^L=\frac{x_{i+1/2,j}-x_{i,j}}{\Delta \xi^L}, \Delta \xi^L = \sqrt{(x_{i+1/2,j}-x_{i,j})^2+(y_{i+1/2,j}-y_{i,j})^2}$$
$$x_\xi^R=\frac{x_{i+1,j}-x_{i+1/2,j}}{\Delta \xi^R}, \Delta \xi^R = \sqrt{(x_{i+1,j}-x_{i+1/2,j})^2+(y_{i+1,j}-y_{i+1/2,j})^2}$$
Tương tự cho $y_\xi$.

Đối với các mặt nằm trên biên của vùng tính toán, phụ thuộc theo vị trí của chúng ta lấy: $x_\xi=x_\xi^L$ hoặc $x_\xi=x_\xi^R$.

## 3. Gradient
Gradient $(\phi_x, \phi_y)$ với $\phi$ là các đại lượng $(T, u, v)$ được tính theo công thức: $(\phi_x, \phi_y)^T=J(\phi_\xi, \phi_\eta)^T$.

**Gradient tại tâm ô lưới (i,j):**

Trong trường hợp lưới đều, ta có thể sử dụng công thức xấp xỉ sai phân trung tâm:
$$\phi_\xi = \frac{\phi_{i+1,j}-\phi_{i-1,j}}{\Delta \xi}, \Delta \xi = \sqrt{(x_{i+1,j}-x_{i-1,j})^2+(y_{i+1,j}-y_{i-1,j})^2}$$
Trường hợp tổng quát, ta sử dụng hàm nội suy:
$$\phi_\xi = interpolation(\phi_\xi^L, \phi_\xi^R, \Delta\xi^L, \Delta\xi^R)$$ với

$$\phi_\xi^L=\frac{\phi_{i,j}-\phi_{i-1,j}}{\Delta \xi^L}, \Delta \xi^L = \sqrt{(x_{i,j}-x_{i-1,j})^2+(y_{i,j}-y_{i-1,j})^2}$$
$$\phi_\xi^R=\frac{\phi_{i+1,j}-\phi_{i,j}}{\Delta \xi^R}, \Delta \xi^R = \sqrt{(x_{i+1,j}-x_{i,j})^2+(y_{i+1,j}-y_{i,j})^2}$$
Tương tự cho $\phi_\eta$.

**Gradient tại tâm mặt (i+1/2,j):**

Theo phương tiếp tuyến với mặt, gradient được tính thông qua hàm nội suy hai giá trị tại hai ô lưới hai bên: $$(\phi_\eta)_{i+1/2,j} = interpolation((\phi_\eta)_{i,j}, (\phi_\eta)_{i+1,j}, (\Delta\xi)_{i,j}, (\Delta\xi)_{i+1,j})$$
Theo phương còn lại, nếu lưới đều thì tâm mặt $C$ nằm giữa đường nối hai tâm ô lưới $(i, j)$ và $(i+1, j)$ - tại điểm có $\xi_C=0.5(\xi_{i, j} + \xi_{i+1,j})$, khi đó, ta có công thức xấp xỉ bậc 2:
$$(\phi_\xi)_{i+1/2,j} = (\phi_\xi)_C \approx \frac{\Delta \phi}{\Delta \xi} = \frac{\phi_{i+1, j} - \phi_{i, j}}{\xi_{i+1,j}-\xi_{i,j}}$$
Trường hợp tổng quát, sử dụng công thức khai triển:
$$(\phi_\xi)_{i+1/2,j}=(\phi_\xi)_C+\left(\frac{\partial \phi_\xi}{\partial \xi}\right)_C (\xi_{i+1/2,j}-\xi_C) + O((\Delta \xi)^2)\\
=(\phi_\xi)_C+\left(\frac{\partial \phi_\xi}{\partial \xi}\right)_C (\xi_{i+1/2,j}-0.5(\xi_{i,j}+\xi_{i+1,j})) + O((\Delta \xi)^2)\\
=(\phi_\xi)_C+\left(\frac{\partial \phi_\xi}{\partial \xi}\right)_C 0.5(\Delta \xi^L-\Delta \xi^R) + O((\Delta \xi)^2)\\
\approx \frac{\phi_{i+1, j} - \phi_{i, j}}{\Delta \xi^L + \Delta \xi^R}
+ \frac{(\phi_\xi)_{i+1,j}-(\phi_\xi)_{i,j}}{\Delta \xi^L + \Delta \xi^R}0.5(\Delta \xi^L-\Delta \xi^R)$$

## 4. Độ nhớt động lực học, mật độ dòng nhiệt

Để tính độ nhớt động lực học hay mật độ dòng nhiệt tại các mặt chúng ta phải sử dụng giá trị nhiệt độ T nào cho phù hợp? Ở đây ta sẽ xác định nhiệt độ T trên các mặt ô lưới thông qua hàm nội suy giá trị $(\rho, p)$ ở hai ô lưới hai bên. Trong VnCFD, tại tâm các lưới ta chỉ lưu các thông số $P=(\rho, u, v, p)$ mà không có T. Tại các mặt ô lưới $P$ xác định theo công thức:
$$P_{i+1/2,j} = interpolation(P_{i,j}, P_{i+1,j}, \Delta \xi^L, \Delta \xi^R)$$

Khi đó, tại các mặt:
$$\mu = \mu(T)$$
$$gradT = grad(p/(R\rho)) = grad p/(R\rho) - p\ grad\rho/(R\rho^2)$$

## 5. GhostCell

Để thuận tiện cho việc tính toán ở trên biên hay ở các ô lưới kề biên, người ta thường sử dụng thêm một "ô lưới ma" bên ngoài các mặt trên biên vùng tính toán gọi là "Ghost Cell". Trong VnCFD_v3, GhostCell có kích thước bằng không, và chứa các thông số dòng chảy tại mặt trên biên kề nó.

## 6. Tổng kết

Như vậy, để tính dòng khuếch tán, chúng ta cần bổ sung vào solver Euler các bước sau:

1. Tính ma trận Jacobi tại các mặt ô lưới
2. Tính Gradient theo tọa độ $\xi, \eta$ dọc đường lưới tại tâm các ô lưới và tại các mặt ô lưới
3. Tính Gradient theo tọa độ $x,y$ tại các mặt ô lưới
4. Tính giá trị các thông số $P=(\rho, u, v, p)$ tại các mặt ô lưới dùng hàm nội suy
5. Tính độ nhớt động lực học, tesor độ nhớt, mật độ dòng nhiệt tại các mặt
6. Tính dòng khuếch tán và bổ sung vào dòng qua mặt.

Ngoài các công thức tính gradient ở trên dành cho lưới có cấu trúc, có thể sử dụng các công thức cho lưới không cấu trúc như trong tài liệu tham khảo thứ 2 về OpenFoam.

# [Bài 27. Điều kiện biên](Bai_27.ipynb)

```python

```