# Modelagem SEIR
Neste notebook está implementado o modelo SEIR de Zhilan Feng. Que inclui quarentena, e hospitalizações

\begin{align}
\frac{dS}{dt}&=-\beta S (I+(1-\rho)H)\\
\frac{dE}{dt}&= \beta S (I+(1-\rho)H)-(\chi+\alpha)E\\
\frac{dQ}{dt}&=\chi E -\alpha Q\\
\frac{dI}{dt}&= \alpha E - (\phi+\delta)I\\
\frac{dH}{dt}&= \alpha Q +\phi I -\delta H\\
\frac{dR}{dt}&= \delta I +\delta H
\end{align}

Denotando o tamanho total da epidemia por $Y_e(t)=E(t)+Q(t)+I(t)+H(t)$, podemos escrever:

\begin{align}
\nonumber \frac{dY_e}{dt}&= \frac{dE}{dt}+\frac{dQ}{dt}+\frac{dI}{dt}+\frac{dH}{dt}\\
\label{ye}&=\beta S (I(1-\rho)H) -\delta(I+H)
\end{align}

```python
from scipy.integrate import odeint
from scipy.integrate import DOP853
import pandas as pd
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
import warnings
import humanizer_portugues as hp
import seaborn as sns
from datetime import timedelta
warnings.filterwarnings("ignore")
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```python
def seqihr(y,t,*params):
    S,E,Q,I,H,R = y
    chi,phi,beta,rho,delta,alpha = params
    return[
        -beta*S*(I+(1-rho)*H), #dS/dt
        beta*S*(I+(1-rho)*H) - (chi+alpha)*E,#dE/dt
        chi*E -alpha*Q,#dQ/dt
        alpha*E - (phi+delta)*I,#dI/dt
        alpha*Q + phi*I -delta*H,#dH/dt
        delta*I + delta*H,#dR/dt
    ]
```

```python
chi=.05  # Taxa de quarentenação
phi=.01 # Taxa de Hospitalização
beta=.2 #taxa de Transmissão
rho=.6 # Atenuação da transmissão quando hospitalizado
delta=1/10 # Taxa de recuperação hospitalar
alpha=1/3 # Taxa de incubaçao
```

```python
inits = [0.9,.1,0,0,0,0]
trange = arange(0,100,.1)
res = odeint(seqihr,inits,trange,args=(chi,phi,beta,rho,delta,alpha))
```

```python

@interact(chi=(0,.1, 0.01),phi=(0,.1,.005),beta=(0,.5,.05),rho=(0,1,.1),delta=(0,1,.05),alpha=(0,1,0.01))
def plota_simulação(chi=0.05,phi=.01,beta=.2,rho=.6,delta=.1,alpha=.33):
    res = odeint(seqihr,inits,trange,args=(chi,phi,beta,rho,delta,alpha))
    fig, ax = subplots(1,1, figsize=(15,10))
    ax.plot(trange,res[:,1:-1])
    ax.set_ylabel('Fração da População')
    ax.set_xlabel('Tempo (dias)')
    ax.grid()
    ax.legend(['Exposto','Quar','Inf','Hosp']);
```

    interactive(children=(FloatSlider(value=0.05, description='chi', max=0.1, step=0.01), FloatSlider(value=0.01, …

## Adicionando Assintomáticos

O modelo de Feng não é apropriado para representar a epidemia da COVID-19. Vamos Modificá-lo para incluir entre outras coisas, os assintomáticos. Para simplificar a notação vamos denotar por $\lambda=\beta(I\color{red}{+A}+(1-\rho)H)$, já incluindo os assintomáticos, $A$.

\begin{align}
\frac{dS}{dt}&=-\lambda[\color{red}{(1-\chi)} S] \\
\frac{dE}{dt}&= \lambda [\color{red}{(1-\chi)} S] -\alpha E\\
\frac{dI}{dt}&= \color{red}{(1-p)}\alpha E - (\phi+\delta)I\\
\color{red}{\frac{dA}{dt}}&= \color{red}{p\alpha E -\delta A}\\
\frac{dH}{dt}&= \phi I -\delta H\\
\frac{dR}{dt}&= \delta I +\delta H \color{red}{+ \delta A}
\end{align}

Neste novo modelo alteramos mais algumas coisinhas ressaltadas em vermelho acima. Em primeiro lugar, fizemos com que a quarentena seja aplicada aos suscetíveis, efetivamente removendo-os da cadeia de transmissão, para simplificar removemos o compartimento $Q$. Além disso, para alimentar a classe dos Assintomáticos, definimos $p$ como a fração de assintomáticos na população. $R_0\approx$

### Adicionando ou removendo a quarentena
Para permitir que a quarentena seja acionada no modelo em uma data definida por um parâmetro $q$, precisamos expressá-la como uma função diferenciável que varie de zero a um rapidamente. Esta função é então multiplicada pelo parâmetro de quarentena $\chi$.
\begin{equation}
    \chi^+(t)=\chi\frac{1+tanh(t-q)}{2}
\end{equation}

Para remover a quarentena podemos usar a função com o sinal trocado na tangente hiperbólica.
\begin{equation}
    \chi^-(t)=\chi\frac{1-tanh(t-q)}{2}
\end{equation}
Abaixo temos uma exemplificação grafica de $\chi^+(t)$ e $\chi^-(t)$

```python
t = arange(-3,10,.01)
d=1
q = 0.3 #Nivel de quarentena desejado
addq = lambda t,d: (1+tanh(t-d))/2
remq = lambda t,d: (1-tanh(t-d))/2
f,(ax1,ax2)=subplots(1,2, figsize=(15,8))
ax1.plot(t,q*addq(t,d), label=f"liga a quarent. no dia {d}")
ax1.plot(t,q*remq(t,d), label=f"desliga a quarent. no dia {d}")
ax2.plot(t,q*addq(t,d)*remq(t,d+5))
ax1.vlines(d,0,q)
ax2.vlines(d,0,q,'g')
ax2.vlines(d+5,0,q, 'r')
ax1.grid()
ax2.grid()
ax1.legend();
```

```python
f,ax=subplots(1,1, figsize=(15,8))
ax.plot(t,q*addq(t,d)*remq(t,d+5))
ax.vlines(d,0,q,'g')
ax.vlines(d+5,0,q, 'r')
ax.set_ylabel('Fraction Quarantined')
ax.set_xlabel('Time(days)')
ax.grid()
savefig('chi_of_t.png')
```

```python
def seqiahr(y,t,*params):
    S,E,I,A,H,R,C,M = y
    chi,phi,beta,rho,delta,alpha,mu,p,q,r = params
    lamb = beta*(I+A)
    chi *= ((1+np.tanh(t-q))/2) * ((1 - np.tanh(t - (q+r))) / 2 )
    return[
        -lamb*((1-chi)*S), #dS/dt
        lamb*((1-chi)*S) - alpha*E,#dE/dt
        (1-p)*alpha*E - delta*I,#dI/dt
        p*alpha*E - delta*A,
        phi*delta*I -(rho+mu)*H,#dH/dt
        (1-phi)*delta*I + rho*H+delta*A ,#dR/dt
        phi*delta*I,#(1-p)*alpha*E+ p*alpha*E # Casos acumulados
        mu*H  #Mortes acumuladas
    ]
```

```python
trange = arange(0, 365, 1)
inits = [0.99, 0, 1.0277e-6, 0.0, 0, 0, 0, 0]
N = 97.3e6
fat = 0.035  # case fatality rate
# sumario = open('cenarios.csv','w')
# sumario.write('R0,Quarentena,tamanho_total,hosp_total,hosp_pico,morte_total\n')

def r0(chi, phi, beta, rho, delta, p, S=1):
    "R0 for seqiahr2"
    return -(S*beta*chi - S*beta)/delta

@interact(χ=(0, 1, 0.05),
          φ=(0, .5, .01),
          β=(0, 1, .02),
          ρ=(0, 1, .1),
          δ=(0, 1, .05),
          α=(0, 10, 0.01),
          mu=(0,1,.01),
          p=(0, 1, .05),
          q=(0, 120, 10),
          r=(0,100,10))
def plota_simulação(χ=0.7, φ=.01, β=.5, ρ=0.05, δ=.1, α=.33, mu=0.03, p=.75, q=30, r=10):
    res = odeint(seqiahr, inits, trange, args=(χ, φ, β, ρ, δ, α, mu, p, q,r))
    rzero = r0(χ, φ, β, ρ, δ, p)
#     et = 1 / rzero
#     idx = np.argwhere(np.abs(res[:, 0] - et) == min(np.abs(res[:, 0] -
#                                                            et)))[0, 0]
    fig, ax = subplots(1, 1, figsize=(15, 10))
    ax.plot(trange, res[:, 1])#E
#     ax.plot(trange, res[:, 2:4])
    ax.plot(trange, res[:, 4]) #H
    ax.plot(trange, res[:,-1]) #M
    ax.set_ylim([0, 0.02])
    Ye = res[:, 1:4].sum(axis=1)
    Imax = (1 - res[-1, 0]) * N  # N-S(inf)
    Hosp_p = res[:, 4].max() * N  # pico de Hospitalizações
    Hosp_tot = (res[-1, -2]) * N
    casos = (res[:, 2] + res[:, 4])  # Casos notificados H+I
    casosT = Hosp_tot + ((1 - p) * α * res[:, 1]).sum() * N  # N*sum(p*α*E)
    M = res[-1,-1]*N
    #     secax = ax.twinx()
    #     secax.plot(trange, Ye,'k:')
    #     secax.set_ylabel('Total prevalence fraction')
    ax.text(0,
            0.005,
            f"$R_0~{rzero:.1f}$\n$p={p}$\n$\\rho={ρ}$\n$\\chi={χ*100}$%",
            fontsize=16)
    ax.text(
        110,
        .01,
        f"Tamanho total: {hp.intword(Imax)} infectados\nHosp. pico: {hp.intword(Hosp_p)}\nHosp. totais: {hp.intword(Hosp_tot)}\nMortes: {hp.intword(M)}",
        fontsize=16)
#     ax.vlines(trange[idx], 0, 0.02,'b')  #res[:,1:-1].max())
    ax.vlines(q, 0, .02)
    ax.vlines(q+r, 0, .02,'r')
    ax.set_ylabel('Fração da População')
    ax.set_xlabel('Tempo (dias)')
    ax.grid()
    #         sumario.write(f'{rzero},{χ*100},{Imax},{Hosp_tot},{Hosp_p},{M}\n')
#     ax.legend(['Exposto', 'Inf', 'Assintomático', 'Hosp', 'Mortes'])
    ax.legend(['Exposto', 'Hospitalizado', 'Mortes'])
#     plt.savefig(f"", dpi=300)
```

    interactive(children=(FloatSlider(value=0.7, description='χ', max=1.0, step=0.05), FloatSlider(value=0.01, des…

```python
# sumario.close()
# suma = pd.read_csv('cenarios.csv')
# suma.drop_duplicates(inplace=True)
# suma
```

```python
def save_data():
    fig,ax = subplots(1,1, figsize=(15,10), constrained_layout=True)
    χ=0.05;φ=.01;β=.25;ρ=.6;δ=.1;α=10; p=.75; q=0
    i=1
    dff = pd.DataFrame(data={'Sem controle':range(365),'Com controle':range(365)})
    for q,R0,c in [(0,3.6,'Sem controle'),(10,1.7,'Com controle')]:
        res = odeint(seqiahr2,inits,trange,args=(q/100,φ,R0/10,ρ,δ,α,p,q))
        df = pd.DataFrame(data=res*N, columns=['S','E','I','A','H','R','C'],index=trange)
        ax = df[['H']].plot(ax=ax, grid=True)
        dff[c] =df.H
    plt.legend(['Sem Controles','Com controles'])
    plt.savefig(f'export/achatando a curva.png', dpi=300)
    dff.to_csv(f'export/achatando a curva.csv')

save_data()
```

## Carregando os dados do Brasil

```python
import  io, gzip
from urllib.request import Request, urlopen
# Fetching cases from Brasil.io
request = Request('https://data.brasil.io/dataset/covid19/caso_full.csv.gz', headers={"User-Agent": "python-urllib"})
response = urlopen(request)
cases = pd.read_csv(io.StringIO(gzip.decompress(response.read()).decode("utf-8")))
cases
```

      city
      city_ibge_code
      date
      epidemiological_week
      estimated_population_2019
      is_last
      is_repeated
      last_available_confirmed
      last_available_confirmed_per_100k_inhabitants
      last_available_date
      last_available_death_rate
      last_available_deaths
      order_for_place
      place_type
      state
      new_confirmed
      new_deaths

      0
      São Paulo
      3550308.0
      2020-02-25
      9
      12252023.0
      False
      False
      1
      0.00816
      2020-02-25
      0.0000
      0
      1
      city
      SP
      1
      0

      1
      NaN
      35.0
      2020-02-25
      9
      45919049.0
      False
      False
      1
      0.00218
      2020-02-25
      0.0000
      0
      1
      state
      SP
      1
      0

      2
      São Paulo
      3550308.0
      2020-02-26
      9
      12252023.0
      False
      False
      1
      0.00816
      2020-02-26
      0.0000
      0
      2
      city
      SP
      0
      0

      3
      NaN
      35.0
      2020-02-26
      9
      45919049.0
      False
      False
      1
      0.00218
      2020-02-26
      0.0000
      0
      2
      state
      SP
      0
      0

      4
      São Paulo
      3550308.0
      2020-02-27
      9
      12252023.0
      False
      False
      1
      0.00816
      2020-02-27
      0.0000
      0
      3
      city
      SP
      0
      0

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      605295
      NaN
      43.0
      2020-08-26
      35
      11377239.0
      True
      False
      115984
      1019.43890
      2020-08-26
      0.0279
      3235
      170
      state
      RS
      3221
      74

      605296
      NaN
      42.0
      2020-08-26
      35
      7164788.0
      True
      False
      137560
      1919.94515
      2020-08-26
      0.0156
      2142
      168
      state
      SC
      1870
      26

      605297
      NaN
      28.0
      2020-08-26
      35
      2298696.0
      True
      False
      71222
      3098.36533
      2020-08-26
      0.0256
      1822
      166
      state
      SE
      296
      14

      605298
      NaN
      35.0
      2020-08-26
      35
      45919049.0
      True
      False
      776135
      1690.22446
      2020-08-26
      0.0376
      29194
      184
      state
      SP
      10465
      282

      605299
      NaN
      17.0
      2020-08-26
      35
      1572866.0
      False
      True
      46364
      2947.73999
      2020-08-25
      0.0134
      621
      162
      state
      TO
      0
      0

605300 rows × 17 columns

```python
df_states = cases[cases.place_type!='state'].groupby(['date','state']).sum()
df_states.reset_index(inplace=True)
df_states.set_index('date', inplace=True)
```

```python
df_states.columns
```

    Index(['state', 'city_ibge_code', 'epidemiological_week',
           'estimated_population_2019', 'is_last', 'is_repeated',
           'last_available_confirmed',
           'last_available_confirmed_per_100k_inhabitants',
           'last_available_death_rate', 'last_available_deaths', 'order_for_place',
           'new_confirmed', 'new_deaths'],
          dtype='object')

```python
# df_states.set_index('date', inplace=True)
fig,ax = subplots(1,1,figsize=(15,8))

for uf in ['SP','RJ','MG','CE','RS', 'PE', 'AM', 'BA']:
    df_states[df_states.state==uf].last_available_confirmed.plot(ax=ax,style='-o', label=uf, grid=True)

ax.legend()
plt.savefig('Casos_confirmados_estados.png',dpi=200)
```

```python
df_states
```

      state
      city_ibge_code
      epidemiological_week
      estimated_population_2019
      is_last
      is_repeated
      last_available_confirmed
      last_available_confirmed_per_100k_inhabitants
      last_available_death_rate
      last_available_deaths
      order_for_place
      new_confirmed
      new_deaths

      date

      2020-02-25
      SP
      3.550308e+06
      9
      12252023.0
      0.0
      0.0
      1
      0.00816
      0.0000
      0
      1
      1
      0

      2020-02-26
      SP
      3.550308e+06
      9
      12252023.0
      0.0
      0.0
      1
      0.00816
      0.0000
      0
      2
      0
      0

      2020-02-27
      SP
      3.550308e+06
      9
      12252023.0
      0.0
      0.0
      1
      0.00816
      0.0000
      0
      3
      0
      0

      2020-02-28
      SP
      3.550308e+06
      9
      12252023.0
      0.0
      0.0
      2
      0.01632
      0.0000
      0
      4
      1
      0

      2020-02-29
      SP
      3.550308e+06
      9
      12252023.0
      0.0
      0.0
      2
      0.01632
      0.0000
      0
      5
      0
      0

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      2020-08-26
      RS
      2.099871e+09
      17080
      11350785.0
      488.0
      0.0
      115984
      462534.87423
      11.9896
      3235
      47320
      3221
      74

      2020-08-26
      SC
      1.241895e+09
      10360
      7164788.0
      296.0
      0.0
      137560
      492925.98844
      4.8632
      2142
      32811
      1870
      26

      2020-08-26
      SE
      2.102946e+08
      2625
      2298696.0
      0.0
      75.0
      70926
      175394.70213
      2.4974
      1808
      8829
      0
      0

      2020-08-26
      SP
      2.276010e+09
      22610
      45919049.0
      646.0
      0.0
      776135
      797469.31263
      18.3873
      29194
      74773
      10465
      282

      2020-08-26
      TO
      2.379053e+08
      4865
      1572866.0
      0.0
      139.0
      46364
      252339.35109
      3.2839
      621
      14684
      0
      0

4493 rows × 13 columns

```python
import episem
def casos_estado_covid(estado, cases):
    casos = cases[(cases.state == estado)].reset_index()[['date', 'last_available_confirmed', 'last_available_deaths']]
    casos['data'] = pd.to_datetime(casos.date)
    casos.set_index('data', inplace=True)
    casos.sort_index(inplace=True)
    casos['incidencia_casos'] = casos.last_available_deaths.diff()
    casos['incidencia_morte'] = casos.last_available_confirmed.diff()
    casos['ew'] = [int(episem.episem(x, out='W')) for x in casos.index]
    casos.to_csv(f'dados_{estado}.csv')
    return casos

estados = set(df_states.state)
for uf in estados:
    casos_estado_covid(uf,df_states)
```

```python
# Pegando os dados do Ministério
casos = pd.read_csv('COVID19_20200411.csv',sep=';')
casos.data = pd.to_datetime(casos.data, dayfirst=True)
casos_estados = casos.groupby(['data','estado']).sum()
casos_estados
```

      casosNovos
      casosAcumulados
      obitosNovos
      obitosAcumulados

      data
      estado

      2020-01-30
      AC
      0
      0
      0
      0

      AL
      0
      0
      0
      0

      AM
      0
      0
      0
      0

      AP
      0
      0
      0
      0

      BA
      0
      0
      0
      0

      ...
      ...
      ...
      ...
      ...
      ...

      2020-04-11
      RS
      4
      640
      1
      15

      SC
      39
      732
      3
      21

      SE
      0
      42
      0
      4

      SP
      203
      8419
      20
      560

      TO
      0
      23
      0
      0

1971 rows × 4 columns

```python
casos.columns
```

    Index(['regiao', 'estado', 'data', 'casosNovos', 'casosAcumulados',
           'obitosNovos', 'obitosAcumulados'],
          dtype='object')

```python
fig,ax = subplots(1,1,figsize=(15,8))
casos_estados.reset_index(inplace=True)
casos_estados.set_index('data', inplace=True)
for uf in ['SP','RJ','MG','CE','RS', 'BA']:
    casos_estados[casos_estados.estado==uf].casosAcumulados.plot(style='-o', label=uf)

ax.legend()
plt.savefig('Casos_Acumulados_estados.png',dpi=200);
```

```python
trange = arange(0,1095,1)
χ=0.0;φ=.1;β=.5;ρ=.6;δ=.1;α=.53; p=.75; q=-2; mu=0.02; r=1000
inits = [0.99,0,1.0277e-8, 0.0,0,0,0,0]
res = odeint(seqiahr,inits,trange,args=(χ,φ,β,ρ,δ,α,mu,p,q,r))
# Com controle
χ=0.1;β=.17
res_c = odeint(seqiahr,inits,trange,args=(χ,φ,β,ρ,δ,α,mu,p,q, r))
plt.plot(trange,res[:,-1],label='no control');
plt.plot(trange,res_c[:,-1],label='with control');
plt.legend();
```

```python
df_brasil = casos_estados.groupby('data').sum()
# df_brasil['casos_simulados']=res[:dias,-1]*N
# dfcp = df_brasil[['confirmed']]
# dfcp.to_csv('export/dados_brasil.csv')
df_brasil
```

      casosNovos
      casosAcumulados
      obitosNovos
      obitosAcumulados

      data

      2020-01-30
      0
      0
      0
      0

      2020-01-31
      0
      0
      0
      0

      2020-02-01
      0
      0
      0
      0

      2020-02-02
      0
      0
      0
      0

      2020-02-03
      0
      0
      0
      0

      ...
      ...
      ...
      ...
      ...

      2020-04-07
      1661
      13717
      114
      667

      2020-04-08
      2210
      15927
      133
      800

      2020-04-09
      1930
      17857
      141
      941

      2020-04-10
      1781
      19638
      115
      1056

      2020-04-11
      1089
      20727
      68
      1124

73 rows × 4 columns

```python
df_brasil.casosAcumulados.plot();
```

```python
dias=400
offset = 35 # quantos dias antes do primeiro caso notificado devemos começar a simulaçao
drange = pd.date_range(df_brasil[df_brasil.casosAcumulados>0].index.min()-timedelta(offset),periods=dias,freq='D')
# df_states.reset_index(inplace=True)
# df_brasil = df_states.groupby('date').sum()
fig,ax = subplots(1,1,figsize=(15,8))
ax.plot(drange,res[:dias,-1]*N,'-v', label='Sem controle')
# ax.plot(drange,res[:dias,-3]*N,'-v', label='Hosp')
# ax.vlines('2020-05-5',0,1e6)
ax.plot(drange,res_c[:dias,-1]*N,'-v', label='Com controle (Simulados)')
# ax.plot(drange,res[:dias,2]*N,'-^', label='Prevalência')
df_brasil[df_brasil.casosAcumulados>0].casosAcumulados.plot(ax=ax, style='-o',
                                                            label='Dados oficiais',
                                                            grid=True,
                                                            logy=True)
# Sem controle
ax.text('2020-05-15',0.6e5, f'Casos Totais: {res[dias,-1]*N:.0f}\nMortes: {res[dias,-1]*N*fat:.0f}',
        fontsize=16)
# com 10% quarentena e R0=1.7
ax.text('2020-11-15',1e3, f'Casos Totais: {res_c[dias,-1]*N:.0f}\nMortes:{res_c[dias,-1]*N*fat:.0f}',
        fontsize=16)
ax.legend();
# plt.savefig('export/Casos_vs_Projeções_log.png',dpi=200)
```

```python
df_sim = pd.DataFrame(data={'sem controle':res[:dias,-1]*N,'com controle':res_c[:dias,-1]*N}, index=drange)
df_sim.to_csv('simulação_brasil_com_e_sem_controle.csv')
```

## Comparando a série do Brasil com a de outros países

```python
confirmed = pd.read_csv('https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv')
confirmed = confirmed.groupby('Country/Region').sum()
```

```python
confirmed
```

      Lat
      Long
      1/22/20
      1/23/20
      1/24/20
      1/25/20
      1/26/20
      1/27/20
      1/28/20
      1/29/20
      ...
      3/25/20
      3/26/20
      3/27/20
      3/28/20
      3/29/20
      3/30/20
      3/31/20
      4/1/20
      4/2/20
      4/3/20

      Country/Region

      Afghanistan
      33.0000
      65.0000
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      84
      94
      110
      110
      120
      170
      174
      237
      273
      281

      Albania
      41.1533
      20.1683
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      146
      174
      186
      197
      212
      223
      243
      259
      277
      304

      Algeria
      28.0339
      1.6596
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      302
      367
      409
      454
      511
      584
      716
      847
      986
      1171

      Andorra
      42.5063
      1.5218
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      188
      224
      267
      308
      334
      370
      376
      390
      428
      439

      Angola
      -11.2027
      17.8739
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      3
      4
      4
      5
      7
      7
      7
      8
      8
      8

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      Venezuela
      6.4238
      -66.5897
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      91
      107
      107
      119
      119
      135
      135
      143
      146
      153

      Vietnam
      16.0000
      108.0000
      0
      2
      2
      2
      2
      2
      2
      2
      ...
      141
      153
      163
      174
      188
      203
      212
      218
      233
      237

      West Bank and Gaza
      31.9522
      35.2332
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      59
      84
      91
      98
      109
      116
      119
      134
      161
      194

      Zambia
      -15.4167
      28.2833
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      12
      16
      22
      28
      29
      35
      35
      36
      39
      39

      Zimbabwe
      -20.0000
      30.0000
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      3
      3
      5
      7
      7
      7
      8
      8
      9
      9

181 rows × 75 columns

```python
conf_US = pd.read_csv('https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_US.csv')
```

```python
conf_US
```

      UID
      iso2
      iso3
      code3
      FIPS
      Admin2
      Province_State
      Country_Region
      Lat
      Long_
      ...
      3/25/20
      3/26/20
      3/27/20
      3/28/20
      3/29/20
      3/30/20
      3/31/20
      4/1/20
      4/2/20
      4/3/20

      0
      16
      AS
      ASM
      16
      60.0
      NaN
      American Samoa
      US
      -14.2710
      -170.1320
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      1
      316
      GU
      GUM
      316
      66.0
      NaN
      Guam
      US
      13.4443
      144.7937
      ...
      37
      45
      51
      55
      56
      58
      69
      77
      82
      84

      2
      580
      MP
      MNP
      580
      69.0
      NaN
      Northern Mariana Islands
      US
      15.0979
      145.6739
      ...
      0
      0
      0
      0
      0
      0
      2
      6
      6
      6

      3
      630
      PR
      PRI
      630
      72.0
      NaN
      Puerto Rico
      US
      18.2208
      -66.5901
      ...
      51
      64
      79
      100
      127
      174
      239
      286
      316
      316

      4
      850
      VI
      VIR
      850
      78.0
      NaN
      Virgin Islands
      US
      18.3358
      -64.8963
      ...
      17
      17
      19
      22
      0
      0
      30
      30
      30
      37

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      3248
      84090053
      US
      USA
      840
      90053.0
      Unassigned
      Washington
      US
      0.0000
      0.0000
      ...
      51
      69
      67
      0
      125
      274
      274
      303
      344
      501

      3249
      84090054
      US
      USA
      840
      90054.0
      Unassigned
      West Virginia
      US
      0.0000
      0.0000
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      3250
      84090055
      US
      USA
      840
      90055.0
      Unassigned
      Wisconsin
      US
      0.0000
      0.0000
      ...
      0
      0
      61
      0
      0
      0
      0
      0
      0
      0

      3251
      84090056
      US
      USA
      840
      90056.0
      Unassigned
      Wyoming
      US
      0.0000
      0.0000
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      3252
      84099999
      US
      USA
      840
      99999.0
      NaN
      Grand Princess
      US
      0.0000
      0.0000
      ...
      28
      28
      28
      103
      103
      103
      103
      103
      103
      103

3253 rows × 84 columns

```python
conf_US.groupby('Country_Region').sum()
```

      UID
      code3
      FIPS
      Lat
      Long_
      1/22/20
      1/23/20
      1/24/20
      1/25/20
      1/26/20
      ...
      3/25/20
      3/26/20
      3/27/20
      3/28/20
      3/29/20
      3/30/20
      3/31/20
      4/1/20
      4/2/20
      4/3/20

      Country_Region

      US
      272936469664
      2730712
      104327612.0
      120958.868944
      -290083.749251
      1
      1
      2
      2
      5
      ...
      65778
      83836
      101657
      121478
      140886
      161807
      188172
      213362
      243453
      275582

1 rows × 78 columns

```python
serie_US = conf_US.groupby('Country_Region').sum().T
serie_US = serie_US.iloc[5:]
serie_US.index  = pd.to_datetime(serie_US.index)
serie_US.index.name = 'data'
# serie_US['acumulados'] = np.cumsum(serie_US.US)
serie_US
```

      Country_Region
      US

      data

      2020-01-22
      1.0

      2020-01-23
      1.0

      2020-01-24
      2.0

      2020-01-25
      2.0

      2020-01-26
      5.0

      ...
      ...

      2020-03-30
      161807.0

      2020-03-31
      188172.0

      2020-04-01
      213362.0

      2020-04-02
      243453.0

      2020-04-03
      275582.0

73 rows × 1 columns

```python
# outros = pd.DataFrame(data=)
suecia = confirmed.loc['Sweden'][2:]
alemanha = confirmed.loc['Germany'][2:]
espanha = confirmed.loc['Spain'][2:]
italia = confirmed.loc['Italy'][2:]
outros = pd.concat([suecia,alemanha,espanha,italia], axis=1)
outros.index = pd.to_datetime(outros.index)
# outros['Sweden_acc'] = np.cumsum(outros.Sweden)
# outros['Germany_acc'] = np.cumsum(outros.Germany)
# outros['Spain_acc'] = np.cumsum(outros.Spain)
# outros['Italy_acc'] = np.cumsum(outros.Italy)
outros
```

      Sweden
      Germany
      Spain
      Italy

      2020-01-22
      0.0
      0.0
      0.0
      0.0

      2020-01-23
      0.0
      0.0
      0.0
      0.0

      2020-01-24
      0.0
      0.0
      0.0
      0.0

      2020-01-25
      0.0
      0.0
      0.0
      0.0

      2020-01-26
      0.0
      0.0
      0.0
      0.0

      ...
      ...
      ...
      ...
      ...

      2020-03-30
      4028.0
      66885.0
      87956.0
      101739.0

      2020-03-31
      4435.0
      71808.0
      95923.0
      105792.0

      2020-04-01
      4947.0
      77872.0
      104118.0
      110574.0

      2020-04-02
      5568.0
      84794.0
      112065.0
      115242.0

      2020-04-03
      6131.0
      91159.0
      119199.0
      119827.0

73 rows × 4 columns

```python
fig,ax = subplots(1,1,figsize=(15,8))
df_brasil[df_brasil.casosAcumulados>0].casosAcumulados.plot(ax=ax, style='-o',
                                                            label='Brasil',
                                                            grid=True,
                                                            logy=True)
serie_US.US.plot(ax=ax, style='-v', grid=True,
                        label='EUA',);
outros.Sweden.plot(ax=ax, style='-v', grid=True,
                        label='Suécia')
outros.Germany.plot(ax=ax, style='-v', grid=True,
                        label='Alemanha')
outros.Spain.plot(ax=ax, style='-v', grid=True,
                        label='Espanha')
outros.Italy.plot(ax=ax, style='-v', grid=True,
                        label='Italia')
ax.legend();
```

```python
fig,ax = subplots(1,1,figsize=(15,8))
alinhados = pd.concat([suecia[suecia>100].reset_index(),
           alemanha[alemanha>100].reset_index(),
           espanha[espanha>100].reset_index(),
           italia[italia>100].reset_index()],
          axis=1)
alinhados['EUA'] = serie_US[serie_US.US>100].reset_index().US
alinhados['Brasil'] = df_brasil[df_brasil.casosAcumulados>100].reset_index().casosAcumulados
alinhados.plot(ax=ax,logy=True, grid=True);
plt.savefig('export/Brasil_vs_outros.png', dpi=300)
alinhados.to_csv('export/Brasil_vs_outros.csv')
```

```python
serie_US[serie_US.acumulados>100].reset_index().acumulados
```

    0         109.0
    1         120.0
    2         131.0
    3         142.0
    4         154.0
    5         166.0
    6         179.0
    7         192.0
    8         205.0
    9         218.0
    10        231.0
    11        244.0
    12        257.0
    13        270.0
    14        285.0
    15        300.0
    16        315.0
    17        330.0
    18        345.0
    19        360.0
    20        376.0
    21        392.0
    22        416.0
    23        446.0
    24        499.0
    25        572.0
    26        676.0
    27        848.0
    28       1065.0
    29       1401.0
    30       1851.0
    31       2365.0
    32       3073.0
    33       4178.0
    34       5735.0
    35       7882.0
    36      10739.0
    37      13657.0
    38      17964.0
    39      24060.0
    40      32933.0
    41      46957.0
    42      66187.0
    43      91814.0
    44     125435.0
    45     169098.0
    46     222834.0
    47     288612.0
    48     372448.0
    49     474105.0
    50     595583.0
    51     736469.0
    52     898276.0
    53    1086448.0
    54    1299810.0
    55    1543263.0
    Name: acumulados, dtype: float64

```python
1-timedelta(32)
```

```python
pd.date_range(df_brasil[df_brasil.casosAcumulados>0].index.min()-timedelta(offset),periods=dias,freq='D')
```

    DatetimeIndex(['2020-01-22', '2020-01-23', '2020-01-24', '2020-01-25',
                   '2020-01-26', '2020-01-27', '2020-01-28', '2020-01-29',
                   '2020-01-30', '2020-01-31',
                   ...
                   '2021-02-15', '2021-02-16', '2021-02-17', '2021-02-18',
                   '2021-02-19', '2021-02-20', '2021-02-21', '2021-02-22',
                   '2021-02-23', '2021-02-24'],
                  dtype='datetime64[ns]', length=400, freq='D')

```python

```


# barrier_reach_avoid

Synthesize a probabilistic barrier certificate to get a lower bound on the probability that a target set is reached.
## Mathematics

Consider the polynomial dynamics

$$
x(t+1) = f(x(t), t, w),  \quad  w \sim \mathcal N(0, \Sigma)
$$

and the sets

\begin{align}
X = \{x : g(x) \geq 0 \} \\
X_0 = \{x : g_0(x) \geq 0\} \\
X_T = \{x : g_1(x) \geq 0\} \\
X_U = \{x : g_U(x) \geq 0\}
\end{align}

**Remark**: for numeric reasons it is preferable that all these sets are compact so that restrictions are only imposed on bounded sets.

We want to find a lower bound on the property that we avoid $X_U$ and reach $X_T$ given that we start in $X_0$ and remain in $X$ during the whole transition.

$$
p_{01} = \mathbb{P} \left[ x(T) \in X_1 \; \land \; x(t) \not \in X_U, t=1, \ldots, T-1 \; \mid \; x(0) \in X_0, x(t) \in X \right]
$$

Consider a certificate $B(x, t)$ satisfying

\begin{align}
    B(x, t) \geq 0        \quad & x \in X  \\
    B(x, 0) \leq \gamma   \quad & x \in X_0     \\
    B(x, t) \geq 1        \quad & x \in X_U, \; t = 1, \ldots, T-1 \\
    B(x, T) \geq 1        \quad & x \not \in X_T, x \in X\\
    B(x(t), t) \leq \mathbb{E} \left[ B(x(t+1), t+1)) \; \mid \; x(t) \right]  \leq B(x(t), t) + c   \quad & x \in X
\end{align}

It obviously follows that with $B(t) = B(x(t), t)$ the last constraint implies $\mathbb{E} \left[ B(T) \right] < B(0) + cT$. Since $B$ is a positive submartingale we can use Doob's inequality

$$
\mathbb{P} \left[ \sup_{0 \leq t \leq T} B(t) \leq 1 \right] = 1 - \mathbb{P} \left[ \sup_{0 \leq t \leq T} B(t) \geq 1 \right] \geq 1 - \mathbb{E} [B(T)] \geq 1 - B(0).
$$

If $A \impliedby B$ and $C \implies D$, then $\mathbb{P}(A \mid C) \geq \mathbb{P}(B \mid D)$. From the constraints we have that

\begin{align}
    x(T) \in X_T \; \land \; x(t) \not \in X_U \impliedby sup_{0 \leq t \leq T} B(t) \leq 1, \\
    x(0) \in X_0 \implies B(0) \leq \gamma.
\end{align}

Therefore,
$$
p_{01} = \mathbb{P} \left[ x(T) \in X_T \; \land \; x(t) \not \in X_U, t=1, \ldots, T-1 \; \mid \; x(0) \in X_0, x(t) \in X \right] \geq \mathbb{P}\left[ B(T) \leq 1 \; \mid \; B(0) \leq \gamma \right] \geq 1 - (cT + \gamma).
$$

Thus we search for a barrier certificate that satisfies the conditions above while minimizing $cT + \gamma$.

```python
from sympy.abc import x, y, w
import posipoly as pp
import matplotlib.pyplot as plt
import numpy as np

# parameters
n = 2          # dimension
T = 4          # time horizon
sigma = 0.1    # noise standard deviation
tot_deg = 8    # overall degree of ppp

# polynomials defining sets
g  = pp.Polynomial.from_sympy(1, [x,y])
g0 = pp.Polynomial.from_sympy(0.25**2 - x**2 - y**2, [x,y])
gT = pp.Polynomial.from_sympy(0.5**2 - (x-1)**2 - y**2, [x,y])
gu = pp.Polynomial.from_sympy(0.2**2 - (x-0.4)**2 - (y-0.5)**2, [x,y])

# dynamics:
#  x(t+1) = x(t) + 0.25 + w
#  y(t+1) = y(t)
ft_x = pp.Polynomial.from_sympy(x + 0.25 + w, [x,y,w])
ft_y = pp.Polynomial.from_sympy(y + w, [x,y,w])

# Plot the sets
xx = np.linspace(-0.5, 1.5, 50)
yy = np.linspace(-1, 1, 50)

XX, YY = np.meshgrid(xx, yy)

g0_val = np.vectorize(g0)(XX, YY)
gu_val = np.vectorize(gu)(XX, YY)
gT_val = np.vectorize(gT)(XX, YY)

plt.figure()

plt.contour(XX, YY, g0_val, levels=[0], colors='black', linestyles='dotted')
plt.contour(XX, YY, gu_val, levels=[0], colors='red', linestyles='dotted')
plt.contour(XX, YY, gT_val, levels=[0], colors='black', linestyles='dotted')

plt.show()
```

    <Figure size 640x480 with 1 Axes>

We synhesize a separate barrier for each discrete time step, i.e. $B(x,t) = B_t(x)$. Using the S procedure we can rewrite the problem above as

\begin{align}
    \mathbb{E} [B_{t+1}(f(x, t, w)) ]  - B_t(x) - \sigma_{1}^t(x) g(x) \geq 0, & \qquad  t=0, \ldots, T-1 \\
    c + B_t(x) - \mathbb{E} [B_{t+1}(f(x, t, w)) ] - \sigma_{2}^t(x) g(x) \geq 0, & \qquad  t=0, \ldots, T-1 \\
    \gamma - B_0(x) - \sigma_{3}(x) g_0(x) \geq 0, \\
    B_t(x) - 1 - \sigma_{4}^t(x) g_U(x)      \geq 0, & \qquad  t=1, \ldots, T-1 \\
    B_T(x) - 1 + \sigma_{5a}(x) g_T(x) - \sigma_{5b}(x)g(x)      \geq 0, & \\
    B_t(x) - \sigma_6(x) g(x)              \geq 0, & \qquad  t=0, \ldots, T \\
    \sigma_1^t(x), \sigma_2^t(x) \geq 0,    & \qquad  t=0, \ldots, T-1 \\
    \sigma_4^t(x) \geq 0, & \qquad t = 1, \ldots, T-1 \\
    \sigma_6^t(x) \geq 0,    & \qquad  t=0, \ldots, T \\
    \sigma_3(x), \sigma_{5a}(x), \sigma_{5b}(x) \geq 0,    & \qquad  t=0, ..., T
\end{align}

```python
deg_B = tot_deg   # need to subtract if deg f larger than 1
deg_g0 = tot_deg-g0.d
deg_gT = tot_deg-gT.d
deg_gu = tot_deg-gu.d
deg_g = tot_deg-g.d

prog = pp.PPP()

prog.add_var('c', n, 0, 'coef')      # scalar variable = polynomial of degree 0
prog.add_var('gamma', n, 0, 'coef')  # scalar variable

for t in range(T+1):
    prog.add_var('B{}'.format(t), n, deg_B, 'coef')

for t in range(T):
    prog.add_var('s1{}'.format(t), n, deg_g, 'pp')
    prog.add_var('s2{}'.format(t), n, deg_g, 'pp')

prog.add_var('s3', n, deg_g0, 'pp')

for t in range(1, T):
    prog.add_var('s4{}'.format(t), n, deg_gu, 'pp')

prog.add_var('s5a', n, deg_gT, 'pp')
prog.add_var('s5b', n, deg_g, 'pp')

for t in range(T+1):
    prog.add_var('s6{}'.format(t), n, deg_g, 'pp')
```

Prepare the transformations

```python
# Identity for scalars to tot_deg
T1 = pp.PTrans.eye(n0=n, d0=0, n1=n, d1=tot_deg)

# Identity for scalars to scalar
T10 = pp.PTrans.eye(n0=n, d0=0, n1=n, d1=0)

# B(x,y) -> E_w[ B(fx(x,y,w), fy(x,y,w)) ]
TBp = pp.PTrans.gaussian_expectation(n0=3, d0=deg_B, i=2, sigma=sigma) \
      * pp.PTrans.composition(n0=n, d0=deg_B, g_list=[ft_x, ft_y])

# Identity for B
TB  = pp.PTrans.eye(n, deg_B)

# Multiplication with g, g0, g1, gu
Tg  = pp.PTrans.mul_pol(n, deg_g, g)
Tg0 = pp.PTrans.mul_pol(n, deg_g0, g0)
TgT = pp.PTrans.mul_pol(n, deg_gT, gT)
Tgu = pp.PTrans.mul_pol(n, deg_gu, gu)
```

These are the constraints in algebra form

    Tbp.B(t+1) - TB.Bt - Tg.s1t        pp    t=0, ..., T-1    (1)
    T1.c + TB.Bt - Tbp.B(t+1) - Tg.s2t pp    t=0, ..., T-1    (2)
    T1.gamma - TB.B0 - Tg0.s3          pp                     (3)
    TB.Bt - Tgu.s4t - 1                pp    t=1, ..., T-1    (4)
    TB.BT + TgT.s5a - Tg.s5b - 1       pp                     (5)
    TB.Bt - Tg.s6                      pp    t=0, ..., T      (6)

```python
# add (1)
for t in range(T):
    Aop = {'B{}'.format(t+1): TBp,
           'B{}'.format(t): -TB,
           's1{}'.format(t): -Tg}
    prog.add_constraint(Aop, pp.Polynomial.zero(n), 'pp')

# add (2)
for t in range(T):
    Aop = {'c': T1,
           'B{}'.format(t): TB,
           'B{}'.format(t+1): -TBp,
           's2{}'.format(t): -Tg}
    prog.add_constraint(Aop, pp.Polynomial.zero(n), 'pp')

# add (3)
prog.add_constraint({'gamma': T1, 'B0': -TB, 's3': -Tg0}, pp.Polynomial.zero(n), 'pp')

# add (4)
for t in range(1, T):
    prog.add_constraint({'B{}'.format(t): TB, 's4{}'.format(t): -Tgu,},
                        pp.Polynomial.one(n), 'pp')

# add (5)
prog.add_constraint({'B{}'.format(T): TB, 's5a': TgT, 's5b': -Tg}, pp.Polynomial.one(n), 'pp')

# add (6)
for t in range(T+1):
    Aop = {'B{}'.format(t): TB,
           's6{}'.format(t): -Tg}
    prog.add_constraint(Aop, pp.Polynomial.zero(n), 'pp')

# add inequality: gamma <= 1  (to exclude large solutions)
prog.add_constraint({'gamma': T10}, pp.Polynomial.one(n), 'iq')

# set c=0
# prog.add_row({'c': T10}, pp.Polynomial.zero(n), 'eq')

# add objective
prog.set_objective({'c': pp.PTrans.eval0(n, 0)* T, 'gamma': pp.PTrans.eval0(n, 0)})
```

Time to solve it!

```python
sol, status = prog.solve('sdd')

c = prog.get_poly('c')(0,0)
gamma = prog.get_poly('gamma')(0,0)

lb = 1-(gamma+c*T)

print('got c={:.2f} and gamma={:.2f}'.format(c, gamma))
print('lower bound is {:.2f}'.format(lb))
```

    optimizing...
    solved in 2.19s
    solsta.optimal
    got c=0.00 and gamma=0.80
    lower bound is 0.20

From the certificates we can now plot a lower bound on the probability that the target set is reached. From the proof above it follows that

$$
\mathbb{P}(x(T) \in X_1 \mid x(t) = x) \geq 1 - (c(T-t) + B_t(x)).
$$

```python
%matplotlib notebook
from matplotlib.pyplot import cm

xx = np.linspace(-0.5, 1.5, 50)
yy = np.linspace(-0.8, 0.8, 50)

XX, YY = np.meshgrid(xx, yy)

g0_val = np.vectorize(g0)(XX, YY)
gu_val = np.vectorize(gu)(XX, YY)
gT_val = np.vectorize(gT)(XX, YY)

levels = sorted([0, 0.4, 0.8, 0.95, 0.99, lb])

for t in [0, 1, 2, 3, 4]:
    Bt = prog.get_poly('B{}'.format(t))
    proba_min = 1 - (np.vectorize(Bt)(XX, YY) + c * (T-t))

    plt.figure()
    CS = plt.contour(XX, YY, proba_min, levels=levels, cmap=cm.get_cmap('plasma'), vmin=0, vmax=1)
    plt.clabel(CS, inline=1, fontsize=10)

    plt.contour(XX, YY, g0_val, levels=[0], colors='black', linestyles='dotted')
    plt.contour(XX, YY, gu_val, levels=[0], colors='red',   linestyles='dotted')
    plt.contour(XX, YY, gT_val, levels=[0], colors='black', linestyles='dotted')
    plt.title('t={}'.format(t))

plt.show()
```

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>


# 6. Unsupervised Learning

```python
%matplotlib inline
```

**Clustering** (and outlier detection) and dimension reduction. Focus on former in this unit.

Points are represented in high-dim Euclidean spaces or any metric spaces.

### Distance functions
 * $d : D \times D \rightarrow [0, \infty)$
 * $d(x, y) \ge 0, \> \forall x, y \in D$
 * $d(x, x) = 0, \> \forall x \in D$
 * $d(x, y) = d(y, x), \> \forall x, y \in D$
 * $d(x, y) + d(y, z) \ge d(x, z), \> \forall x, y, z \in D$ (Triangle inequality)
 * **Bonus** if $d(x, y) = 0 \implies x = y, \> \forall x, y \in D$, then we say that $D$ is a **metric space**.

Note: it seems certain authors simply ignore distance functions without the identity of indiscernables and also call metrics distance functions (e.g. in ML slides).

## Types of clustering

### Hierarchical clustering
Build a tree representing distances among the data points.

Each point starts by being its own cluster, and then cluster keep getting combined based on their "closeness", until a certain threshold is reached (e.g. desired cluster count or compactness). Alternatively, one can keep going until only a single "cluster" exists, and return a hierarchy of clusters.

Other merging criteria:

 - minimum distance between clusters, where that distance is minimum distance between any points of each cluster
 - cluster radius (merge clusters resulting in the smallest-radius cluster) or diameter.

Other stopping criteria:

 - a new best cluster's diameter exceeds some threshold
 - a new best cluster's density exceeds some threshold (density = e.g. nr points / volume)
 - a new best cluster is considered bad (sudden jump in average diameter, compared to previous steps)

Picking a clustroid in non-Euclidean spaces: a point which minimizes:

 - $\sum$ of distances to other points in cluster
 - max distance to another point in cluster
 - $\sum$ of square distances

Overall, not that many things change in non-Euclidean spaces.

*Examples:* single-, average-linkage **agglomerative clustering**.

### Partitional approaches
Define and optimize an objective function defined over partitions.
TODO: What are partitions? Nothing in textbook

*Examples:* spectral clustering, graph-cut based approaches.

### Model-based approaches (main focus)
Maintain cluster "models" and infer cluster membership. Point assignment techniques. Viewed as the "standard" clustering methods.

*Examples:* k-means, BFR, CURE, Gaussian mixture models (GMMs), etc.

### Hybrid

The GRGPF algorithm handles non-main-memory data and does not require a Euclidean space. It takes ideas from both hierarchical and point-assignments (model-based) techniques: it represents clusters by sample points in main memory, but also tries to organize them hierarchically.

GRGPF: the donut technique: in a cluster, keep track of the clustroid, the k-closest, and k-furthest elements.

## Other categorization

### Euclidean space assumption
Some algorithms only work in Euclidean spaces, whereas others work with any distance measure.

Key difference: Euclidean spaces allow expressing clusters as **centroids** (average of points). This is not possible in non-Euclidean spaces; instead, we use **clustroids**, which are actual points from the dataset (which we consider representative).

### Scale assumption
Whether the algorithm assumes the data fits in main or just secondary memory. If the latter is true, we need to take certain shortcuts and summarize our clusters in memory.

## The curse of dimensionality
In high dimensional spaces (especially but not necessarily Euclidean spaces) almost all pairs of points are equally far away from each other and almost any two vectors are orthogonal (the cosine of their angle is almost always zero, since the $\cos$ denominator grows linearly in $d$, but the numerator doesn't).

This is problematic for clustering, as it becomes very hard to build clusters when there are essentially no pairs of points which are close.

### Example: Volume of a high-dimensional sphere

Consider a sphere of radius $r = 1$ in $D$-dimensional space. $V_{D}(r) = K_{D}r^{D}$, with $K_D$ being some constant (e.g. $\frac{4}{3}\pi$ in $\mathbb{R}^{2}$).

What is the volume of the sphere that lies between radius $r = 1 - \epsilon$ and $r = 1$? (i.e. its outer shell).

\begin{equation}
\begin{aligned}
    \frac{V_D(1) - V_D(1 - \epsilon)}{V_D(1)} & = \frac{K_D - K_D(1 - \epsilon)^{D}}{K_D} \\
    & = 1 - (1 - \epsilon)^{D} \xrightarrow {D\to\infty} 1
\end{aligned}
\end{equation}

 > "Thus, in spaces of high dimensionality, most of the volume of a sphere is concentrated in a thin shell near the surface!"
 >
 > -- Bishop, PRML

The following example is based on the second data mining homework, and implements a simple form of cosine hashing, where a 1-bit hash function consists of the sign of a dot product between a to-be-hashed vector and the randomly vector which characterizes the hash function.

It can be seen that as the number of dimensions increases, the utility of the hash functions drops, as most vector pairs become orthogonal (normalized "angle" 0.5).

```python
import random
import math

import numpy as np
import matplotlib.pyplot as plt

def rand_vector_uniform(dimension):
    v = [random.uniform(-1, 1) for _ in range(dimension)]
    return normalize(v)

def norm(v):
    return math.sqrt(sum([x * x for x in v]))

def normalize(v):
    nrm = norm(v)
    return [x / nrm for x in v]

def angle(v1, v2):
    assert len(v1) == len(v2)
    return math.acos(dot_product(v1, v2) / (norm(v1) * norm(v2)))

def random_hash_function(dimension):
    h = rand_vector_uniform(dimension)
    def hash_function(vector):
        return math.copysign(1.0, np.inner(vector, h))
    return hash_function

def hash_agree(v1, v2, hashes):
    return len([h for h in hashes if h(v1) == h(v2)])

def p_same_hash(N, M, d):
    """N vector pairs, M hash functions, d dimensions."""
    vecgen = rand_vector_uniform
    vector_pairs = [(vecgen(d), vecgen(d)) for _ in range(N)]
    hashes = [random_hash_function(d) for _ in range(M)]

    angle_coefs = [1 - angle(v1, v2) / math.pi for (v1, v2) in vector_pairs]
    probabilities = [hash_agree(v1, v2, hashes) * 1.0 / M for (v1, v2) in vector_pairs]
    y = angle_coefs
    x = probabilities

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    ax.scatter(x, y, label="foo")
    ax.set_xlabel("Normalized angle (1 - angle(u, v) / pi)")
    ax.set_ylabel("Probability of u, v hashing to the same bucket")
    ax.set_ylim([0, 1])
    ax.set_xlim([0, 1])
    fig.text(0.5, 0.85, "Dimensions: %d, N = %d, M = %d" % (d, N, M), ha='center')
    # fig.savefig("plot-%d-%d-%d.png" % (N, M, d))
    plt.show()
```

```python
p_same_hash(200, 100, 2)
```

```python
p_same_hash(200, 100, 10)
```

```python
p_same_hash(200, 100, 100)
```

## The k-means problem
 * Points in *Euclidean space*, $x_i \in \mathbb{R}^d$.
 * Clusters as centers $\mu_j \in \mathbb{R}^d$; need to know cluster count in advance.
 * Each point assigned to closest center.
 * **Goal**: pick centers to minimize average squared distance:

\begin{equation}
\begin{aligned}
L(\mu) & = L(\mu_1, \dots, \mu_k) = \sum_{i = 1}^{N} \min_{j} \| x_i - \mu_j \|_2^2 \\
\mu^{*} & = \arg \min_{\mu} L(\mu)
\end{aligned}
\end{equation}

NP-hard to solve, so we use **Lloyd's heuristic**, commonly (though not entirely accurately) referred to as the k-means algorithm. Even this heuristic can take exponential time in $\mathbb{R}^{2}$ with specially crafted input!

The feasible space for k-means is always $\mathbb{R}^{d \> \times \> k}$ (no projection possible).

Can pick an optimal $k$ using a binary-search technique. We search for the largest value of $k$ for which a decrease below k clusters results in a radically higher average diameter of the clusters.

### The algorithm:

* Initialize cluster centers (randomly or in a smarter way): $\mu^{(0)} = [\mu_1^{(0)}, \dots, \mu_k^{(0)} ]$
* Assign every point to closest cluster: $z_i = \arg \min_{j} \| x_i - \mu_j^{t - 1} \|_2^2$
* Update cluster centers to be at the center of the newly updated cluster: $\mu_j^{(t)} = \frac{1}{n_j} \sum_{i:z_i = j} x_i$
* Repeat until convergence (e.g. minimum cluster center movement threshold).

### Properties
 * Guaranteed (can be shown) to *monotonically decrease average squared distance* in each iteration: $L(\mu^{t+1}) \le L(\mu^{t})$
 * Converges to *local* optimum; in practice, we do multiple restarts so that we minimize the chance to get stuck in a local optimum.
 * $O(nkd)$ per iteration ($n$ elements, $k$ clusters, $d$ dimensions); have to process entire data set in every iteration, so difficult to parallelize.

### Initializing the clusters

Want to pick points that have a good chance of lying in different clusters. Two approaches:

1. Pick points that are far away from each other.
    * Pick first point at random, and then sequentially pick the farthest point from the already-picked points.
    * There are better way to do this; see coresets below!

2. Cluster a subsample of the data (e.g. using hierarchical clustering) until we get k clusters, and use their centroids as initial k-means centroids.

TODO: k-means++

### The BFR algorithm

Variant of k-means designed for high-dim Euclidean spaces.

Makes strong assumptions about the clusters--they must be normally distributed around a centroid, but is capable of streaming through data (though not in a parallel or distributed fashion).

Keep track of "lite" representation of data in memory:

 * *The Discard Set*: highly summarized information on points which clearly belong to a cluster. The points themselves are not retained, hence the name.
 * *The Compressed Set*: summaries for groups of points found close to each other, but not to any cluster. Also called miniclusters.
 * *The Retained Set*: outliers; stored uncompressed in main memory until they can be shoved into one of the previous two categories.

*Image credit: Rajaraman, Anand, and Jeffrey D. Ullman. Mining of massive datasets. Vol. 77. Cambridge: Cambridge University Press, 2012.*

## Clustering a stream (mostly from book)

Sliding window approach (of size $N$). Ask for centroids for last $m \le N$ points.

Idea: precluster subsets of points so that we can quickly answer queries.

### The BDMO Algorithm
 * Based on technique for counting items in stream (DGIM algorithm for counting 1's in the sliding window of a stream).
 * Uses buckets over time, each with its own clusters.

### Parallel environments
 * Can use MapReduce, but we're almost always constrained to a single reducer.
 * Coreset merging can be hierarchical; TODO: elaborate
 * General idea:
     - mappers apply clustering on partitions of the data and output their clusters
     - reducer merges these clusters (e.g. by running a clustering algorithm again)

## Online k-means

Online SVM (& SGD) had the property that theirr loss function *decomposed additively* over the data set; the total loss is just a sum of individual hinge loss funtion values, based on the model $w$.

\begin{equation}
    L(w) = \sum_{t=1}^{N} \operatorname{hinge}(x_t; y_t, w)
\end{equation}

This meant that we could take a sub-gradient for every data point and iteratively update our model:

\begin{equation}
    w^{(t+1)} = \operatorname{Proj}_{S}\left(w^{(t)} - \eta_t \nabla f_t(w_t)\right)
\end{equation}

For k-means, our loss function is also just a sum of "individual data point penalties". I.e., it also decomposes additivel over the data set.

Note: the order of $x$ and $\mu$ doesn't matter, since the one we're deriving by will always be first in the derivative formula (calculus 101, but I got confused by it for some reason).

\begin{equation}
    L(\mu) = \sum_{t=1}^{N} \min_j \| x_t - \mu_j \|_2^2 = \sum_{t=1}^{N} l_t(\mu)
\end{equation}

We differentiate by $\mu$ since it is the thing we want to optimize (change). We obviously can't compute any gradient with respect to $x$, since we can't "change" the data.

\begin{equation}
    \frac{\partial}{\partial \mu_j} l_t(\mu) = 2(\mu_j - x_t)
\end{equation}

Question: dm-10:11, does the order of the arguments inside $\| \cdot \|$ matter? Should it? Nope. The first, positive, term is always going to be the one we're deriving by.

Our function is not diffable all the way, since it has discontinuities (see full definition in slides), but we can still use a *subgradient* to implement online k-means.

### The algorithm
 * Initialize centers randomly
 * For t = 1:N
     * Find closest cluster: $c = \arg \min_j \| \mu_j - x_t \|_2$
     * Move that cluster slightly towards the current point: $\mu_c \leftarrow \mu_c + \eta_t (x_t - \mu_c)$

Only requirement to converge (to local optimum): $\sum_t \eta_t = \infty$, $\sum_t\eta_t^2 < \infty$. $\eta_t = \frac{1}{t}$ is a good option.

Remember: $\sum_{n=1}^{\infty} \frac{1}{n} = \infty$ BUT $\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} < \infty$.

### Practical aspects
 * Random ordering is best (like in SGD)
 * Want to first choose larger $k$, then drop/merge small clusters
 * Run multiple random "restarts" in parallel, by keeping track of several cluster sets at once (initialized differently)

However, this algorithm still doesn't lend itself to parallelization very nicely. Moreover, the objective function is non-convex, unlike SGD. We *can* get stuck in local optima.

## Coresets

Alternative to online k-means: summarize large data sets, and *then* run k-means on the summary.

Key idea: represent many points by one weighted representative. Our k-means objective now becomes ($C$ is the summarized version of our data $D$):

\begin{equation}
    L_k(\mu; C) = \sum_{(w, x) \in C} w \min_{j} \| \mu_j - x \|_2^2
\end{equation}

$C$ is called a $(k, \epsilon)$-coreset for data set $D$ if ($k$ clusters, $\epsilon$ error threshold vs. the clustering for the whole dataset):

\begin{equation}
    (1 - \epsilon)L_k(\mu; D) \le L_k(\mu; C) \le (1 + \epsilon)L_k(\mu; D)
\end{equation}

### Constructing a coreset
Uniform sampling fails for imbalanced data, which is very common.

Use **importance sampling** instead (do not confuse with *uncertainty sampling* from active learning).

Key idea: interpret k-means cost function as expectation, i.e. a sum  over products between some function value (a point's distance to the clusters) and a probability (uniform and boring $\frac{1}{N}$ if we take all our points into consideration, which is the default in the original k-means problem statement; **this is the insight** -- we can sample much fewer points than $N$ according to some other (smarter) distribution and they'll still approximate our $L$ well.

So what distribution to use?

We want to be biased towards small clusters, so we don't miss those points like we would be likely to if we were doing uniform sampling. And to compensate for this bias, we also want to give them a small weight (weight $\gamma \propto \frac{1}{q}$).

This also correspons to the mathematical consequence of treating or loss function as an expectation and swapping our uniform sampling with something else. We had:

\begin{equation}
 L(\mu; D) \approx \frac{1}{m} \sum_{l=1}^{m} \frac{d(x_{i_l}, \mu)}{q(i_l)}
\end{equation}

When sampling according to distribution $q$, we have to give every term $i$ a weight of $\frac{1}{q(i)}$ in order to correctly approximate $L(\mu; D)$.

### Are coresets really better than uniform sampling?

We can verify by computing the reduction in $L$'s variance we get by using $C$, our smart importance-sampled coreset, versus $U$, an uniformly-sampled "coreset".

(see slides)

Want to maximize $q(x_i)$ for the sampled points in order to reduce variance.

Need to reduce variance for all clusterings simulatenously? (TODO: why?)

**Sensitivity** is the worst case (max) effect a data point can have on *any* possible clustering (max over $\mu$).

We want to sample the most sensitive points, since they are likely to have the highest impact on the solution. However, *sensitivity is hard to compute*.

TODO: wait, both q and s are distributions?

But we can show that just sampling "enough" points from $q$ is good enough to construct a coreset.

TODO: info on bicriteria approximation.

Algorithm:
 * Repeat until no more points left
     * Sample a small set uniformly at random
     * Remove half of points nearest the samples
 * Partition data into Voronoi diagram centered at selected points
 * Construct $q$ by giving weights to points based on:
     * Points in sparse cells get more mass
     * Points far from cell center get more mass
 * Sample elements from $D$ using $q$

Coresets have multiple applications: k-means, k-line means, PageRank, SVMs, etc.

## Coreset composition

### Merge
The union of two $(k, \epsilon)$-coresets is a $(k, \epsilon)$-coreset.

### Compress
A  $(k, \delta)$ corest of a $(k, \epsilon)$-coreset is a $(k, \epsilon + \delta + \epsilon\delta)$-coreset.

Can merge & compress online. Error grows linearly with number of compressions. Can instead merge coresets as trees (error growth $\propto$ size of tree). Use more memory but lower error.

Can construct k, e coresets in parallel. And use 1 or more reduce stages to merge (and, optionally, compress) them.

```python

```


# Calcul Numeric - Subiecte de examen

**Notă**: formulele de $\LaTeX$ nu se văd prea bine în viewer-ul de pe GitHub, e mai bine să descărcați acest document local.

```python
import math
import numpy as np
import scipy.spatial
import matplotlib.pyplot as plt
```

## Exercițiul 1

Metoda bisecției - secțiunea I.1 din [cursul 1](https://drive.google.com/file/d/1hUFKessAxlGIT_gNFlH7-4yyuyQ_4vFe/view)

### a)

Pentru a găsi intervalele de monotonie ale lui $f(x) = x^3 - 4x + 1$, calculăm derivata funcției: $f'(x) = 3x^2 - 4$.

Vedem că derivata se anulează în $\pm \frac{2 \sqrt{3}}{3}$. Deci intervalele de monotonie sunt:
- $\left(-\infty, -\frac{2 \sqrt{3}}{3}\right)$
- $\left(-\frac{2 \sqrt{3}}{3}, +\frac{2 \sqrt{3}}{3}\right)$
- $\left(+\frac{2 \sqrt{3}}{3}, +\infty\right)$

### b)

Observăm că $f\left(+ \frac{2 \sqrt{3}}{3}\right) < 0$, și de exemplu $f(4) > 0$. Deci în acest interval funcția are cel puțin o soluție reală. Dar deoarece este și monotonă pe $\left(\frac{2 \sqrt{3}}{3}, +\infty\right)$, această soluție este unică.

### c)

```python
def bisection_search(f, a, b, epsilon=1e-3):
    # Calculăm valorile în capete
    f_a = f(a)
    f_b = f(b)

    # Prima estimare, mijlocul intervalului inițial
    x_num = (a + b) / 2

    # Numărul necesar de iterații
    num_iterations = math.floor(math.log2((b - a) / epsilon) - 1) + 1

    last_rel_error = math.inf

    # Aplicăm algoritmul
    for step in range(num_iterations):

        print(f"a = {a}, b = {b}, error = {last_rel_error}")
        value = f(x_num)

        # Am găsit fix valoarea căutată, ieșim
        if value == 0:
            break
        elif f_a * value < 0:
            b = x_num
        else:
            a = x_num

        x_num_vechi = x_num
        x_num = (a + b) / 2

        last_rel_error = abs((x_num - x_num_vechi)) / abs(x_num_vechi + epsilon)

    return x_num

f = lambda x: x ** 3 - 4 * x + 1
print("x1 =", bisection_search(f, -4, -1))
print("x2 =", bisection_search(f, -1, 1))
print("x3 =", bisection_search(f, 1, 4))
```

    a = -4, b = -1, error = inf
    a = -2.5, b = -1, error = 0.30012004801920766
    a = -2.5, b = -1.75, error = 0.2144082332761578
    a = -2.125, b = -1.75, error = 0.08827683615819208
    a = -2.125, b = -1.9375, error = 0.04841208365608055
    a = -2.125, b = -2.03125, error = 0.023088289619504987
    a = -2.125, b = -2.078125, error = 0.011283625203105253
    a = -2.125, b = -2.1015625, error = 0.005578862804605909
    a = -2.125, b = -2.11328125, error = 0.0027739558830056367
    a = -2.119140625, b = -2.11328125, error = 0.0013831411689202646
    a = -2.1162109375, b = -2.11328125, error = 0.0006925284490686877
    x1 = -2.115478515625
    a = -1, b = 1, error = inf
    a = 0.0, b = 1, error = 500.0
    a = 0.0, b = 0.5, error = 0.499001996007984
    a = 0.25, b = 0.5, error = 0.49800796812749004
    a = 0.25, b = 0.375, error = 0.16622340425531915
    a = 0.25, b = 0.3125, error = 0.09968102073365231
    a = 0.25, b = 0.28125, error = 0.05535872453498671
    a = 0.25, b = 0.265625, error = 0.029301453352086265
    a = 0.25, b = 0.2578125, error = 0.015092972711905336
    a = 0.25390625, b = 0.2578125, error = 0.007662130685300968
    x2 = 0.2548828125
    a = 1, b = 4, error = inf
    a = 1, b = 2.5, error = 0.2998800479808077
    a = 1.75, b = 2.5, error = 0.21416333523700745
    a = 1.75, b = 2.125, error = 0.08819379115710255
    a = 1.75, b = 1.9375, error = 0.04836213567191128
    a = 1.84375, b = 1.9375, error = 0.025409947147309934
    a = 1.84375, b = 1.890625, error = 0.012390140751998943
    a = 1.84375, b = 1.8671875, error = 0.006272791141146165
    a = 1.85546875, b = 1.8671875, error = 0.003156193714545424
    a = 1.85546875, b = 1.861328125, error = 0.001573131748735202
    a = 1.8583984375, b = 1.861328125, error = 0.0007878051957328269
    x3 = 1.860595703125

## Exercițiul 2

Metoda Newton-Rhapson - secțiunea I.2 din [cursul 1](https://drive.google.com/file/d/1hUFKessAxlGIT_gNFlH7-4yyuyQ_4vFe/view)

### a)

#### Formula

Formula pentru următorul din șirul aproximărilor este

$$x_k = x_{k-1} - \frac{f (x_{k-1})}{f'(x_{k - 1})}$$

#### Convergență

Fie $f \in C^2([a, b])$, $f'$ și $f''$ nu se anulează pe $[a, b]$ și $f(a) f(b) < 0$.

Fie $x_0 \in [a, b]$ astfel încât să aibă loc condiția

$$f(x_0) f''(x_0) > 0$$

atunci ecuația $f(x) = 0$ are o soluție unică $x^* \in (a, b)$ iar șirul $x_k$ construit prin metoda Newton-Rhapson converge la $x^*$.

### b)

Fie $f(x) = x^2 + 2 x − 1$, $f \colon [0.1, 2] \to \mathbb{R}$.

Atunci $f'(x) = 2x + 2$, $f''(x) = 2$.

Avem că:
- $f$ este de clasă $C^2$
- $f'$ și $f''$ nu se anulează pe $[0.1, 2]$
- $f(0.1) f(2) = -0.79 \cdot 7 < 0$

Dacă luăm $x_0 = 1$ avem $f(x_0) f''(x_0) = 2 \cdot 2 > 0$, deci ipotezele teoremei de convergență sunt satisfăcute.

### c)

```python
def newton_rhapson(f, df, x0, epsilon=1e-3):
    # Primul punct este cel primit ca parametru
    prev_x = x0
    # Aplicăm prima iterație
    x = x0 - f(x0) / df(x0)

    # Continuăm să calculăm până avem precizia cerută.
    last_rel_error = math.inf
    while abs(x - prev_x) / abs(prev_x) > epsilon:
        print(f"x = {x}, error = {last_rel_error}")
        x, prev_x = x - f(x) / df(x), x

        last_rel_error = abs((x - prev_x)) / abs(prev_x)

    return x

f = lambda x: x**2 + 2 * x - 1
df = lambda x: 2 * x + 2
x0 = 1
newton_rhapson(f, df, x0)
```

    x = 0.5, error = inf
    x = 0.4166666666666667, error = 0.16666666666666663
    x = 0.41421568627450983, error = 0.00588235294117645

    0.41421356237468987

## Exercițiul 3

Metoda lui Gauss - [cursul 3](https://drive.google.com/file/d/1huTen5Elw203cEOoQMRt1gchFciXlIXp/view)

```python
A1 = np.array([
    [1, 0, -2],
    [2, -2, 0],
    [1, 2, 1],
], dtype=np.float64)
b1 = np.array([
    [-5],
    [-6],
    [5]
], dtype=np.float64)

A2 = np.array([
    [2, -2, 1],
    [1, 3, -2],
    [3, -1, -1]
], dtype=np.float64)
b2 = np.array([
    [-3],
    [1],
    [2]
], dtype=np.float64)
```

```python
def compute_upper_triangular(A, b, partial_pivot=False, full_pivot=False):
    """Aduce un sistem la forma superior triunghiulară
    folosind Gauss cu/fără pivotare totală/parțială.
    """
    if partial_pivot and full_pivot:
        raise Exception("Se poate folosi doar un tip de pivotare")

    N = A.shape[0]

    # Formez matricea sistemului
    M = np.concatenate((A, b), axis=1)

    indices = np.arange(0, N)

    for k in range(N - 1):
        print(M)

        if partial_pivot:
            # Găsesc indicele elementului de valoare maximă
            index = np.argmax(M[k:, k])

            # Pivotez
            M[[k, index]] = M[[index, k]]
        elif full_pivot:
            submatrix = M[k:, k:N - 1]
            index = np.unravel_index(np.argmax(np.abs(submatrix)), submatrix.shape)

            # Obțin indicii în matricea mare
            index = (index[0] + k, index[1] + k)

            M[[k, index[0]]] = M[[index[0], k]]
            M[:, [k, index[1]]] = M[:, [index[1], k]]

            indices[[k, index[1]]] = indices[[index[1], k]]

        # Selectez coloana
        ratios = M[k + 1:, k]

        # Determin raportul pentru fiecare rând
        ratios = ratios / M[k, k]

        row = M[k, :]

        # Înmulțesc fiecare raport cu primul rând
        difference = np.outer(ratios, row)

        # Actualizez matricea
        M[k + 1:, :] -= difference

    return M, indices

def solve_upper_triangular(U, C, print_steps=False):
    """Rezolvă un sistem în formă superior triunghiulară,
    folosind substituția descendentă.
    """
    N = U.shape[0]

    x = np.zeros(N)

    # Merg de la ultima linie către prima,
    # și rezolv pe rând ecuațiile prin substituție
    for i in range(N - 1, -1, -1):
        coefs = U[i, i + 1:]
        values = x[i + 1:]

        x[i] = (C[i] - coefs @ values) / U[i, i]

        if print_steps:
            print("Pasul", N - i, ":",
                C[i], "-", coefs, "@", values, ")",
                "/", U[i, i])

    return x

def solve_system(A, b, pivot=None):
    "Rezolvă un sistem folosind metoda Gauss."

    determinant = np.linalg.det(A)

    if np.abs(determinant) < 1e-14:
        # Sistemul nu are soluție
        return None

    partial_pivot = False
    full_pivot = False

    if pivot == "full":
        full_pivot = True
    elif pivot == "partial":
        partial_pivot = True

    M, indices = compute_upper_triangular(A, b, partial_pivot=partial_pivot, full_pivot=full_pivot)

    N = A.shape[0]
    U = M[:,:N]
    C = M[:,N]

    x = solve_upper_triangular(U, C)
    x = x[indices]

    return x

print(solve_system(A1, b1, pivot='partial'))
print()
print(solve_system(A2, b2, pivot='full'))
```

    [[ 1.  0. -2. -5.]
     [ 2. -2.  0. -6.]
     [ 1.  2.  1.  5.]]
    [[ 2. -2.  0. -6.]
     [ 0.  1. -2. -2.]
     [ 0.  3.  1.  8.]]
    [-1.  2.  2.]

    [[ 2. -2.  1. -3.]
     [ 1.  3. -2.  1.]
     [ 3. -1. -1.  2.]]
    [[ 3.          1.         -2.          1.        ]
     [ 0.          2.66666667 -0.33333333 -2.33333333]
     [ 0.          3.33333333 -1.66666667  2.33333333]]
    [-1.4 -2.  -4.2]

## Exercițiul 4

Descompunerea LU - [cursul 4](https://drive.google.com/file/d/14jt-8XcekhX8CgMgMgtnwtDEpr_lzd02/view)

```python
A = np.array([
    [2, 3, 1],
    [4, 8, 4],
    [-4, 0, 10],
], dtype=np.float64)

b = np.array([
    [5],
    [18],
    [20],
], dtype=np.float64)
```

```python

N = A.shape[0]
P = np.eye(N)
L = np.zeros((N, N))
U = np.copy(A)

partial_pivot = True

for k in range(N - 1):
    if partial_pivot:
        # Găsesc indicele elementului de magnitudine maximă
        index = k + np.argmax(np.abs(U[k:, k]))

        # Pivotez
        U[[k, index]] = U[[index, k]]
        L[[k, index]] = L[[index, k]]

        # Interschimb în permutare
        P[[k, index]] = P[[index, k]]

    # Selectez coloana pe care lucrez
    ratios = U[k + 1:, k]

    # Determin raportul pentru fiecare rând
    ratios = ratios / U[k, k]

    # Actualizez matricea inferior triunghiulară
    L[k + 1:, k] = ratios

    # Selectez rândul pe care vreau să-l actualizez
    row = U[k, :]

    # Înmulțesc fiecare raport cu primul rând
    difference = np.outer(ratios, row)

    # Actualizez matricea superior triunghiulară
    U[k + 1:, :] -= difference

L += np.eye(N)

print("L = ")
print(L)
print("U = ")
print(U)
print("L @ U = ")
print(L @ U)
print("P @ A = ")
print(P @ A)
print()

print("Rezolv pentru b = ", b.T)

# Permut numerele din vector
b = P @ b

y = np.zeros(N)

# Merg de la prima linie în jos,
# și rezolv pe rând ecuațiile prin substituție
for i in range(0, N):
    coefs = L[i, :i + 1]
    values = y[:i + 1]

    y[i] = (b[i] - coefs @ values) / L[i, i]

print("Obțin y = ", y)

x = np.zeros(N)

# Merg de la ultima linie în sus,
# și rezolv pe rând ecuațiile prin substituție
for i in range(N - 1, -1, -1):
    coefs = U[i, i + 1:]
    values = x[i + 1:]

    x[i] = (y[i] - coefs @ values) / U[i, i]

print("Obțin x = ", x)
print("Verificare: A @ x = ", A @ x)
```

    L =
    [[ 1.     0.     0.   ]
     [-1.     1.     0.   ]
     [ 0.5   -0.125  1.   ]]
    U =
    [[ 4.    8.    4.  ]
     [ 0.    8.   14.  ]
     [ 0.    0.    0.75]]
    L @ U =
    [[ 4.  8.  4.]
     [-4.  0. 10.]
     [ 2.  3.  1.]]
    P @ A =
    [[ 4.  8.  4.]
     [-4.  0. 10.]
     [ 2.  3.  1.]]

    Rezolv pentru b =  [[ 5. 18. 20.]]
    Obțin y =  [18.   38.    0.75]
    Obțin x =  [-2.5  3.   1. ]
    Verificare: A @ x =  [ 5. 18. 20.]

## Exercițiul 5

```python
A = np.array([
    [4, 2, 4],
    [2, 2, 3],
    [4, 3, 14]
])
```

### a)

```python
def simetrica(M):
    "Verifică dacă matricea `M` este simetrică."
    return np.all(M == M.T)

simetrica(A)
```

    True

```python
def pozitiv_semidefinita(M):
    "Verifică dacă matricea M este pozitiv-semidefinită."
    for i in range(1, M.shape[0]+1):
        minor = M[:i, :i]
        # Dacă cel puțin un minor principal nu are determinantul nenegativ,
        # nu este pozitiv-semidefinită.
        if np.linalg.det(minor) < 0:
            return False
    return True

pozitiv_semidefinita(A)
```

    True

### b)

```python
def descompunere_cholesky(M):
    N = M.shape[0]

    L = np.eye(N)

    for i in range(N):
        L_i = np.eye(N)

        pivot = M[i, i]
        L_i[i:, i] = M[i:, i] / np.sqrt(pivot)

        M_nou = np.eye(N)
        outer = np.outer(M[i + 1:, i], M[i, i + 1:])
        M_nou[i + 1:, i + 1:] = M[i + 1:, i + 1:] - outer / pivot

        L = L @ L_i
        M = M_nou

    return L

L = descompunere_cholesky(A)
print(L)
```

    [[2. 0. 0.]
     [1. 1. 0.]
     [2. 1. 3.]]

### c)

Din descompunerea Cholesky am obținut $L$, și știm că $A = L L^T$.

Ecuația se rescrie

$$L L^T x = b$$

Deci putem rezolva mai întâi sistemul inferior triunghiular

$$L y = b$$

Și apoi cel superior triunghiular

$$L^T x = y$$

```python
b = np.array([
    [10],
    [6],
    [11]
])
```

```python
def substitutie_descendenta(U, b):
    N = b.shape[0]
    x = np.zeros(N)

    for i in range(N - 1, -1, -1):
        coefs = U[i, i + 1:]
        values = x[i + 1:]

        x[i] = (b[i] - coefs @ values) / U[i, i]

    return x

def substitutie_ascendenta(L, b):
    N = b.shape[0]
    x = np.zeros(N)

    for i in range(0, N):
        coefs = L[i, :i + 1]
        values = x[:i + 1]

        x[i] = (b[i] - coefs @ values) / L[i, i]

    return x

y = substitutie_ascendenta(L, b)
x = substitutie_descendenta(L.T, y)
print("x =",x)
```

    x = [2. 1. 0.]

## Exercițiul 6

Metoda Jacobi - [cursul 7](https://drive.google.com/file/d/1Dk_6UFiM674Zwdm6VRAc9-a1rVQAH7Mu/view) sau [pe Wikipedia](https://en.wikipedia.org/wiki/Jacobi_eigenvalue_algorithm)

```python
A = np.array([
    [4, 1, 1],
    [1, 4, 1],
    [1, 1, 4]
], dtype=np.float64)
```

```python
def gaseste_max_nediagonal(M):
    "Caută elementul de valoare absolută maximă care nu se află pe diagonala principală"

    # Matricea e simetrică, suficient să caut sub diagonala principală
    sub_diagonala = np.tril(M, k=-1)

    # Caut elementul de magnitudine maximă
    index = np.argmax(np.abs(sub_diagonala))

    # Returnez indicii elementului
    return np.unravel_index(index, M.shape)

def construieste_matrice_de_rotatie(i, j, theta):
    direction = np.ones(3)
    direction[i] = 0
    direction[j] = 0

    r = scipy.spatial.transform.Rotation.from_rotvec(theta * direction)

    return r.as_matrix()

def pas_metoda_jacobi(M):
    "Aplică o iterație din metoda Jacobi"

    p, q = gaseste_max_nediagonal(M)

    print(f"Maximul este la poziția i={p}, j={q}")

    if np.abs(M[p, p] - M[q, q]) < 1e-7:
        theta = np.pi / 4
    else:
        theta = np.arctan(2 * M[p, q] / (M[q, q] - M[p, p]))/2

    print(f"Rotesc la un unghi de {theta} radiani")

    R = construieste_matrice_de_rotatie(p, q, theta)

    return R @ M @ R.T

print("Matricea inițială:")
print(A)
print()

A1 = pas_metoda_jacobi(A)
print("După primul pas:")
print(A1)
print()

A2 = pas_metoda_jacobi(A1)
print("După al doilea pas:")
print(A2)
print()

print("Valorile proprii aproximative:")
print(np.diagonal(A2))
```

    Matricea inițială:
    [[4. 1. 1.]
     [1. 4. 1.]
     [1. 1. 4.]]

    Maximul este la poziția i=1, j=0
    Rotesc la un unghi de 0.7853981633974483 radiani
    După primul pas:
    [[ 3.00000000e+00  0.00000000e+00 -1.11022302e-16]
     [ 0.00000000e+00  5.00000000e+00  1.41421356e+00]
     [-1.11022302e-16  1.41421356e+00  4.00000000e+00]]

    Maximul este la poziția i=2, j=1
    Rotesc la un unghi de 0.6154797086703874 radiani
    După al doilea pas:
    [[ 3.00000000e+00  6.40987562e-17 -9.06493304e-17]
     [ 6.40987562e-17  3.33333333e+00  9.42809042e-01]
     [-9.06493304e-17  9.42809042e-01  5.66666667e+00]]

    Valorile proprii aproximative:
    [3.         3.33333333 5.66666667]

Verificare:

```python
eigenvalues, _ = np.linalg.eigh(A)
print("Valorile proprii:", eigenvalues)
```

    Valorile proprii: [3. 3. 6.]

## Exercițiul 7

Polinom de interpolare Lagrange - [cursul 8](https://drive.google.com/file/d/14YiX_LDd2cOAiLBF4dyHlWcWzqRZ8htm/view)

Fie o funcție $f$. Avem nodurile de interpolare $x_1$, $x_2$, $x_3$ și valorile corespunzătoare $y_1 = f(x_1)$, $y_2 = f(x_2)$, $y_3 = f(x_3)$.

### a) Metoda directă

Construim și rezolvăm sistemul
$$
\begin{pmatrix}
1 & x_1 & x_1^2 \\
1 & x_2 & x_2^2 \\
1 & x_3 & x_3^2
\end{pmatrix}
\cdot
\begin{pmatrix}
a_1 \\
a_2 \\
a_3
\end{pmatrix}
=
\begin{pmatrix}
y_1 \\
y_2 \\
y_3
\end{pmatrix}
$$
unde $a_1$, $a_2$, $a_3$ sunt coeficienții polinomului cerut
$$
P_2(x) = a_1 + a_2 x + a_3 x^2
$$

### b) Metoda Lagrange

Construim polinoamele $L_{n, k}$:
$$
L_{2, 1}(x) = \frac{(x - x_2) (x - x_3)}{(x_1 - x_2) (x_1 - x_3)} \\
L_{2, 2}(x) = \frac{(x - x_1) (x - x_3)}{(x_2 - x_1) (x_2 - x_3)} \\
L_{2, 3}(x) = \frac{(x - x_1) (x - x_2)}{(x_3 - x_1) (x_3 - x_2)}
$$

Polinomul cerut este
$$
P_2(x) = L_{2, 1}(x) \cdot y_1 + L_{2, 2}(x) \cdot y_2 + L_{2, 3}(x) \cdot y_3
$$

### c) Metoda Newton

Construim și rezolvăm sistemul inferior triunghiular
$$
\begin{array}{lcl}
c_1 & = & y_1 \\
c_1 + c_2 (x_2 - x_1) & = & y_2 \\
c_1 + c_2 (x_3 - x_1) + c_3 (x_3 - x_1) (x_3 - x_2) & = & y_3
\end{array}
$$

Polinomul cerut este
$$
P_2(x) = c_1 + c_2 (x - x_1) + c_3 (x - x_1) (x - x_2)
$$

### d) Metoda Newton cu diferențe divizate

Diferențele divizate de ordin 0, 1, 2 sunt:
$$
f[x_1] = f(x_1) \\
f[x_1, x_2] = \frac{f[x_2] - f[x_1]}{x_2 - x_1} \\
f[x_1, x_2, x_3] = \frac{f[x_2, x_3] - f[x_1, x_2]}{x_3 - x_1}
$$

Construim un tabel cu diferențele pentru cazul nostru:

| $x_i$ | DD ordin 0 | DD ordin 1 | DD ordin 2 |
|-------|------------|------------|------------|
| $x_1$ | $$f[x_1] = f(x_1) = y_1$$ | | |
| $x_2$ | $$f[x_2] = f(x_2) = y_2$$ | $$f[x_1, x_2]$$ | |
| $x_3$ | $$f[x_3] = f(x_3) = y_3$$ | $$f[x_2, x_3]$$ | $$f[x_1, x_2, x_3]$$ |

Polinomul cerut este
$$
P_2(x) = f[x_1] + f[x_1, x_2] (x - x_1) + f[x_1, x_2, x_3] (x - x_1) (x - x_2)
$$

## Exercițiul 8

### a)

$$
f(x) = \ln x \\
x = (1, e, e^2)
$$

Cu metoda Lagrange obținem:

$$
L_{2, 1}(x) = \frac{(x - e) (x - e^2)}{(1 - e) (1 - e^2)} \\
L_{2, 2}(x) = \frac{(x - 1) (x - e^2)}{(e - 1) (e - e^2)} \\
L_{2, 3}(x) = \frac{(x - 1) (x - e)}{(e^2 - 1) (e^2 - e)} \\
P_2(x) =  L_{2, 1}(x) \cdot \ln 1 + L_{2, 2}(x) \cdot \ln e + L_{2, 3}(x) \cdot \ln e^2
$$

Cu metoda Newton cu diferențe divizate obținem:

$$
f[x_1] = \ln 1; f[x_2] = \ln e; f[x_3] = \ln e^2 \\
f[x_1, x_2] = \frac{\ln e - \ln 1}{e - 1}; f[x_2, x_3] = \frac{\ln e^2 - \ln e}{e^2 - e} \\
f[x_1, x_2, x_3] = \frac{f[x_2, x_3] - f[x_1, x_2]}{e^2 - 1} \\
P_2(x) = f[x_1] + f[x_1, x_2] (x - 1) + f[x_1, x_2, x_3] (x - 1) (x - e)
$$

### b)

Analog cu cel de mai sus, dar pentru $f(x) = \sin \left(\frac{\pi x}{2}\right)$ și $x = (-1, 0, 1)$.

## Exercițiul 9

Avem punctele $(-2, 1)$, $(-1, 4)$, $(0, 11)$, $(1, 16)$, $(2, 13)$, $(3, -4)$.

Calculăm tabelul diferențelor divizate:

| $x_i$ | DD ordin 0 | DD ordin 1 | DD ordin 2 | DD ordin 3 | DD ordin 4 | DD ordin 5 |
|-------|------------|------------|------------|------------|------------|------------|
| $$-2$$ | $$1$$ |
| $$-1$$ | $$4$$ | $$3$$ |
| $$0$$ | $$11$$ | $$7$$ | $$2$$ |
| $$1$$ | $$16$$ | $$5$$ | $$-1$$ | $$-1$$ |
| $$2$$ | $$13$$ | $$-3$$ | $$-4$$ | $$-1$$ | $$0$$ |
| $$3$$ | $$-4$$ | $$-17$$ | $$-7$$ | $$-1$$ | $$0$$ | $$0$$ |

Observăm că diferențele divizate de ordin 4 sau mai mare sunt 0, deci polinomul pe care l-am obține e de grad 3.

## Exercițiul 10

Avem punctele $(0, 0)$, $(0.5, y)$, $(1, 3)$, $(2, 2)$.

Polinomul de interpolare obținut prin metoda Lagrange este

$$
\begin{align}
P_3(x) &= L_{3, 1}(x) y_1 + L_{3, 2}(x) y_2 + L_{3, 3}(x) y_3 + L_{3, 4}(x) y_4 \\
&= 0 + L_{3, 2}(x) y + L_{3, 3}(x) \cdot 3 + L_{3, 4}(x) \cdot 2 \\
&= \frac{(x - 0)(x - 1)(x - 2)}{(0.5 - 0) (0.5 - 1) (0.5 - 2)} \cdot y + \frac{(x - 0)(x - 0.5)(x - 2)}{(1 - 0)(1 - 0.5)(1 - 2)} \cdot 3 + \frac{(x - 0)(x - 0.5) (x - 1)}{(2 - 0)(2 - 0.5)(2 - 1)} \cdot 2
\end{align}
$$

Dacă scoatem doar termenii de grad 3 rămânem cu

$$
\frac{x^3}{0.375} \cdot y + \frac{x^3}{-0.5} \cdot 3 + \frac{x^3}{3} \cdot 2
$$

Din cerință, știm că coeficientul acestui termen trebuie să fie egal cu 6, deci

$$
6 = \frac{y}{0.375} - \frac{3}{0.5} + \frac{2}{3}
$$

de unde obținem $y = 4.25$

## Exercițiul 11

În cerință primim un tabel cu valorile polinomului în anumite puncte.\
O să considerăm că acestea sunt polii de interpolare, deci avem
- $x_1 = 0, f(x_1) = 2$
- $x_2 = 1, f(x_2) = -1$
- $x_3 = 2, f(x_3) = 4$

Din faptul că toate diferențele divizate de ordinul 3 sunt $1$, rezultă că diferențele divizate de ordin mai mare vor fi $0$; deci polinomul $P_n$ este de **gradul 3**.

Deci polinomul din ipoteză are forma:
$$
P_3(x) = f[x_1] + f[x_1, x_2] (x - 0) + f[x_1, x_2, x_3] (x - 0) (x - 1) + f[x_1, x_2, x_3, x_4] (x - 0) (x - 1) (x - 2)
$$

Ni se cere coeficientul lui $x^2$, acesta corespunde cu diferența divizată de ordin 2. Acesta apare odată cu coeficientul $f[x_1, x_2, x_3]$ și iese și din $f[x_1, x_2, x_3, x_4] x (x - 1) (x - 2)$ cu coeficientul $-3 f[x_1, x_2, x_3, x_4]$.

Din definiția diferențelor divizate știm că
$$
f[x_1, x_2, x_3, x_4] = \frac{f[x_2, x_3, x_4] - f[x_1, x_2, x_3]}{x_4 - x_1}
$$
$$
f[x_1, x_2, x_3] = \frac{f[x_2, x_3] - f[x_1, x_2]}{x_3 - x_1}
$$
unde
$$
\begin{align}
f[x_1, x_2] &= \frac{f(x_2) - f(x_1)}{x_2 - x_1} \\
f[x_2, x_3] &= \frac{f(x_3) - f(x_2)}{x_3 - x_2}
\end{align}
$$

Din datele pe care le avem, obținem
$$
\begin{align}
f[x_1, x_2] &= -3 \\
f[x_2, x_3] &= 5
\end{align}
$$
și
$$
f[x_1, x_2, x_3] = \frac{5 + 3}{2} = 4
$$

De asemenea, $f[x_1, x_2, x_3, x_4]$ este o diferență divizată de ordin 3, deci din ipoteză este egală cu $1$.

Deci coeficientul lui $x^2$ este $4 + (- 3) \cdot 1 = 4 - 3 = 1$.

## Exercițiul 12

Plecând de la valorile date, putem deduce celelalte numere din tabel:

$$
f[x_1, x_2, x_3] = \frac{f[x_3, x_2] - f[x_2, x_1]}{x_3 - x_1} \iff
\frac{50}{7} = \frac{10 - f[x_1, x_2]}{0.7} \iff
f[x_1, x_2] = 5
$$

$$
f[x_2, x_3] = \frac{f[x_3] - f[x_2]}{x_3 - x_2} \iff
10 = \frac{6 - f(x_2)}{0.7 - 0.4} \iff
f(x_2) = 3
$$

$$
f[x_1, x_2] = \frac{f[x_2] - f[x_1]}{x_2 - x_1} \iff
5 = \frac{3 - f(x_1)}{0.4} \iff
f(x_1) = 1
$$

$$
P_2(x) = 1 + 5 \cdot (x - 0) + \frac{50}{7} \cdot (x - 0) \cdot (x - 0.4)
$$

## Exercițiul 13

Interpolare spline liniară - secțiunea V.1 din [cursul 10](https://drive.google.com/file/d/1qclFs2MCxV_heEU4toWulxuGy-dY5VTO/view)

### a)

Definiția poate fi găsită în curs.

### b)

Fie $f(x) = \ln x$. Avem punctele $x = (1, e, e^2)$ și $y = f(x) = (\ln 1, \ln e, \ln e^2) = (0, 1, 2)$.

Calculăm coeficienții:

$$
a_1 = f(x_1) = 0 \\
b_1 = \frac{f(x_2) - f(x_1)}{x_2 - x_1} = \frac{1 - 0}{e - 1} = \frac{1}{e - 1}
$$

<hr/>

$$
a_2 = f(x_2) = 1 \\
b_2 = \frac{f(x_3) - f(x_2)}{x_3 - x_2} = \frac{2 - 1}{e^2 - e} = \frac{1}{e \cdot (e - 1)}
$$

Cele două porțiuni sunt:

$$
S_1 \colon [1, e) \to \mathbb{R}, S_1(x) = a_1 + b_1 (x - 1) \\
S_2 \colon [e, e^2] \to \mathbb{R}, S_2(x) = a_2 + b_2 (x - e)
$$

Funcția spline liniară care interpolează $f$ pe $[1, e^2]$ este reuniunea funcțiilor definite mai sus.

## Exercițiul 14

Interpolare spline pătratică - secțiunea V.2 din [cursul 10](https://drive.google.com/file/d/1qclFs2MCxV_heEU4toWulxuGy-dY5VTO/view)

### a)

Definiția poate fi găsită în curs.

### b)

Avem aceeași funcție și aceleași puncte ca la _13. b)_

Coeficienții sunt:

$$
h_1 = x_2 - x_1 = e - 1 \\
a_1 = f(x_1) = 0 \\
b_1 = f'(x_1) = \frac{1}{e} \\
\begin{align}
c_1 &= \frac{1}{h_1^2} (f(x_2) - f(x_1) - h_1 b_1) \\
&= \frac{1}{{(e - 1)}^2} \left(1 - 0 - (e - 1) \cdot \frac{1}{e}\right) \\
&= \frac{1}{{(e - 1)}^2} - \frac{1}{(e - 1) \cdot e}
\end{align}
$$

<hr/>

$$
h_2 = x_3 - x_2 = e^2 - e \\
a_2 = f(x_2) = 1 \\
b_2 = \frac{2}{h_1} (f(x_2) - f(x_1)) - b_1 = \frac{2}{e - 1} (1 - 0) - \frac{1}{e} = \frac{2}{e-1} - \frac{1}{e} \\
\begin{align}
c_2 &= \frac{1}{{(e^2 - e)}^2} \left(2 - 1 - (e - 1) \cdot \left(\frac{1}{{(e - 1)}^2} - \frac{1}{(e - 1) \cdot e}\right)\right) \\
&= \frac{1}{{(e^2 - e)}^2} \left(1 - \frac{1}{e - 1} + \frac{1}{e} \right)
\end{align}
$$

Cele două porțiuni sunt $S_1(x) = a_1 + b_1 (x - 1) + c_1 (x - 1)^2$, $S_2(x) = a_2 + b_2 (x - e) + c_2 (x - e)^2$.

## Exercițiul 15

Ce se cere este să deducem/scriem schema numerică de calcul a coeficienților unei funcții de interpolare spline pătratice, care se găsește și în curs:

$$
a_i = f(x_i), i \in [1, n] \\
b_1 = f'(x_1) \\
b_i = \frac{2}{h_{i - 1}} \left(f(x_i) - f(x_{i-1}) \right) - b_{i-1}, i \in [2, n] \\
c_i = \frac{1}{h_i^2} \left(f(x_{i + 1} - f(x_i) - h_i b_i)\right), i \in [1, n]
$$

Este suficient să aplicăm formulele de mai sus pentru $i = 1$ și $i = 2$.

## Exercițiul 16

Funcția de interpolare spline cubică - [cursul 11](https://drive.google.com/file/d/1RYFk7vVf4D8IoGXkIPy1rZGiZTbt-PJH/view)

Formulele pentru calcularea coeficienților unei funcții spline cubice sunt mai complicate. Trebuie să definim un sistem de liniar și să îl rezolvăm pentru a putea determina $b$-urile, iar din acestea putem calcula $c$-urile și $d$-urile.

De asemenea, există mai multe tipuri de funcții spline cubice, în funcție de constrângerile de la care plecăm:
- ni se dau $f'(x_0)$ și $f'(x_n)$ - sistemul pentru acest caz este în curs, și a fost dat și la [tema 3](https://github.com/GabrielMajeri/teme-fmi/blob/master/cn/teme/Majeri_Gabriel_332_Tema3.ipynb).
- ni se dau $f''(x_0)$ și $f''(x_n)$ - în enunț se cere funcția spline cubică **naturală**, deci $f''(x_0) = f''(x_n) = 0$.

Putem folosi sistemul [descris aici](https://www.math.ntnu.no/emner/TMA4215/2008h/cubicsplines.pdf). Funcția va fi de forma
$$
S_i(x) = \frac{z_{i + 1}}{6 h_i} (x - x_i)^3 + \frac{z_i}{6 h_i}(x_{i + 1} - x)^3 + \left(\frac{y_{i + 1}}{h_i} - \frac{z_{i + 1}}{6} h_i\right) (x - x_i) + \left(\frac{y_i}{h_i} - \frac{h_i}{6} z_i\right) (x_{i + 1} - x)
$$

```python
def calculeaza_spline_cubic_natural(x, y):
    N = len(x)

    h = x[1:] - x[0:-1]
    b = (y[1:] - y[0:-1]) / h
    v = 2 * (h[0:-1] + h[1:])
    u = 6 * (b[1:] - b[0:-1])

    Z = np.zeros((N - 2, N - 2))
    for i in range(N - 2):
        Z[i, i] = v[i]
        if i < N - 3:
            Z[i, i + 1] = h[i]
            Z[i + 1, i] = h[i]

    # z[0] and z[n] sunt 0 pentru o funcție spline cubică naturală
    z = np.zeros(N)
    z[1:-1] = np.linalg.solve(Z, u)

    A = z[1:] / (6 * h[:-1])
    B = z[:-1] / (6 * h[:-1])
    C = y[1:] / h[:-1] - (z[1:] / 6) * h[:-1]
    D = y[:-1] / h[:-1] - (h[:-1] / 6) * z[:-1]

    for i in range(N - 1):
        print(f"S_{i} = {A[i]} * (x - {x[i]})^3 + {B[i]} * ({x[i + 1]} - x)^3 + {C[i]} * (x - {x[i]}) + {D[i]} * ({x[i + 1]} - x)")
```

### a)

```python
x = np.array([0, 1, 2], dtype=np.float64)
y = np.array([0, 1, 2], dtype=np.float64)
calculeaza_spline_cubic_natural(x, y)
```

    S_0 = 0.0 * (x - 0.0)^3 + 0.0 * (1.0 - x)^3 + 1.0 * (x - 0.0) + 0.0 * (1.0 - x)
    S_1 = 0.0 * (x - 1.0)^3 + 0.0 * (2.0 - x)^3 + 2.0 * (x - 1.0) + 1.0 * (2.0 - x)

### b)

```python
x = np.array([1, 2, 3], dtype=np.float64)
y = np.array([2, 3, 5], dtype=np.float64)
calculeaza_spline_cubic_natural(x, y)
```

    S_0 = 0.25 * (x - 1.0)^3 + 0.0 * (2.0 - x)^3 + 2.75 * (x - 1.0) + 2.0 * (2.0 - x)
    S_1 = 0.0 * (x - 2.0)^3 + 0.25 * (3.0 - x)^3 + 5.0 * (x - 2.0) + 2.75 * (3.0 - x)

## Exercițiul 18

Din condiția că derivata de ordin I și II a funcției spline trebuie să fie continuă, avem că

$$
S_1'(1) = S_2'(1) \iff 2 - 3 \cdot 1^2 = B \iff \fbox{B = -1} \\
S_1''(1) = S_2''(1) \iff - 6 \cdot 1 = 2 C \iff \fbox{C = -3}
$$

Din ipoteză ni se spune și că $S''(b) = 0$, deci

$$
S_2''(2) = 0 \iff 2 C + 6 D (2 - 1) = 0 \iff 6 D - 6 = 0 \iff \fbox{D = 1}
$$

## Exercițiul 19

Scriem funcțiile $S_i$ și derivatele lor:

$$
\begin{align}
S_1 &\colon [1, 2) \to \mathbb{R} \\
S_1(x) &= 3(x - 1) + 2 (x - 1)^2 - (x - 1)^3 \\
S_1'(x) &= 3 + 4 (x - 1) - 3(x - 1)^2 \\
S_1''(x) &= 4 - 6 (x - 1)
\end{align}
$$

$$
\begin{align}
S_2 &\colon [2, 3] \to \mathbb{R} \\
S_2(x) &= A + B (x - 2) + C (x - 2)^2 + D (x - 2)^3 \\
S_2'(x) &= B + 2C (x - 2) + 3 D(x - 2)^2 \\
S_2''(x) &= 2C + 6D (x - 2)
\end{align}
$$

Știm că funcția $S$ trebuie să fie continuă, deci
$$
S_1(2) = S_2(2) \iff 3(2 - 1) + 2 (2 - 1)^2 - (2 - 1)^3 = A \iff 3 + 2 - 1 = A \iff \fbox{$A = 4$}
$$

Știm că funcția $S'$ trebuie să fie continuă, deci
$$
S_1'(2) = S_2'(2) \iff 3 + 4 (2 - 1) - 3 (2 - 1) = B \iff 3 + 4 - 3 = B \iff \fbox{$B = 4$}
$$

Știm că funcția $S''$ trebuie să fie continuă, deci
$$
S_1''(2) = S_2''(2) \iff 4 - 6 = 2 C \iff \fbox{$C = -1$}
$$

Din cerință ni se spune că
$$
f'(1) = f'(3) \iff S_1'(1) = S_2'(3) \iff 3 = 4 - 2 + 3D \iff \fbox{$D = \frac{1}{3}$}
$$

## Exercițiul 22

Derivarea numerică - secțiunile VI.1 și VI.2 din [cursul 12](https://drive.google.com/file/d/1c6twFTnfwdq-3B9soATOsjv9NAh6811i/view)

Scriem [dezvoltarea în serie Taylor până la ordinul 4](https://www.wolframalpha.com/input/?i=Taylor+series+expansion+of+f%28x+%2B+h%29+up+to+order+4) pentru termeni:

$$
f(x - h) = f(x) - h f'(x) + \frac{h^2}{2} f''(x) - \frac{h^3}{6} f^{(3)}(x) + \frac{h^4}{24} f^{(4)} + O(h^5) \\
f(x + h) = f(x) + h f'(x) + \frac{h^2}{2} f''(x) + \frac{h^3}{6} f^{(3)}(x) + \frac{h^4}{24} f^{(4)} + O(h^5) \\
f(x + 2h) = f(x) + 2h f'(x) + 2 h^2 f''(x) + \frac{4 h^3}{3} f^{(3)}(x) + \frac{2 h^4}{3} f^{(4)} + O(h^5) \\
f(x + 3h) = f(x) + 3h f'(x) + \frac{9 h^2}{2} f''(x) + \frac{9h^3}{2} f^{(3)}(x) + \frac{27 h^4}{8} f^{(4)} + O(h^5)
$$

După aceea, scriem combinația liniară din indicație $A f(x - h) + B f(x + h) + C f(x + 2h) + D f(x + 3h)$ și obținem:

$$
f(x) \cdot (A + B + C + D) \\
+ h f'(x) \cdot (-A + B + 2C + 3D) \\
+ h^2 f''(x) \cdot \left(\frac{1}{2} A + \frac{1}{2} B + 2 C + \frac{9}{2} D\right) \\
+ h^3 f^{(3)}(x) \cdot \left(- \frac{1}{6} A + \frac{1}{6} B + \frac{4}{3} C + \frac{9}{2} D\right) \\
+ h^4 f^{(4)}(x) \cdot \left(\frac{1}{24} A + \frac{1}{24} B + \frac{2}{3} C + \frac{27}{8} D\right) + O(h^5)
$$

Vrem să obținem o formulă în care să rămână doar $f'(x)$, deci construim sistemul:
$$
\begin{pmatrix}
-1 & 1 & 2 & 3 \\
\frac{1}{2} & \frac{1}{2} & 2 & \frac{9}{2} \\
-\frac{1}{6} & \frac{1}{6} & \frac{4}{3} & \frac{9}{2} \\
\frac{1}{24} & \frac{1}{24} & \frac{2}{3} & \frac{27}{8}
\end{pmatrix}
\begin{pmatrix}
A \\
B \\
C \\
D
\end{pmatrix}
=
\begin{pmatrix}
0 \\
1 \\
0 \\
0 \\
0
\end{pmatrix}
$$

(pe coeficientul lui $f(x)$ îl lăsăm să varieze liber, nu ne deranjează dacă apare în rezultatul final)

```python
A = np.array([
    [-1, 1, 2, 3],
    [1/2, 1/2, 2, 9/2],
    [-1/6, 1/6, 4/3, 9/2],
    [1/24, 1/24, 2/3, 27/8]
])
b = np.array([[1, 0, 0, 0]]).T

solution = np.linalg.solve(A, b)

print("Coeficienții A, B, C, D:", solution.T)
print("Coeficientul lui f(x):", np.sum(solution))
```

    Coeficienții A, B, C, D: [[-0.25        1.5        -0.5         0.08333333]]
    Coeficientul lui f(x): 0.8333333333333334

Obținem formula:

$$\frac{-0.25 f(x - h) + 1.5 f(x + h) - 0.5 f(x + 2h) + 0.0833 f(x + 3h) + 0.833 f(x)}{h} \approx f'(x)$$

cu ordinul de aproximare $\frac{O\left(h^5\right)}{h} = O\left(h^4\right)$

## Exercițiul 23

Avem nevoie să scriem expansiunea Taylor până avem erori de ordinul $O(h^5)$, pentru că atunci când vom scoate termenul $f'''(x)$ o să rămânem cu o eroare de ordinul $\frac{O(h^5)}{h^3} = O(h^2)$.

Ca să ne asigurăm că sistemul pe care îl construim are soluție, avem nevoie de cel puțin 4 termeni. Vom folosi $f(x - 2h)$, $f(x - h)$, $f(x + h)$, $f(x + 2h)$.

```python
A = np.array([
    [-2, -1, 1, 2],
    [2, 1/2, 1/2, 2],
    [-4/3, -1/6, 1/6, 4/3],
    [2/3, 1/24, 1/24, 2/3]
], dtype=np.float64)
b = np.array([[0, 0, 1, 0]]).T

solution = np.linalg.solve(A, b)

print("Coeficienții A, B, C, D:", solution.T)
print("Coeficientul lui f(x):", np.sum(solution))
```

    Coeficienții A, B, C, D: [[-0.5  1.  -1.   0.5]]
    Coeficientul lui f(x): 0.0

Deci formula este
$$
\frac{-0.5 f(x - 2h) + f(x - h) - f(x + h) + 0.5 f(x + 2h)}{h^3} \approx f'''(x)
$$
cu ordinul de aproximare $O(h^2)$.

## Exercițiul 24

Ca să obținem o formulă de ordin de eroare $O(h^n)$ plecând de la una de forma $f'(x) = \varphi(h) + a_1 h + a_2 h^2 + \dots$, putem folosi metoda de extrapolare Richardson (secțiunea VI.3 din [cursul 12](https://drive.google.com/file/d/1c6twFTnfwdq-3B9soATOsjv9NAh6811i/view)).

În curs se derivă o formulă pentru termeni de forma $\frac{h}{3^k}$, dar în loc să folosim $\varphi(h), \varphi\left(\frac{h}{2}\right), \varphi\left(\frac{h}{2^2}\right)$ putem să înlocuim cu $\varphi(h), \varphi\left(\frac{h}{3}\right), \varphi\left(\frac{h}{3^2}\right)$.

Pe baza notației din curs, avem:
$$
Q_{i, j} = \varphi_j \left(\frac{h}{3^{i - j}}\right)
$$
definit inductiv prin
$$
Q_{i, j} = \varphi_{j - 1} \left(\frac{h}{3^{i - j + 1}}\right) + \frac{1}{3^{j - 1} - 1}\left(
\varphi_{j - 1}\left(\frac{h}{3^{i - (j - 1)}}\right) - \varphi_{j - 1}\left(\frac{h}{3^{i - 1 - (j - 1)}}\right)
\right)
$$

Scris mai pe scurt:
$$
Q_{i, j} = Q_{i, j-1} + \frac{1}{3^{j - 1} - 1} \left(Q_{i, j - 1} - Q_{i - 1, j - 1} \right)
$$
unde
$$
Q_{1, 1} = \varphi(h), Q_{2, 1} = \varphi\left(\frac{h}{3}\right), Q_{3, 1} = \varphi\left(\frac{h}{3^2}\right)
$$

Dacă aplicăm formula inductivă de mai sus plecând de la $Q_{3, 3}$, obținem aproximarea de ordinul $O(h^3)$ care se cere în enunț:

$$
Q_{3. 3} = Q_{3, 2} + \frac{1}{3^2 - 1} \left(Q_{3, 2} - Q_{2, 2}\right) \\
Q_{3, 2} = Q_{3, 1} + \frac{1}{3^1 - 1} \left(Q_{3, 1} - Q_{2, 1}\right) \qquad Q_{2, 2} = Q_{2, 1} + \frac{1}{3^1 - 1} \left(Q_{2, 1} - Q_{1, 1}\right) \\
\iff \\
Q_{3, 2} = \varphi\left(\frac{h}{3^2}\right) + \frac{1}{2} \left(\varphi\left(\frac{h}{3^2}\right) - \varphi\left(\frac{h}{3}\right)\right) \qquad
Q_{2, 2} = \varphi\left(\frac{h}{3}\right) + \frac{1}{2} \left(\varphi\left(\frac{h}{3}\right) - \varphi(h)\right)
$$

## Exercițiul 25

Formule de cuadratură Newton-Cotes - secțiunea VII.2 din [cursul 13](https://drive.google.com/file/d/1DCPEvUUGvzI1cGpAiGzLdMCv4qWPc4T3/view).

Formula de cuadratură a dreptunghiului este:
$$
I_0 (f) = (b - a) f\left(\frac{a + b}{2}\right)
$$

Formula de cuadratură a trapezului este:
$$
I_1 (f) = \frac{b - a}{2} \cdot \left[f(a) + f(b)\right]
$$

Formula de cuadratură Simpson este:
$$
I_2 (f) = \frac{b - a}{6} \cdot \left[f(a) + 4 f\left(\frac{a + b}{2}\right) + f(b)\right]
$$

În cazul nostru:
$$
\frac{2 - 0}{2} \cdot \left[f(0) + f(2)\right] = 5 \implies f(0) + f(2) = 5 \\
(2 - 0) \cdot f \left(\frac{0 + 2}{2}\right) = 2 f(1) = 4 \implies f(1) = 2
$$

Deci formula Simpson ne-ar da rezultatul
$$
I_2 (f) = \frac{2 - 0}{6} \cdot \left[f(0) + 4 f(1) + f(2)\right] = \frac{13}{3}
$$

## Exercițiul 26

Rezolvăm asemănător cu exercițiul anterior. Din ipoteză știm că $a = 0$, $b = 2$, $I_1(f) = 4$ și $I_2(f) = 2$.

Scriem formulele de cuadratură a trapezului și Simpson pentru cazul nostru:
$$
I_1(f) = f(0) + f(2) \\
I_2(f) = \frac{1}{3} [f(0) + 4 f(1) + f(2)]
$$

Făcând o substituție, obținem:
$$
I_2(f) = \frac{1}{3} [I_1(f) + 4 f(1)]
$$

Înlocuim cu datele din ipoteză și rezolvăm:
$$
2 = \frac{1}{3} [4 + 4 f(1)] \iff 6 = 4 + 4 f(1) \iff \frac{3}{2} = 4 f(1) \iff \fbox{$f(1) = \frac{3}{8}$}
$$

## Exercițiul 27

Formule de cuadratură sumată - secțiunea VII.3 din [cursul 13](https://drive.google.com/file/d/1DCPEvUUGvzI1cGpAiGzLdMCv4qWPc4T3/view).

Formula de cuadratură sumată a trapezului este
$$
I_{1, m}(f) = \frac{h}{2}\left(f(x_1) + 2 \sum_{k = 2}^{m} f(x_k) + f(x_{m + 1}) \right)
$$

În cazul nostru, pentru $m = 4$, avem $h = \frac{1}{4} = 0.25$. Formula s-ar scrie ca:
$$
\begin{align}
I_{1, 4}(f) &= \frac{h}{2} (f(0) + 2 (f(0.25) + f(0.5) + f(0.75)) + f(1)) \\
&= 0.125 (1 + 2(\alpha + 2.5 + \alpha) + 2)) \\
&= 0.125 (8 + 4 \alpha) = 1 + 0.5 \alpha
\end{align}
$$
Egalând cu valoarea dată din cerință:
$$
1 + 0.5\alpha \approx 1.75 \iff 0.5 \alpha \approx 0.75 \iff \fbox{$\alpha \approx 1.5$}
$$


```python
from sympy import *
init_printing()
```

http://een.iust.ac.ir/profs/Abrishamifar/Analog%20Integrated%20Circuit%20Design/Data%20For%20Design/Useful%20equations/Equations.pdf

```python
Id, W,L, VDS, VGS, Vnth, VnA, nTechConst=symbols("I_D, W, L, V_DS, V_GS, V_thn, V_An, k_n'")
Id, W,L, VDS, VGS, Vnth, VnA, nTechConst
```

```python
NLinFull=nTechConst*(W/L)*((VGS-Vnth)*VDS-VDS**2/2)*(1+VDS/VnA)
NLinFull
```

```python
NLinSimp=nTechConst*(W/L)*((VGS-Vnth)*VDS-VDS**2/2)
NLinSimp
```

```python
NSatFull=nTechConst/2*W/L*(VGS-Vnth)**2 *(1+VDS/VnA)
NSatFull
```

```python
NSatSimp=nTechConst/2*W/L*(VGS-Vnth)**2
NSatSimp
```

```python
NIdFull=Piecewise(
    (0, VGS<Vnth),
    (NLinFull, And(VGS>=Vnth, VDS<VGS-Vnth)),
    (NSatFull, And(VGS>=Vnth, VDS>=VGS-Vnth))
)
NIdFull
```

```python
NIdSimp=Piecewise(
    (0, VGS<Vnth),
    (NLinSimp, And(VGS>=Vnth, VDS<VGS-Vnth)),
    (NSatSimp, And(VGS>=Vnth, VDS>=VGS-Vnth))
)
NIdSimp
```

```python
NCutoffCond=VGS<Vnth; NCutoffCond
```

```python
NLinearCond=VDS<VGS-Vnth; NLinearCond
```

```python
NSatCond=VDS>=VGS-Vnth; NSatCond
```

```python
NLinFull.subs({})
```

```python
class NMOS():
    def __init__():
        pass

    @staticmethod
    def LinFull(subs={}):
        return NLinFull.subs(subs)

    @staticmethod
    def LinSimp(subs={}):
        return NLinSimp.subs(subs)

    @staticmethod
    def SatFull(subs={}):
        return NSatFull.subs(subs)

    @staticmethod
    def IdFull(subs={}):
        return NIdFull.subs(subs)

    @staticmethod
    def IdSimp(subs={}):
        return NIdSimp.subs(subs)

    @staticmethod
    def CutoffCond(subs={}):
        return NCutoffCond.subs(subs)

    @staticmethod
    def LinearCond(subs={}):
        return NLinearCond.subs(subs)

    @staticmethod
    def SatCond(subs={}):
        return NSatCond.subs(subs)
```
NMOS.LinFull(subs={VnA:5})print(NMOS.LinFull())
print(NMOS.LinSimp())
print(NMOS.SatFull())
print(NMOS.IdFull())
print(NMOS.IdSimp())
print(NMOS.CutoffCond())
print(NMOS.LinearCond())
print(NMOS.SatCond())

```python
VSD, VSG, Vpth, VpA, pTechConst=symbols("V_SD, V_SG, V_thp, V_Ap, k_p'")
VSD, VSG, Vpth, VpA, pTechConst
```

```python
PLinFull=pTechConst*(W/L)*((VSG-abs(Vpth))*VSD-VSD**2/2)*(1+VSD/abs(VpA))
PLinFull
```

```python
PLinSimp=pTechConst*(W/L)*((VSG-abs(Vpth))*VSD-VSD**2/2)
PLinSimp
```

```python
PSatFull=pTechConst/2*W/L*(VSG-abs(Vpth))**2 *(1+VSD/abs(VpA))
PSatFull
```

```python
PSatSimp=pTechConst/2*W/L*(VSG-abs(Vpth))**2
PSatSimp
```

```python
PIdFull=Piecewise(
    (0, VSG<abs(Vpth)),
    (PLinFull, And(VSG>=abs(Vpth), VSD<VSG-abs(Vpth))),
    (PSatFull, And(VSG>=abs(Vpth), VSD>=VSG-abs(Vpth)))
)
PIdFull
```

```python
PIdSimp=Piecewise(
    (0, VSG<abs(Vpth)),
    (PLinSimp, And(VSG>=abs(Vpth), VSD<VSG-abs(Vpth))),
    (PSatSimp, And(VSG>=abs(Vpth), VSD>=VSG-abs(Vpth)))
)
PIdSimp
```

```python
PCutoffCond=VSG<abs(Vpth); PCutoffCond
```

```python
PLinearCond=VSD<VSG-abs(Vpth); PLinearCond
```

```python
PSatCond=VSD>=VSG-abs(Vpth); PSatCond
```

```python
class PMOS():
    def __init__():
        pass

    @staticmethod
    def LinFull(subs={}):
        return PLinFull.subs(subs)

    @staticmethod
    def LinSimp(subs={}):
        return PLinSimp.subs(subs)

    @staticmethod
    def SatFull(subs={}):
        return PSatFull.subs(subs)

    @staticmethod
    def IdFull(subs={}):
        return PIdFull.subs(subs)

    @staticmethod
    def IdSimp(subs={}):
        return PIdSimp.subs(subs)

    @staticmethod
    def CutoffCond(subs={}):
        return PCutoffCond.subs(subs)

    @staticmethod
    def LinearCond(subs={}):
        return PLinearCond.subs(subs)

    @staticmethod
    def SatCond(subs={}):
        return PSatCond.subs(subs)
```
print(PMOS.LinFull(subs={VpA:5}))
print(PMOS.LinSimp())
print(PMOS.SatFull())
print(PMOS.IdFull())
print(PMOS.IdSimp())
print(PMOS.CutoffCond())
print(PMOS.LinearCond())
print(PMOS.SatCond())

```python

```