# Curvas Características P-V e I-V de uma Célula Fotovoltaica

Este programa estima parâmetros de uma célula fotovoltaica real e traça suas curvas características P-V e I-V para diferentes variações de temperatura e irradiância solar. Antes de traçar as curvas características P-V e I-V, é necessário calcular todos os parâmetros que não são dados no datasheet de um painel solar fotovoltaico.

```python
import numpy as np
import sympy as sy # Importa o sympy como sy para poder utilizar a exponencial sy.exp()
from sympy import *
%matplotlib inline
import matplotlib.pyplot as plt

"""
Referências
------------

[1] Aminde, N. O. (2013). Controle extremal via funções de monitoração e reali-mentação de saída (Dissertação de mestrado).
    Universidade Federal do Rio de Janeiro, Rio de Janeiro, RJ, Brasil.
[3] Sera, D., Teodorescu, R., & Rodriguez, P. (2007, June). PV panel model based on datasheet values.
    IEEE International Symposium on Industrial Electron-ics, Vigo, Spain.
[4] Villalva, M. G., Gazoli, J. R., & Filho, E. R. (2009). Comprehensive approach to modeling and simulation of
    photovoltaic arrays. IEEE Transactions on Po-wer Electronics, 24(4), 1198-1208.
"""

class ModeloUmDiodo(object):
    """ Classe que estima parâmetros de uma célula fotovoltaica real e traça suas curvas características P-V e V-I
    para diferentes variações de temperatura [K] e irradiância solar [W/m²]."""
    constante_boltzmann = 1.38065e-23 # Constante de Boltzann
    carga_eletron = 1.602e-19 # Carga do elétron
    temperatura_referencia = 25 + 273 # Temperatura de referência em Kelvin [K]
    irradiacao_referencia = 1000 # Irradiação de referência [W/m²]
    gap_energia = 1.12 # Gap de energia do silício à 25°C
    NOCT = 49 # Temperatura em Graus Celcius [°C]

    def __init__(self,
                 corrente_curto_circuito,
                 tensao_circuito_aberto,
                 numero_celulas_serie = 1,
                 coeficiente_temperatura_corrente = 0.003,
                 fator_idealidade_diodo = 1.2):

        self.corrente_curto_circuito = corrente_curto_circuito
        self.tensao_circuito_aberto = tensao_circuito_aberto
        self.numero_celulas_serie = numero_celulas_serie
        self.coeficiente_temperatura_corrente = coeficiente_temperatura_corrente
        self.fator_idealidade_diodo = fator_idealidade_diodo

    # Sequencia de métodos que irão retornar os parâmetros da célula fotovoltaica

    def get_Tc(self): # Temperatura da célula
        return self.Tc

    def get_Vt(self): # Potencial térmico
        return self.Vt

    def get_Io(self): # Corrente Io
        return self.Io

    def get_Is(self): # Fonte de corrente Is
        return self.Is

    def get_Ior(self): # Corrente de Saturação Reversa
        return self.Ior

    def get_Isat(self): # Corrente de Saturação
        return self.Isat

    def get_I_vec(self): # Vetor da corrente nominal da célula
        return self.I_vec

    def get_V_vec(self): # Vetor da tensão nominal da célula
        return self.V_vec

    def get_Ilamb_vec(self): # Vetor da corrente nominal da célula utilizando o lambdify
        return self.Ilamb_vec

    def get_Vlamb_vec(self): # Vetor da tensão nominal da célula utilizando o lambdify
        return self.Vlamb_vec

    def get_Ipot_vec(self): # Vetor da corrente nominal da célula (cálculo da potência)
        return self.Ipot_vec

    def get_Vpot_vec(self): # Vetor da tensão nominal da célula (cálculo da potência)
        return self.Vpot_vec

    def get_P_vec(self): # Vetor da potência nominal da célula
        return self.P_vec

    # Métodos que irão calcular os parâmetros da célula solar fotovoltaica.

    def temperatura_celula(self, irradiancia_ambiente, temperatura_ambiente):
        # Baseada na equação (4) e depende do NOCT, irradiação e temperatura ambientes
        Tc = temperatura_ambiente+(irradiancia_ambiente*(self.NOCT - 20))/800
        self.Tc = Tc

    def potencial_termico(self):
        # Baseado na definição de potêncial térmico
        Vt = (self.constante_boltzmann*self.numero_celulas_serie*self.Tc)/self.carga_eletron
        self.Vt = Vt

    def corrente_inicial(self, irradiancia_ambiente):
        # Baseada na equação (3) e define o valor da corrente Io
        Io = (irradiancia_ambiente*self.corrente_curto_circuito)/self.irradiacao_referencia
        self.Io = Io

    def fonte_corrente(self):
        # Baseada na equação (2) e define o valor da fonte de corrente do circuito equivalente
        Is = self.Io + self.coeficiente_temperatura_corrente*(self.Tc - self.temperatura_referencia)
        self.Is = Is

    def corrente_saturacao_reversa(self):
        # Baseada na equação (7) e define a corrente de saturação reversa do diodo
        Ior = self.corrente_curto_circuito/(np.exp((self.carga_eletron*self.tensao_circuito_aberto)/
                (self.constante_boltzmann*self.fator_idealidade_diodo*self.Tc))-1)
        self.Ior = Ior

    def corrente_saturacao(self):
        # Baseada na equação (6) e define a corrente de saturação do diodo
        Isat = self.Ior*((self.Tc/self.temperatura_referencia)**3)*np.exp(((self.carga_eletron*self.gap_energia)/
                (self.constante_boltzmann*self.fator_idealidade_diodo))*
                (1/self.temperatura_referencia - 1/self.Tc))
        self.Isat = Isat

    # Sequência de funções que calcula a corrente, tensão e potência nominal da célula fotovoltaica.
    def corrente_nominal(self, irradiancia_ambiente, temperatura_ambiente):
        """Esta função retorna a corrente e tensão nominal da célula, inserindo o valor da irradiância ambiente
        [W/m²] e a temperatura ambiente [K]."""
        # Utiliza os métodos anteriores para calcular os parâmetros da célula Fotovoltaica
        self.temperatura_celula(irradiancia_ambiente, temperatura_ambiente)
        self.potencial_termico()
        self.corrente_inicial(irradiancia_ambiente)
        self.fonte_corrente()
        self.corrente_saturacao_reversa()
        self.corrente_saturacao()

        # Calcula a corrente nominal da célula Fotovoltaica através dos parâmetros encontrados
        V = Symbol ('V') # Determina V como um símbolo
        V_vec = np.arange(0, self.tensao_circuito_aberto, 0.001) # Cria um vetor de tensão de acordo com Voc inserido
        I_vec = np.array([N(self.Is - self.Isat*(sy.exp(V/ # Cria o vetor de corrente nominal utilizando o vetor V_vec
                (self.fator_idealidade_diodo*self.Vt))-1).subs(V, xx)) for xx in V_vec])
        self.I_vec = I_vec
        self.V_vec = V_vec

    def corrente_nominal_lamb(self, irradiancia_ambiente, temperatura_ambiente):
        """Esta função retorna a corrente e tensão nominal da célula, inserindo o valor da irradiância ambiente
        [W/m²] e a temperatura ambiente [K] utilizando o lambdify do Sympy."""
        # A função Lambdify transforma expressões do Sympy em funções "lambda"
        # que podem ser utilizadas para calcular valores numéricos rapidamente

        # Utiliza os métodos anteriores para calcular os parâmetros da célula Fotovoltaica
        self.temperatura_celula(irradiancia_ambiente, temperatura_ambiente)
        self.potencial_termico()
        self.corrente_inicial(irradiancia_ambiente)
        self.fonte_corrente()
        self.corrente_saturacao_reversa()
        self.corrente_saturacao()

        # Calcula a corrente nominal da célula Fotovoltaica através dos parâmetros encontrados
        Vlamb = Symbol ('Vlamb') # Determina Vlamb como um símbolo
        Vlamb_vec = np.arange(0, self.tensao_circuito_aberto, 0.001) # Cria um vetor de tensão de acordo com Voc inserido
        f = lambdify([Vlamb], self.Is - self.Isat*(sy.exp(Vlamb/(self.fator_idealidade_diodo*self.Vt))-1), 'numpy')
        Ilamb_vec = f(Vlamb_vec) # Utiliza-se o lambdify do Sympy para calcular o vetor de corrente
        self.Vlamb_vec = Vlamb_vec
        self.Ilamb_vec = Ilamb_vec

    def potencia_nominal(self, irradiancia_ambiente, temperatura_ambiente):
        """Esta função retorna a potência e tensão nominal da célula, inserindo o valor da irradiância ambiente
        [W/m²] e a temperatura ambiente [K]."""
        # Utiliza os métodos anteriores para calcular os parâmetros da célula Fotovoltaica
        self.temperatura_celula(irradiancia_ambiente, temperatura_ambiente)
        self.potencial_termico()
        self.corrente_inicial(irradiancia_ambiente)
        self.fonte_corrente()
        self.corrente_saturacao_reversa()
        self.corrente_saturacao()

        # Calcula a potência nominal da célula Fotovoltaica através dos parâmetros encontrados
        Vpot = Symbol ('Vpot') # Determina Vpot como um símbolo
        Vpot_vec = np.arange(0, self.tensao_circuito_aberto, 0.001) # Cria um vetor de tensão de acordo com Voc inserido
        Ipot_vec = np.array([N(self.Is - self.Isat*(sy.exp(Vpot/ # Cria o vetor de corrente utilizando o vetor Vpot_vec
                (self.fator_idealidade_diodo*self.Vt))-1).subs(Vpot, xx)) for xx in Vpot_vec])
        P_vec = Vpot_vec*Ipot_vec # Defeni-se o vetor de potência da célula
        self.P_vec = P_vec
        self.Ipot_vec = Ipot_vec
        self.Vpot_vec = Vpot_vec

    def plota_grafico_corrente(self):
        """Esta função retorna do gráfico da curva característica I-V"""
        i_max = self.I_vec.max() # Calcula os valores máximos dos vetores de corrente e tensão
        v_max = self.V_vec.max()

        plt.figure(figsize=(8, 4), dpi=80) # Define o tamanho da figura em polegadas e dpi = pontos por polegada
        plt.plot(self.V_vec, self.I_vec, color="navy", linewidth=2.5, linestyle="-", label = "I-V")
        plt.legend(loc='best') # Insere a legenda no "melhor" local da figura
        plt.xlim(0, float(v_max)*0.9) # Define os limites do eixo x e y
        plt.ylim(0, float(i_max)*1.1)
        plt.xlabel('Tensão [V]') # Define-se o titulo dos eixos x e y
        plt.ylabel('Corrente [I]')
        plt.title('Curva Característica I-V') # Define-se o titulo do gráfico
        plt.grid(True) # Insere grade no gráfico

    def plota_grafico_potencia(self):
        """Esta função retorna do gráfico da curva característica P-V"""
        p_max = self.P_vec.max() # Calcula os valores máximos dos vetores de potência e tensão
        vpot_max = self.Vpot_vec.max()

        plt.figure(figsize=(8, 4), dpi=80) # Define o tamanho da figura em polegadas e dpi = pontos por polegada
        plt.plot(self.Vpot_vec, self.P_vec, color="tomato", linewidth=2.5, linestyle="-", label = "P-V")
        plt.legend(loc='best') # Insere a legenda no "melhor" local da figura
        plt.xlim(0, float(vpot_max)*0.9) # Define os limites do eixo x e y
        plt.ylim(0, float(p_max)*1.1)
        plt.xlabel('Tensão [V]') # Define-se o titulo dos eixos x e y
        plt.ylabel('Potência [W]')
        plt.title('Curva Característica P-V') # Define-se o titulo do gráfico
        plt.grid(True) # Insere grade no gráfico

    def plota_grafico_corrente_lamb(self):
        """Esta função retorna do gráfico da curva característica I-V utilizando o lambdify do Sympy"""
        ilamb_max = self.Ilamb_vec.max() # Calcula os valores máximos dos vetores de corrente e tensão
        vlamb_max = self.Vlamb_vec.max()

        plt.figure(figsize=(8, 4), dpi=80) # Define o tamanho da figura em polegadas e dpi = pontos por polegada
        plt.plot(self.Vlamb_vec, self.Ilamb_vec, color="darkgreen", linewidth=2.5, linestyle="-", label = "I-V")
        plt.legend(loc='best') # Insere a legenda no "melhor" local da figura
        plt.xlim(0, float(vlamb_max)*0.9) # Define os limites do eixo x e y
        plt.ylim(0, float(ilamb_max)*1.1)
        plt.xlabel('Tensão [V]') # Define-se o titulo dos eixos x e y
        plt.ylabel('Corrente [I]')
        plt.title('Curva Característica I-V Utilizando o Lambdify') # Define-se o titulo do gráfico
        plt.grid(True) # Insere grade no gráfico
```

```python
# EXEMPLOS DE USO - Curva Característica I-V para Isc= 2 [A] e Voc = 0,596 [V]
# Irradiação Solar de Ga = 1000 W/m² e Temperatura Ambiente Ta = 298 Kelvin
a = ModeloUmDiodo(2, 0.596)
%timeit a.corrente_nominal(1000, 298)
a.plota_grafico_corrente()
```

```python
# EXEMPLOS DE USO - Curva Característica P-V para Isc= 2 [A] e Voc = 0,596 [V]
# Irradiação Solar de Ga = 1000 W/m² e Temperatura Ambiente Ta = 298 Kelvin
b = ModeloUmDiodo(2, 0.596)
%timeit b.potencia_nominal(1000,298)
b.plota_grafico_potencia()
```

```python
# EXEMPLOS DE USO - Curva Característica P-V para Isc= 2 [A] e Voc = 0,596 [V]
# Irradiação Solar de Ga = 1000 W/m² e Temperatura Ambiente Ta = 298 Kelvin
# Utilizando o lambdify do Sympy
c = ModeloUmDiodo(2, 0.596)
%timeit c.corrente_nominal_lamb(1000,298)
c.plota_grafico_corrente_lamb()
```

```python
# EXEMPLOS DE USO - Curva Característica I-V para Isc= 2 [A] e Voc = 0,596 [V]
# Irradiação Solar de Ga = 200 W/m² e Temperatura Ambiente Ta = 298 Kelvin
d = ModeloUmDiodo(2, 0.596)
%timeit d.corrente_nominal(200, 298)
d.plota_grafico_corrente()
```

```python
# EXEMPLOS DE USO - Curva Característica P-V para Isc= 2 [A] e Voc = 0,596 [V]
# Irradiação Solar de Ga = 200 W/m² e Temperatura Ambiente Ta = 298 Kelvin
e = ModeloUmDiodo(2, 0.596)
%timeit e.potencia_nominal(200,298)
e.plota_grafico_potencia()
```

```python
# EXEMPLOS DE USO - Curva Característica P-V para Isc= 2 [A] e Voc = 0,596 [V]
# Irradiação Solar de Ga = 200 W/m² e Temperatura Ambiente Ta = 298 Kelvin
# Utilizando o lambdify do Sympy
f = ModeloUmDiodo(2, 0.596)
%timeit f.corrente_nominal_lamb(200,298)
f.plota_grafico_corrente_lamb()
```

```python
# EXEMPLOS DE USO LIVRE - Curva Característica I-V para Isc e Voc desejados
# Irradiação Solar Ga [W/m²] e Temperatura Ambiente Ta [K]
# Seguir indicações do item 3.1. Limitações Físicas da Célula Fotovoltaica e Ambientais
g = ModeloUmDiodo(4, 0.55)
%timeit g.corrente_nominal(600, 310)
g.plota_grafico_corrente()
```

```python
# EXEMPLOS DE USO LIVRE - Curva Característica P-V para Isc e Voc desejados
# Irradiação Solar Ga [W/m²] e Temperatura Ambiente Ta [K]
# Seguir indicações do item 3.1. Limitações Físicas da Célula Fotovoltaica e Ambientais
h = ModeloUmDiodo(4, 0.55)
%timeit h.potencia_nominal(600,310)
h.plota_grafico_potencia()
```

```python
# EXEMPLOS DE USO LIVRE - Curva Característica P-V para Isc e Voc desejados
# Irradiação Solar Ga [W/m²] e Temperatura Ambiente Ta [K]
# Seguir indicações do item 3.1. Limitações Físicas da Célula Fotovoltaica e Ambientais
# Utilizando o lambdify do Sympy
i = ModeloUmDiodo(4, 0.55)
%timeit i.corrente_nominal_lamb(600,310)
i.plota_grafico_corrente_lamb()
```

```python

```


##  Tarea 1-09-2020

```python
!pip install sympy
```

    Requirement already satisfied: sympy in c:\users\dell\documents\andres\lib\site-packages (1.4)
    Requirement already satisfied: mpmath>=0.19 in c:\users\dell\documents\andres\lib\site-packages (from sympy) (1.1.0)

```python
from sympy import *
from sympy.plotting import *
init_printing(use_latex=True)
```

##   Punto 1

## 1.

```python
x,t = symbols("x,t")
```

```python
f=(x-t+1)/(x**2+t**2)
f
```

```python
def evaluar(f,a,b):
    z=f.subs([(x,a),(t,b)])
    return z
```

```python
evaluar (f,2,1)
```

```python
evaluar (f,3,0.5)
```

```python
evaluar (f,-1/4,3/4)
```

## 2.

```python
x,y = symbols("x,y")
```

```python
f=3*𝑥**2-5*𝑥*𝑦+𝑦**3
f
```

```python
def evaluar(f,a,b):
    z=f.subs([(x,a),(y,b)])
    return z
```

```python
evaluar (f,1,2)
```

```python
evaluar (f,-1,1-5)
```

```python
evaluar (f,3,-2)
```

## 3.

```python
x,y = symbols("x,y")
```

```python
g= E**(2*x**2*y-5*x+2*y)
g
```

```python
def evaluar(g,a,b):
    z=g.subs([(x,a),(y,b)])
    return z
```

```python
evaluar (g,0,0)
```

```python
evaluar (g,-1,1)
```

## 4

```python
x,y = symbols("x,y")
```

```python
h= log(x**2+4*x+y**2-1)
h
```

```python
def evaluar(h,a,b):
    z=h.subs([(x,a),(y,b)])
    return z
```

```python
evaluar (h,1,2)
```

```python
evaluar (h,0,-0.5)
```

```python
evaluar (h,1/2,1/2)
```

## Punto 2

1. Una empresa produce dos productos, $X$ y $Y$. Las unidades de costos de mano de obra y de materiales
   son de 5 dólares en el caso del producto $X$ y de 12 dólares por lo que
   respecta a $Y$. Además, la empresa también tiene costos fijos
   de  3000 dólares al mes. Exprese el costo mensual $C$ (en dólares)
   como una función de las unidades de $X$ y $Y$ producidas.
   ¿Cuál es el costo total de producir 200 unidades de $X$ y 150
   unidades de $Y$?

```python
x,y = symbols("x,y")
```

```python
c=5*x+12*y+3000
c
```

```python
def evaluar(c,a,b):
    z=c.subs([(x,a),(t,b)])
    return z
```

```python
c.subs([(x,200),(y,150)])

```

2. Se lanza un nuevo producto al mercado. El volumen de ventas $x$ se incrementa como una función del tiempo $t$ y depende        también de la cantidad $A$ gastada en la campaña publicitaria. Si, con $t$ medido en meses y A en dólares,

   $$x=200(5-e^{-0.002A})(1-e^{-t})$$

   Determine el volumen de ventas si ha transcurrido 1 mes y la inversión fue de 400 dólares.

```python
x=200*(5-E**-0.002*400)*(1-E**-1)
x
```

3. La función de producción de cierta empresa está dada por

   $$P=5L + 2L^2+ 3LK + 8K + 3K^2$$

   en donde $L$ es el insumo mano de obra medido en miles de horas-hombre por semana, $K$ es el monto de capital invertido        medido en miles de dólares por semana y $P$ es la producción semanal en miles de artículos. Determine la productividad          cuando $L=5$ y $K=12$.

```python
l,k = symbols("l,k")
```

```python
P=5*l+2*l**2+3*l*k+8*k+3*k**2
P
```

```python
def evaluar(P,a,b):
    z=P.subs([(l,a),(k,b)])
    return z
```

```python
evaluar (P,5,12)
```

4. Suponga que el costo de producción de una empresa que fabrica    dos artículos $A$ y $B$ viene dada por

   $$C(x, y) = 1500 -7.5x- 15y-0.3xy+0.3x^2 + 0.2y^2$$

   Si la empresa puede vender cada unidad de $A$ a 20 dólares y      cada unidad de $B$ a
   16 dólares, encuentre la función de utilidad por la producción    y venta de los articulos $A$ y $B$.

```python
x,y = symbols("x,y")
```

```python
c=1500-7.5*x-15*y-0.3*x*y+0.3*x**2+0.2*y**2
c
```

```python
i=1500-7.5*20-15*16-0.3*20*16+0.3*20**2+0.2*16**2
i
```

```python
u=i-c
u
```

5. Juguetes Mónica produce dos tipos diferentes de
   cochecitos de plástico con un costo de 300 pesos y 900 pesos      cada uno. Las demandas anuales $x_1$ y $x_2$ (en miles) están    dadas por

    $$x_1 = 30 + 2p_2 - 5p_1 \ \ \ \  y \ \ \ \ x_2 = 100 + p_1 -     2p_2 $$

   con $p_1$ y $p_2$ los precios unitarios (en pesos) de los dos    tipos de cochecitos. Determine la utilidad en función de los      precios $p_1$ y $p_2$.

```python
p1,p2 = symbols("p1,p2")
```

```python
c=30+2*p2-5*p1
c
```

```python
i=30+2*900-5*300
i
```

```python
u=i-c
u
```

```python
c=100+p1-2*p2
c
```

```python
i=100+300-2*900
i
```

```python
u=i-c
u
```

6. Los costos de producción de una empresa que fabrica los artículos $A,\ B,\ C$ y $D$ vienen dados por
$$C(x,y,z,w)=0.1x+2.2y+1.7z+4w+2xy+0.5zw+2000$$
    siendo $x,\ y,\ z$ y $w$ la cantidad de artículos producidos de los productos $A,\ B,\ C$ y $D$ respectivamente. Halle el costo de fabricar 200 unidades de $A$, 450 de $B$, 635 de $C$, y 1230 de $D$.

```python
x,y,z,w = symbols("x,y,z,w")
```

```python
c=0.1*x+2.2*y+1.7*z+4*w+2*x*y+0.5*z*w+2000
c
```

```python
c.subs([(x,200),(y,450),(z,635),(w,1230)])
```

7. Halle la función de utilidades de la empresa del punto anterior si se sabe que los productos $A,\ B,\ C$ y $D$ se pueden vender cada uno a $2\ ,3\ ,2$ y $6$ dólares cada uno.

```python
x,y,z,w = symbols("x,y,z,w")
```

```python
c=0.1*x+2.2*y+1.7*z+4*w+2*x*y+0.5*z*w+2000
c
```

```python
i=0.1*2+2.2*3+1.7*2+4*6+2*2*3+0.5*2*6+2000
i
```

```python
u=i-c
u
```

##   Punto 3

## 1.

```python
f=3*x**2*y**2
f
```

```python
dfx=diff(f,x)
dfx
```

```python
dfxy=diff(dfx,y)
dfxy
```

## 2.

```python
g= 2*x/(3*x**2+2*y**3)
g
```

```python
dgx=diff(g,x)
dgx
```

```python
dgxy=diff(dgx,y)
dgxy
```

## 3.

```python
h= E**(2*x+3*y-1)
h
```

```python
dhx=diff(h,x)
dhx
```

```python
dhxy=diff(dhx,y)
dhxy
```

```python

```


# An Introduction to Bayesian Statistical Analysis

Before we jump in to model-building and using MCMC to do wonderful things, it is useful to understand a few of the theoretical underpinnings of the Bayesian statistical paradigm. A little theory (and I do mean a *little*) goes a long way towards being able to apply the methods correctly and effectively.

There are several introductory references to Bayesian statistics that go well beyond what we will cover here.

## What *is* Bayesian Statistical Analysis?

Though many of you will have taken a statistics course or two during your undergraduate (or graduate) education, most of those who have will likely not have had a course in *Bayesian* statistics. Most introductory courses, particularly for non-statisticians, still do not cover Bayesian methods at all, except perhaps to derive Bayes' formula as a trivial rearrangement of the definition of conditional probability. Even today, Bayesian courses are typically tacked onto the curriculum, rather than being integrated into the program.

In fact, Bayesian statistics is not just a particular method, or even a class of methods; it is an entirely different paradigm for doing statistical analysis.

> Practical methods for making inferences from data using probability models for quantities we observe and about which we wish to learn.
*-- Gelman et al. 2013*

A Bayesian model is described by parameters, uncertainty in those parameters is described using probability distributions.

All conclusions from Bayesian statistical procedures are stated in terms of *probability statements*

This confers several benefits to the analyst, including:

- ease of interpretation, summarization of uncertainty
- can incorporate uncertainty in parent parameters
- easy to calculate summary statistics

## Bayesian vs Frequentist Statistics: What's the difference?

See the [VanderPlas talk on Tuesday](https://conference.scipy.org/scipy2014/schedule/presentation/444/).

Any statistical paradigm, Bayesian or otherwise, involves at least the following:

1. Some **unknown quantities** about which we are interested in learning or testing. We call these *parameters*.
2. Some **data** which have been observed, and hopefully contain information about (1).
3. One or more **models** that relate the data to the parameters, and is the instrument that is used to learn.

### The Frequentist World View

- The data that have been observed are considered **random**, because they are realizations of random processes, and hence will vary each time one goes to observe the system.
- Model parameters are considered **fixed**. The parameters' values are unknown, but they are fixed, and so we *condition* on them.

In mathematical notation, this implies a (very) general model of the following form:

\\[f(y | \theta)\\]

Here, the model \\(f\\) accepts data values \\(y\\) as an argument, conditional on particular values of \\(\theta\\).

Frequentist inference typically involves deriving **estimators** for the unknown parameters. Estimators are formulae that return estimates for particular estimands, as a function of data. They are selected based on some chosen optimality criterion, such as *unbiasedness*, *variance minimization*, or *efficiency*.

> For example, lets say that we have collected some data on the prevalence of autism spectrum disorder (ASD) in some defined population. Our sample includes \\(n\\) sampled children, \\(y\\) of them having been diagnosed with autism. A frequentist estimator of the prevalence \\(p\\) is:

>

> \\[\hat{p} = \frac{y}{n}\\]
>

> Why this particular function? Because it can be shown to be unbiased and minimum-variance.

It is important to note that new estimators need to be derived for every estimand that is introduced.

### The Bayesian World View

- Data are considered **fixed**. They used to be random, but once they were written into your lab notebook/spreadsheet/IPython notebook they do not change.
- Model parameters themselves may not be random, but Bayesians use probability distribtutions to describe their uncertainty in parameter values, and are therefore treated as **random**. In some cases, it is useful to consider parameters as having been sampled from probability distributions.

This implies the following form:

\\[p(\theta | y)\\]

This formulation used to be referred to as ***inverse probability***, because it infers from observations to parameters, or from effects to causes.

Bayesians do not seek new estimators for every estimation problem they encounter. There is only one estimator for Bayesian inference: **Bayes' Formula**.

## Bayesian Inference, in 3 Easy Steps

Gelman et al. (2013) describe the process of conducting Bayesian statistical analysis in 3 steps.

### Step 1: Specify a probability model

As was noted above, Bayesian statistics involves using probability models to solve problems. So, the first task is to *completely specify* the model in terms of probability distributions. This includes everything: unknown parameters, data, covariates, missing data, predictions. All must be assigned some probability density.

This step involves making choices.

- what is the form of the sampling distribution of the data?
- what form best describes our uncertainty in the unknown parameters?

### Step 2: Calculate a posterior distribution

The mathematical form \\(p(\theta | y)\\) that we associated with the Bayesian approach is referred to as a **posterior distribution**.

> posterior /pos·ter·i·or/ (pos-tēr´e-er) later in time; subsequent.

Why posterior? Because it tells us what we know about the unknown \\(\theta\\) *after* having observed \\(y\\).

This posterior distribution is formulated as a function of the probability model that was specified in Step 1. Usually, we can write it down but we cannot calculate it analytically. In fact, the difficulty inherent in calculating the posterior distribution for most models of interest is perhaps the major contributing factor for the lack of widespread adoption of Bayesian methods for data analysis. Various strategies for doing so comprise this tutorial.

**But**, once the posterior distribution is calculated, you get a lot for free:

- point estimates
- credible intervals
- quantiles
- predictions

### Step 3: Check your model

Though frequently ignored in practice, it is critical that the model and its outputs be assessed before using the outputs for inference. Models are specified based on assumptions that are largely unverifiable, so the least we can do is examine the output in detail, relative to the specified model and the data that were used to fit the model.

Specifically, we must ask:

- does the model fit data?
- are the conclusions reasonable?
- are the outputs sensitive to changes in model structure?

## Why be Bayesian?

At this point, it is worth addressing the question of why one might consider an alternative statistical paradigm to the classical/frequentist statistical approach. After all, it is not always easy to specify a full probabilistic model, nor to obtain output from the model once it is specified. So, why bother?

> ... the Bayesian approach is attractive because it is useful. Its usefulness derives in large measure from its simplicity. Its simplicity allows the investigation of far more complex models than can be handled by the tools in the classical toolbox.
*-- Link and Barker 2010*

We already noted that there is just one estimator in Bayesian inference, which lends to its ***simplicity***. Moreover, Bayes affords a conceptually simple way of coping with multiple parameters; the use of probabilistic models allows very complex models to be assembled in a modular fashion, by factoring a large joint model into the product of several conditional probabilities.

Bayesian statistics is also attractive for its ***coherence***. All unknown quantities for a particular problem are treated as random variables, to be estimated in the same way. Existing knowledge is given precise mathematical expression, allowing it to be integrated with information from the study dataset, and there is formal mechanism for incorporating new information into an existing analysis.

Finally, Bayesian statistics confers an advantage in the ***iterpretability*** of analytic outputs. Because models are expressed probabilistically, results can be interpreted probabilistically. Probabilities are easy for users (particularly non-technical users) to understand and apply.

### Example: confidence vs. credible intervals

A commonly-used measure of uncertainty for a statistical point estimate in classical statistics is the ***confidence interval***. Most scientists were introduced to the confidence interval during their introductory statistics course(s) in college. Yet, a large number of users mis-interpret the confidence interval.

Here is the mathematical definition of a 95% confidence interval for some unknown scalar quantity that we will here call \\(\theta\\):

\\[Pr(a(Y) < \theta < b(Y) | \theta) = 0.95\\]

how the endpoints of this interval are calculated varies according to the sampling distribution of \\(Y\\), but for as an example, the confidence interval for the population mean when \\(Y\\) is normally distributed is calculated by:

\\[Pr(\bar{Y} - 1.96\frac{\sigma}{\sqrt{n}}< \theta < \bar{Y} + 1.96\frac{\sigma}{\sqrt{n}}) = 0.95\\]

It would be tempting to use this definition to conclude that there is a 95% chance \\(\theta\\) is between \\(a(Y)\\) and \\(b(Y)\\), but that would be a mistake.

Recall that for frequentists, unknown parameters are **fixed**, which means there is no probability associated with them being any value except what they are fixed to. Here, the interval itself, and not \\(\theta\\) is the random variable. The actual interval calculated from the data is just one possible realization of a random process, and it must be strictly interpreted only in relation to an infinite sequence of identical trials that might be (but never are) conducted in practice.

A valid interpretation of the above would be:

> If the experiment were repeated an infinite number of times, 95% of the calculated intervals would contain \\(\theta\\).

This is what the statistical notion of "confidence" entails, and this sets it apart from probability intervals.

Since they regard unknown parameters as random variables, Bayesians can and do use probability intervals to describe what is known about the value of an unknown quantity. These intervals are commonly known as ***credible intervals***.

The definition of a 95% credible interval is:

\\[Pr(a(y) < \theta  There is a 95% chance \\(\theta\\) is between \\(a\\) and \\(b\\).

Hence, the credible interval is a statement of what we know about the value of \\(\theta\\) based on the observed data.

## Probability

> *Misunderstanding of probability may be the greatest of all impediments to scientific literacy.*
> — Stephen Jay Gould

Because of its reliance on probabilty models, its worth talking a little bit about probability. There are three different ways to define probability, depending on how it is being used.

### 1. Classical probability

\\[Pr(X=x) = \frac{\text{# x outcomes}}{\text{# possible outcomes}}\\]

Classical probability is an assessment of **possible** outcomes of elementary events. Elementary events are assumed to be equally likely.

### 2. Frequentist probability

\\[Pr(X=x) = \lim_{n \rightarrow \infty} \frac{\text{# times x has occurred}}{\text{# independent and identical trials}}\\]

Unlike classical probability, frequentist probability is an EMPIRICAL definition. It is an objective statement desribing events that have occurred.

### 3. Subjective probability

\\[Pr(X=x)\\]

Subjective probability is a measure of one's uncertainty in the value of \\(X\\). It characterizes the state of knowledge regarding some unknown quantity using probability.

It is not associated with long-term frequencies nor with equal-probability events.

For example:

- X = the true prevalence of diabetes in Austin is < 15%
- X = the blood type of the person sitting next to you is type A
- X = the Nashville Predators will win next year's Stanley Cup
- X = it is raining in Nashville

## Probability distributions

Bayesian inference uses probability distributions as building blocks for specifying models. They are used to assign uncertainty to unknown parameters, or sampling distributions to data.

Probability distributions assign probabilities to each possible outcome or value of a particular variable. Probabilities must be **positive**, and the sum (or integral) of the probabilities of all possible values must be 1.

There are two distinct classes of distributions, determined by whether the variable being described is **discrete** or **continuous**.

### Discrete Probability Distributions

$$X = \{0,1\}$$

$$Y = \{\ldots,-2,-1,0,1,2,\ldots\}$$

**Probability Mass Function**:

For discrete $X$,

$$Pr(X=x) = f(x|\theta)$$

***e.g. Poisson distribution***

The Poisson distribution models unbounded counts:

$$Pr(X=x)=\frac{e^{-\lambda}\lambda^x}{x!}$$

* $X=\{0,1,2,\ldots\}$
* $\lambda > 0$

$$E(X) = \text{Var}(X) = \lambda$$

```python
%matplotlib inline
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

theta = 3
x = np.random.poisson(theta, size=1000)
x.mean(), x.var()
```

    (2.9500000000000002, 3.1594999999999929)

```python
_ = plt.hist(x, bins=30)
```

```python
y = stats.poisson.pmf(range(10), theta)
plt.plot(y, 'ro')
```

### Continuous Random Variables

$$X \in [0,1]$$

$$Y \in (-\infty, \infty)$$

**Probability Density Function**:

For continuous $X$,

$$Pr(x \le X \le x + dx) = f(x|\theta)dx \, \text{ as } \, dx \rightarrow 0$$

***e.g. normal distribution***

$$f(x) = \frac{1}{\sqrt{2\pi\sigma^2}}\exp\left[-\frac{(x-\mu)^2}{2\sigma^2}\right]$$

* $X \in \mathbf{R}$
* $\mu \in \mathbf{R}$
* $\sigma>0$

$$\begin{align}E(X) &= \mu \cr
\text{Var}(X) &= \sigma^2 \end{align}$$

```python
mu, sig = 10, 3

x = np.random.normal(mu, sig, 1000)
x.mean(), x.std()
```

    (9.9980174423479724, 2.990905875326102)

```python
_ = plt.hist(x, bins=30)
```

```python
xvals = np.linspace(-10, 30)
y = stats.norm.pdf(xvals, mu, sig)
plt.plot(xvals, y, 'ro')
```

As a reference, the PyMC provides a description of most of the commonly-encountered probability distibutions used in statistical analysis.

```python
from IPython.display import HTML
HTML('')
```

## Bayes' Formula

Now that we have some probability under our belt, we turn to Bayes' formula. This, as you recall, is the engine that allows us to obtain estimates of unknown quantities that we care about, using information retained by the data we observe. It turns out to be quite simple to derive Bayes' formula directly from the definition of conditional probability.

Again, the goal in Bayesian inference is to calculate the **posterior distribution** of our unknowns:

\\[Pr(\theta|Y=y)\\]

This expression is a **conditional probability**. It is the probability of \\(\theta\\) *given* the observed values of \\(Y=y\\).

In general, the conditional probability of A given B is defined as follows:

\\[Pr(B|A) = \frac{Pr(A \cap B)}{Pr(A)}\\]

To gain an intuition for this, it is helpful to use a Venn diagram:

Notice from this diagram that the following conditional probability is also true:

\\[Pr(A|B) = \frac{Pr(A \cap B)}{Pr(B)}\\]

These can both be rearranged to be expressions of the joint probability of A and B. Setting these equal to one another:

\\[Pr(B|A)Pr(A) = Pr(A|B)Pr(B)\\]

Then rearranging:

\\[Pr(B|A) = \frac{Pr(A|B)Pr(B)}{Pr(A)}\\]

This is Bayes' formula. Replacing the generic A and B with things we care about reveals why Bayes' formula is so important:

The equation expresses how our belief about the value of \\(\theta\\), as expressed by the **prior distribution** \\(P(\theta)\\) is reallocated following the observation of the data \\(y\\), as expressed by the posterior distribution the posterior distribution.

The innocuous denominator \\(P(y)\\) cannot be calculated directly, and is actually the expression in the numerator, integrated over all \\(\theta\\):

\\[Pr(\theta|y) = \frac{Pr(y|\theta)Pr(\theta)}{\int Pr(y|\theta)Pr(\theta) d\theta}\\]

The intractability of this integral is one of the factors that has contributed to the under-utilization of Bayesian methods by statisticians.

### Priors

Once considered a controversial aspect of Bayesian analysis, the prior distribution characterizes what is known about an unknown quantity before observing the data from the present study. Thus, it represents the information state of that parameter. It can be used to reflect the information obtained in previous studies, to constrain the parameter to plausible values, or to represent the population of possible parameter values, of which the current study's parameter value can be considered a sample.

### Likelihood functions

The likelihood represents the information in the observed data, and is used to update prior distributions to posterior distributions. This updating of belief is justified becuase of the **likelihood principle**, which states:

> Following observation of \\(y\\), the likelihood \\(L(\theta|y)\\) contains all experimental information from \\(y\\) about the unknown \\(\theta\\).

Bayesian analysis satisfies the likelihood principle because the posterior distribution's dependence on the data is only through the likelihood. In comparison, most frequentist inference procedures violate the likelihood principle, because inference will depend on the design of the trial or experiment.

What is a likelihood function? It is closely related to the probability density (or mass) function. Taking a common example, consider some data that are binomially distributed (that is, they describe the outcomes of \\(n\\) binary events). Here is the binomial sampling distribution:

\\[p(Y|\theta) = {n \choose y} \theta^{y} (1-\theta)^{n-y}\\]

We can code this easily in Python:

```python
from scipy.misc import comb

pbinom = lambda y, n, p: comb(n, y) * p**y * (1-p)**(n-y)
```

This function returns the probability of observing \\(y\\) events from \\(n\\) trials, where events occur independently with probability \\(p\\).

```python
pbinom(3, 10, 0.5)
```

    0.1171875

```python
pbinom(1, 25, 0.5)
```

    7.4505805969238281e-07

```python
yvals = range(10+1)
plt.plot(yvals, [pbinom(y, 10, 0.5) for y in yvals], 'ro')
```

What about the likelihood function?

The likelihood function is the exact same form as the sampling distribution, except that we are now interested in varying the parameter for a given dataset.

```python
pvals = np.linspace(0, 1)
y = 4
plt.plot(pvals, [pbinom(y, 10, p) for p in pvals])
```

So, though we are dealing with the same equation, these are entirely different functions; the distribution is discrete, while the likelihood is continuous; the distribtion's range is from 0 to 10, while the likelihood's is 0 to 1; the distribution integrates (sums) to one, while the likelhood does not.

## Example: Genetic probabilities

Let's put Bayesian inference into action using a very simple example. I've chosen this example because it is one of the rare occasions where the posterior can be calculated by hand. We will show how data can be used to update our belief in competing hypotheses.

Hemophilia is a rare genetic disorder that impairs the ability for the body's clotting factors to coagualate the blood in response to broken blood vessels. The disease is an **x-linked recessive** trait, meaning that there is only one copy of the gene in males but two in males, and the trait can be masked by the dominant allele of the gene.

This implies that males with 1 gene are *affected*, while females with 1 gene are unaffected, but *carriers* of the disease. Having 2 copies of the disease is fatal, so this genotype does not exist in the population.

In this example, consider a woman whose mother is a carrier (because her brother is affected) and who marries an unaffected man. Let's now observe some data: the woman has two consecutive (non-twin) sons who are unaffected. We are interested in determining **if the woman is a carrier**.

To set up this problem, we need to set up our probability model. The unknown quantity of interest is simply an indicator variable \\(W\\) that equals 1 if the woman is affected, and zero if she is not. We are interested in the probability that the variable equals one, given what we have observed:

\\[Pr(W=1 | s_1=0, s_2=0)\\]

Our prior information is based on what we know about the woman's ancestry: her mother was a carrier. Hence, the prior is \\(Pr(W=1) = 0.5\\). Another way of expressing this is in terms of the **prior odds**, or:

\\[O(W=1) = \frac{Pr(W=1)}{Pr(W=0)} = 1\\]

Now for the likelihood: The form of this function is:

\\[L(W | s_1=0, s_2=0)\\]

This can be calculated as the probability of observing the data for any passed value for the parameter. For this simple problem, the likelihood takes only two possible values:

\\[\begin{aligned}
L(W=1 &| s_1=0, s_2=0) = (0.5)(0.5) = 0.25 \cr
L(W=0 &| s_1=0, s_2=0) = (1)(1) = 1
\end{aligned}\\]

With all the pieces in place, we can now apply Bayes' formula to calculate the posterior probability that the woman is a carrier:

\\[\begin{aligned}
Pr(W=1 | s_1=0, s_2=0) &= \frac{L(W=1 | s_1=0, s_2=0) Pr(W=1)}{L(W=1 | s_1=0, s_2=0) Pr(W=1) + L(W=0 | s_1=0, s_2=0) Pr(W=0)} \cr
 &= \frac{(0.25)(0.5)}{(0.25)(0.5) + (1)(0.5)} \cr
 &= 0.2
\end{aligned}\\]

Hence, there is a 0.2 probability of the woman being a carrier.

Its a bit trivial, but we can code this in Python:

```python
prior = 0.5
p = 0.5

L = lambda w, s: np.prod([(1-i, p**i * (1-p)**(1-i))[w] for i in s])
```

```python
s = [0,0]

post = L(1, s) * prior / (L(1, s) * prior + L(0, s) * (1 - prior))
post
```

    0.20000000000000001

Now, what happens if the woman has a third unaffected child? What is our estimate of her probability of being a carrier then?

Bayes' formula makes it easy to update analyses with new information, in a sequential fashion. We simply assign the posterior from the previous analysis to be the prior for the new analysis, and proceed as before:

```python
L(1, [0])
```

    0.5

```python
s = [0]
prior = post

L(1, s) * prior / (L(1, s) * prior + L(0, s) * (1 - prior))
```

    0.11111111111111112

Thus, observing a third unaffected child has further reduced our belief that the mother is a carrier.

---

## References

Gelman A, Carlin JB, Stern HS, Dunson DB, Vehtari A, Rubin DB. Bayesian Data Analysis, Third Edition. CRC Press; 2013.

```python
from IPython.core.display import HTML
def css_styling():
    styles = open("styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

notebooks/.ipynb_checkpoints/7. Introduction to Bayes-checkpoint.ipynb


# Integrerte hastighetslover
Her skal vi integrere [0. ordens](#0.-ordens-reaksjon), [1. ordens](#1.-ordens-reaksjon) og [2. ordens](#2.-ordens-reaksjon) hastighetslover. Målet vårt er å bruke definisjonen på reaksjonshastighet, sammen med hastighetsloven, for å finne integrerte hastighetslover.

For å gjøre det mer konkret, ser vi på reaksjonen,

$$\text{A} \to \text{produkter},$$

der reaksjonshastigheten, $r$, er gitt ved,

$$r = -\frac{\text{d} [\text{A}]}{\text{d} t}.$$

Hastighetsloven er,

$$r = k [\text{A}]^\alpha,$$

der $k$ er hastighetskonstanten og $\alpha$ er reaksjonens orden. Setter vi disse to uttrykkene for $r$ lik hverandre, så får vi en differensialligning vi kan løse,

$$-\frac{\text{d} [\text{A}]}{\text{d} t} = k [\text{A}]^\alpha.$$

I differensialligningen er det *funksjonen* $[\text{A}]$ som er ukjent. Vi
skal løse differensialligningen ved å bruke [SymPy](https://www.sympy.org/).

## 0. ordens reaksjon
For en 0. ordens reaksjon setter vi $\alpha = 0$ og får da følgende differensialligning,

$$-\frac{\text{d} [\text{A}]}{\text{d} t} = k.$$

```python
import sympy as sym
```

```python
# Definer variabler:
k, t = sym.symbols('k t', positive=True, real=True)
# Definer [A] som ukjent, men fortell SymPy at dette er en funksjon:
A = sym.Function('A')(t)
```

```python
# Definer differensialligningen vi skal løse:
ligning0 = sym.Eq(-sym.Derivative(A,t), k)
# Skriv ut ligningen for å sjekke at den ser riktig ut:
ligning0
```

```python
# Her definerer vi at konsentrasjonen ved t = 0 er A0:
A0 = sym.symbols('A0', positive=True, real=True)
start = {A.subs(t, 0): A0}
# Løs differensialligningen:
løsning0 = sym.dsolve(ligning0, ics=start)
løsning0
```

Den integrerte hastighetsloven for en 0. ordens reaksjon blir altså,

$$ [\text{A}] = [\text{A}]_0 - kt.$$

Vi kan også prøve å finne halveringstiden ($t_{1/2}$) som er tiden det tar til halvparten av opprinnelig mengde av A er igjen. Dvs. tiden som oppfyller:

$$ [\text{A}] = \frac{[\text{A}]_0}{2} = [\text{A}]_0 - kt_{1/2}.$$

```python
# Ved halveringstiden er halvparten av opprinnelig mengde A igjen, dvs A(t) = A0 / 2.
# Vi skriver dette som en ligning:
halv_0 = løsning0.subs({A: A0/2})
halv_0
```

```python
# Og løser den:
sym.solve(halv_0)
```

Halveringstiden er altså bestemt ved,

$$ \text{A}_0 = 2 k t_{1/2} \implies t_{1/2} = \frac{\text{A}_0}{ 2 k}.$$

## 1. ordens reaksjon
For en 1. ordens reaksjon setter vi $\alpha = 1$ og får da følgende differensialligning,

$$-\frac{\text{d} [\text{A}]}{\text{d} t} = k [\text{A}].$$

```python
# Definer differensialligningen vi skal løse:
ligning1 = sym.Eq(-sym.Derivative(A,t), k * A)
# Skriv ut ligningen for å sjekke at den ser riktig ut:
ligning1
```

```python
# Løs differensialligningen:
løsning1 = sym.dsolve(ligning1, ics=start)
løsning1
```

```python
# Vi kan også skrive ligningen på logaritmisk form:
løsning11 = sym.Eq(sym.log(løsning1.lhs), sym.log(løsning1.rhs))
sym.simplify(løsning11)
```

Den integrerte hastighetsloven for en 1. ordens reaksjon blir altså,

$$ \ln [\text{A}] = \ln [\text{A}]_0 - kt.$$

Vi kan også prøve å finne halveringstiden ($t_{1/2}$) som er tiden det tar til halvparten av opprinnelig mengde av A er igjen. Dvs. tiden som oppfyller:

$$ \ln [\text{A}] = \ln \left( \frac{[\text{A}]_0}{2} \right) = \ln [\text{A}]_0 - kt_{1/2}.$$

```python
# Ved halveringstiden er halvparten av opprinnelig mengde A igjen, dvs A(t) = A0 / 2.
# Vi skriver dette som en ligning:
halv_1 = løsning1.subs({A: A0/2})
halv_1
```

```python
# Og løser den:
sym.solve(halv_1)
```

Her finner vi at halveringstiden oppfyller,

$$ k = \frac{\ln 2}{t_{1/2}} \implies t_{1/2} = \frac{\ln 2}{k} .$$

(Vi merker oss at denne halveringstiden er *uavhengig* av startkonsentrasjonen.)

## 2. ordens reaksjon
For en 2. ordens reaksjon setter vi $\alpha = 2$ og får da følgende differensialligning,

$$-\frac{\text{d} [\text{A}]}{\text{d} t} = k [\text{A}]^2.$$

```python
# Definer differensialligningen vi skal løse:
ligning2 = sym.Eq(-sym.Derivative(A,t), k * A**2)
# Skriv ut ligningen for å sjekke at den ser riktig ut:
ligning2
```

```python
# Løs differensialligningen:
løsning2 = sym.dsolve(ligning2, ics=start)
løsning2
```

Denne løsningen ser kanskje litt forskjellig ut fra læreboken, la oss skrive den om:

```python
løsning22 = sym.Eq(1 / løsning2.lhs, 1 / løsning2.rhs)
løsning22
```

Den integrerte hastighetsloven for en 2. ordens reaksjon blir altså,

$$ \frac{1}{[\text{A}]} = \frac{1}{[\text{A}]_0} + kt.$$

Vi kan også prøve å finne halveringstiden ($t_{1/2}$) som er tiden det tar til halvparten av opprinnelig mengde av A er igjen. Dvs. tiden som oppfyller:

$$ \frac{1}{[\text{A}]} = \frac{1}{\frac{[\text{A}]_0}{2}} = \frac{2}{[\text{A}]_0} = \frac{1}{[\text{A}]_0} + kt_{1/2}.$$

```python
# Ved halveringstiden er halvparten av opprinnelig mengde A igjen, dvs A(t) = A0 / 2.
# Vi skriver dette som en ligning:
halv_2 = løsning22.subs({A: A0/2})
halv_2
```

```python
# Og løser den:
sym.solve(halv_2)
```

Halveringstiden er gitt ved,

$$[\text{A}_0] = \frac{1}{k t_{1/2}} \implies t_{1/2} = \frac{1}{k [\text{A}_0]}.$$

## Oppsummering

Vi har funnet integrerte lover ved å løse differensialligningene. Vi fant:

| Orden | Integrert hastighetslov                                     | Halveringstid                            |
|-------|-------------------------------------------------------------|------------------------------------------|
| 0     | $[\text{A}]_t = [\text{A}]_0  - k t$                        | $t_{1/2} = \frac{[\text{A}]_0}{2 k}$     |
| 1     | $[\text{A}]_t = [\text{A}]_0 \text{e}^{-kt}$                | $t_{1/2} = \frac{\ln 2}{k}$              |
| 2     | $\frac{1}{[\text{A}]_t} = \frac{1}{[\text{A}]_0} + kt$      | $t_{1/2} = \frac{1}{k [\text{A}]_0}$     |
| 3     | $\frac{1}{[\text{A}]_t^2} = \frac{1}{[\text{A}]_0^2} + 2kt$ | $t_{1/2} = \frac{3}{2 k [\text{A}]_0^2}$ |

I tabellen over har vi også tatt med en 3. ordens hastighetslov. Kan du vise at dette stemmer (f.eks. ved å endre Python-koden over)? Og, litt ekstra utfordring for de som har matematikk-fag med differensialligninger, kan du løse det generelt for en orden $n \geq 2$? Dvs. for

$$-\frac{\text{d} [\text{A}]}{\text{d} t} = k [\text{A}]^n, \quad n \geq 2$$

kan du finne en generell integrert hastighetslov?


```python
import numpy as np
import matplotlib.pyplot as plt
from sympy import *
import warnings
warnings.filterwarnings('ignore')

rho = 5  # 自然利率
alpha = 0.2  # 资本份额
beta = 0.99  # 跨期贴现率
sigma = 1  # 消费的跨期替代弹性的倒数
theta = 3/4  # 名义粘性指数（一个季度内价格不变的企业比例）
phi = 5  # 劳动的跨期替代弹性的倒数（其倒数是Fisher劳动供给弹性）
e = 9  # 产品的替代弹性
lam = (1-theta)/theta*(1-beta*theta)*(1-alpha)/(1-alpha+alpha*e);
psi_ya = (1+phi)/(sigma*(1-alpha)+phi+alpha)
k = (sigma+(phi+alpha)/(1-alpha))*lam  # NKPC 参数 ：pi_t = E pi_{t+1} + k x_t + u_t
Theta = k/e  # 福利损失函数中产出缺口相对于通胀率的比重
gamma = Theta/(k**2+Theta*(1+beta))  # \hat{p}_t = \gamma*\hat{p}_{t-1}+\gamma\beta\hat{p}_{t+1} +\kappa\Lambda/Theta \delta/(1-\beta\delta\rho_u)u_t
delta = (1-np.sqrt(1-4*gamma**2*beta))/(2*gamma*beta)  # 由上式迭代得出的价格的一阶自回归系数
Lambda = lam/e**2  # 平衡路径不同于有效路径的前提下，福利损失函数中产出波动的一阶项系数

T = 12  # 总期数
tz = 5  # 在前tz期，自然利率为负，然后回到正常水平rho
xpi = np.zeros((2,T+1))  # 第0期到第12期
er = 4  # 负实际利率

# 一下两个矩阵刻画了产出缺口（这里指同有效产出的缺口，而非同自然产出的缺口）与通胀的动态
A = Matrix([[1,1/sigma],[k,beta+k/sigma]])
B = Matrix([[1/sigma],[k/sigma]])

# 名义和实际利率
r = np.append(np.ones(tz+1)*-er,np.ones(T-tz)*rho)
it = np.append(np.zeros(tz+1),np.ones(T-tz)*rho)

# Case1 : 自由裁量（相机决策）
# 央行每一期单独优化，而不是考虑全时期总体优化，在冲击结束后，产出缺口和通胀都立即回到0
# 由于每一期的决策只取决于当期的影响，因此称“相机决策”
for i in range(tz+1):
    xpi[:,tz-i:tz+1-i] = A@xpi[:,tz+1-i:tz+2-i]-er*B

# Case2 : 可信承诺（前瞻指引）
# 央行最小化损失函数贴现和，尽管有更多时期产出缺口为负，但是显著减小冲击在初期造成的影响，由于效用函数的凹性，总的福利损失比相机决策少。
tc = 7  # 在7期以后才让利率回升
xpi_c = np.zeros((2,T+1))
M = np.linalg.inv(np.array([[-k,1+beta*(1-delta)],[beta*(1-delta)+k**2/Theta,-k/Theta]]))@np.array([[beta*(1-delta),(1-delta)/sigma],[0,(1-delta)/Theta]])
H = Matrix([[1,1/beta/sigma],[k,1/beta*(1+k/sigma)]])
J = np.array([[0,1],[Theta,k]])

# 第tc+1期是一个转折点，这一期的状态变量待定
x = Symbol('x')
y = Symbol('y')

# 利用动态方程（以及初值）迭代求解tc+1期的状态变量
xpi_c = Matrix(xpi_c)
xpi_c[:,tc+1:tc+2] = np.array([[x],[y]])
for i in range(tc-tz):
    xpi_c[:,tc-i:tc+1-i] = A@xpi_c[:,tc+1-i:tc+2-i]+rho*B
for i in range(tz+1):
    xpi_c[:,tz-i:tz+1-i] = A@xpi_c[:,tz+1-i:tz+2-i]-er*B
summ = Matrix(np.zeros_like(xpi_c[:,tc+1:tc+2]))
for j in range(tc+1):
    summ += H**j@J@Matrix(xpi_c[:,tc-j:tc+1-j])
sol = solve(M@summ+xpi_c[:,tc+1:tc+2],[x,y])
xpi_c[:,tc+1:tc+2] = np.array([[sol[x]],[sol[y]]])

# 再求出各期状态变量
for i in range(tc-tz):
    xpi_c[:,tc-i:tc+1-i] = A@xpi_c[:,tc+1-i:tc+2-i]+rho*B
for i in range(tz+1):
    xpi_c[:,tz-i:tz+1-i] = A@xpi_c[:,tz+1-i:tz+2-i]-er*B

# 求出各期拉格朗日参数 并根据tc+1期的参数计算之后各时期的状态变量
xis = Matrix(np.zeros((2,T+2))) # 从-1开始
for i in range(tc+2):
    xis[:,i+1:i+2] = H@xis[:,i:i+1]-J@xpi_c[:,i:i+1]
for i in range(T-tc-1):
    xpi_c[0,tc+2+i] = k*delta**(i+1)/Theta*xis[0,tc+2]
    xpi_c[1,tc+2+i] = (1-delta)*delta**i*xis[0,tc+2]
xpi_c = np.asarray(xpi_c)

# 计算名义利率
it_c = r.copy()
for i in range(len(r)-1):
    it_c[i] += xpi_c[1,i+1]+sigma*(xpi_c[0,i+1]-xpi_c[0,i])

# 画图
fig,axes = plt.subplots(2,2,figsize=(9,7))
axes[0,0].plot(xpi[0],'-o',label='discretion')
axes[0,0].plot(xpi_c[0],'-o',label='commitment')
axes[0,0].axhline(0,alpha=0.3,ls='--')
axes[0,0].legend()
axes[0,0].set_title('Output')

axes[0,1].plot(xpi[1],'-o',label='discretion')
axes[0,1].plot(xpi_c[1],'-o',label = 'commitment')
axes[0,1].axhline(0,alpha=0.3,ls='--')
axes[0,1].set_title('Inflation')

axes[1,0].plot(it,'-o',label = 'discretion')
axes[1,0].plot(it_c,'-o',label='commitment')
axes[1,0].set_title('Nominal rate')

axes[1,1].plot(r,'-o')
axes[1,1].set_title('Natural rate')

for ax in axes.flatten():
    ax.set_xticks(np.linspace(0,T,T/2+1))
```

```python
print(gamma,delta,lam,Lambda,k,Theta,psi_ya)
```

    0.03111119620156227 0.031141065074019782 0.4467076923076922 0.005514909781576447 3.3503076923076915 0.37225641025641015 1.0

```python
theta**4
```

    0.31640625

```python

```


## Problem Statement

Use PINNs along with the OpenFOAM data to solve the inverse problem of finding the viscosity of the flow, given the flow field data

## Challenge

The main challenge in this problem is to correctly formulate the problem using PINNs. In order to achieve that, you will have to complete the following parts successfully:
1. Assimilate the OpenFOAM data
2. Set-up the correct equation residuals to miminize
3. Create the neural network and solve the inverse problem

The viscosity in the OpenFOAM simulation was set to $0.02 \text{ }m^2/s$. A successful completion of the problem should result in infering out the same viscosity within 10% error margin. The OpenFOAM data that was used for validation in the previous parts would now be used as a training input.

Let us start by importing the required packages and modules

**Note: You need to edit the `chip_2d_inverse_template.py` script that is placed in the ../source_code/chip_2d/ directory.**

From the top menu, click on File, and Open `chip_2d_inverse_template.py` from the current directory at `../source_code/chip_2d` directory. Remember to SAVE your code after changes, before running below cells.

```python
from sympy import Symbol
import numpy as np
import tensorflow as tf
from simnet.solver import Solver
from simnet.dataset import TrainDomain, ValidationDomain, MonitorDomain
from simnet.data import Validation, Monitor, BC
from simnet.sympy_utils.geometry_2d import Rectangle, Line, Channel2D
from simnet.sympy_utils.functions import parabola
from simnet.csv_utils.csv_rw import csv_to_dict
from simnet.PDES.navier_stokes import IntegralContinuity, NavierStokes
from simnet.controller import SimNetController
from simnet.architecture import FourierNetArch
```

For this problem, since we are interested in only assimilating the OpenFOAM data and getting the inference out of it, you do not need create the geometry. Fill in the `Chip2DTrain` based on the data generated from OpenFOAM. Remember to add appropriate keys to the dataset to solve for the correct equations.

```python
#TODO: Replace all the placeholders with appropriate values

# define sympy variables to parametrize domain curves
x, y = Symbol('x'), Symbol('y')

# OpenFOAM data
mapping = {'Points:0': 'x', 'Points:1': 'y',
           'U:0': 'u', 'U:1': 'v', 'p': 'p'}
openfoam_var = csv_to_dict('openfoam/2D_chip_fluid0.csv', mapping)
openfoam_var['x'] -= 2.5 # normalize pos
openfoam_var['y'] -= 0.5
openfoam_invar_numpy = {key: value for key, value in openfoam_var.items() if key in ['x', 'y']}
openfoam_outvar_numpy = {key: value for key, value in openfoam_var.items() if key in ['u', 'v', 'p']}

#TODO: Add keys and appropriate values for continuity and momentum equations in x and y directions here:
openfoam_outvar_numpy['continuity'] = placeholder
openfoam_outvar_numpy['momentum_x'] = placeholder
openfoam_outvar_numpy['momentum_y'] = placeholder

class Chip2DTrain(TrainDomain):
  def __init__(self, **config):
    super(Chip2DTrain, self).__init__()

    # fill in the appropriate parameters for the from_numpy function
    interior=BC.from_numpy(placeholder, placeholder, batch_size=placeholder)
    self.add(interior, name="Interior")
```

Create a `MonitorDomain` to monitor the average viscosity predicted by the model

```python
class Chip2DMonitor(MonitorDomain):
  def __init__(self, **config):
    super(Chip2DMonitor, self).__init__()

    global_monitor = Monitor(openfoam_invar_numpy, {'average_nu': lambda var: tf.reduce_mean(var['nu'])})
    self.add(global_monitor, 'GlobalMonitor')
```

Now complete the last part of the code by creating the `ChipSolver` to solve our problem. Don't forget to stop the gradients for appropriate variables while setting the equations.

```python
#TODO: Replace all the placeholders with appropriate values
class ChipSolver(Solver):
  train_domain = placeholder
  monitor_domain = placeholder

  def __init__(self, **config):
    super(ChipSolver, self).__init__(**config)

    self.equations = (NavierStokes(nu=placeholder, rho=1, dim=2, time=False).make_node(stop_gradients=[placeholder]))

    flow_net = self.arch.make_node(name='flow_net',
                                   inputs=['x', 'y'],
                                   outputs=['u', 'v', 'p'])
    invert_net = self.arch.make_node(name='invert_net',
                                     inputs=['x', 'y'],
                                     outputs=['nu'])
    self.nets = [flow_net, invert_net]

  @classmethod
  def update_defaults(cls, defaults):
    defaults.update({
        'network_dir': './network_checkpoint_chip_2d_inverse',
        'rec_results': True,
        'rec_results_freq': 100,
        'start_lr': 3e-4,
        'max_steps': 40000,
        'decay_steps': 100,
        'xla': True
        })
if __name__ == '__main__':
  ctr = SimNetController(ChipSolver)
  ctr.run()
```

# Licensing
This material is released by NVIDIA Corporation under the Creative Commons Attribution 4.0 International (CC BY 4.0)


```python
%load_ext autoreload
%autoreload 2
```

```python
import numpy as np
import matplotlib.pyplot as plt
from pyodesys.symbolic import SymbolicSys, get_logexp, symmetricsys, PartiallySolvedSystem
import sympy as sp
sp.init_printing()
%matplotlib inline
```

```python
l3sys = SymbolicSys.from_callback(lambda t, y, p: [-p[0]*y[0], p[0]*y[0] - p[1]*y[1], p[1]*y[1]],
                                   3, 2, linear_invariants=[[1, 1, 1]])
l3sys.exprs, l3sys.linear_invariants, l3sys.nonlinear_invariants or []
```

```python
logexp = get_logexp()
LogLogSys = symmetricsys(logexp, logexp)
def expr_proc(exprs):
    return [sp.powsimp(expr.expand(), force=True).simplify() for expr in exprs]

t3sys = LogLogSys.from_other(l3sys, exprs_process_cb=expr_proc)
t3sys.exprs, t3sys.linear_invariants or [], t3sys.nonlinear_invariants
```

```python
l2sys = PartiallySolvedSystem.from_linear_invariants(l3sys)
l2sys.exprs, l2sys.linear_invariants or [], l2sys.nonlinear_invariants or []
```

```python
t2sys = LogLogSys.from_other(l2sys, exprs_process_cb=expr_proc)
t2sys.exprs, l2sys.linear_invariants or [], l2sys.nonlinear_invariants or []
```

```python
y0, y1, y2 = l3sys.dep
p0, p1 = l3sys.params
x = l3sys.indep
exp = l3sys.be.exp
assert l3sys.exprs[0] + p0*y0 == 0
assert l3sys.exprs[1] - p0*y0 + p1*y1 == 0
assert l3sys.exprs[2] - p1*y1 == 0
assert l3sys.linear_invariants.tolist() == [[1, 1, 1]]

assert t3sys.exprs[0] + p0*exp(x) == 0
assert t3sys.exprs[1] + p1*exp(x) - p0*exp(x+y0-y1) == 0
assert t3sys.exprs[2] - p1*exp(x+y1-y2) == 0
assert t3sys.nonlinear_invariants[0] - exp(y0) - exp(y1) - exp(y2) == 0

i0, i1, i2 = l2sys.init_dep
assert l2sys.exprs[0] - p0*(i0 + i1 + i2 - y1 - y2) + p1*y1 == 0
assert l2sys.exprs[1] - p1*y1 == 0

assert t2sys.exprs[0] - exp(x-y1)*(p0*(i0 + i1 + i2 - exp(y1) - exp(y2)) - p1*exp(y1))
```

```python
l2sys.init_dep
```

```python
params = [2.05, 2.4]
results = [odesys.integrate(
    [1e-8, 3.5], [1, 1e-16, 1e-16], params, integrator='cvode', nsteps=2000,
    atol=1e-9, rtol=1e-9) for odesys in [l3sys, l2sys, t3sys, t2sys]]
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for res, ax in zip(results, axes):
    res.plot(ax=ax)
```

```python
from pyodesys.tests.bateman import bateman_full
for res in results:
    ref = bateman_full(res.yout[0, :], params + [0], res.xout - res.xout[0], exp=np.exp)
    assert np.allclose(res.yout, np.array(ref).T)
```

```python
logexp16 = get_logexp(1, 10**-16)
LogLog16 = symmetricsys(logexp16, logexp16)
t3sys16 = LogLog16.from_other(l3sys, lower_bounds=[0]*l3sys.ny, upper_bounds=[1+1e-8]*l3sys.ny)
assert np.all(t3sys16.lower_bounds < -30) and np.all(t3sys16.upper_bounds > 0)
```

```python

```


# 2. Gyakorlat - 1 DoF csillapítatlan rendszer
2021.02.16

## Feladat:

A mellékelt ábrán egy 1 szabadságfokú lengőrendszer látható, mely áll egy $m_1$ tömegből, valamint $k_1$ és $k_2$ rugómerevségű rugókból. Az egyik rugó egy vertikális befogott tartóhoz csatlakozik, melynek hossza $l$, a kereszmetszetét az $a$ és $b$ paraméterrel lehet jellemezni, továbbá anyagának rugalmassági modulusza $E$. A gerenda tömege elhanyagolható. Az $m_1$ tömeg helyzete az $x$ általánosított koordinátával írható le. A rendszert egy ütközés hozza mozgásba (rezgésbe), mely az $m_1$ tömeg és $m_2$ redukált tömeg között jön létre.

### Adatok:
|||
|-----------------|-----------------|
| $a$ = 0,006 m   | $b$ = 0,025 m   |
| $l$ = 0,5 m     | $E$ = 200 GPa   |
| $m_1$ = 5 kg    | $m_2$ = 1 kg    |
| $k_1$ = 100 N/m | $k_2$ = 50 N/m  |
| $c_1$ = 0 m/s   | $c_2$ = 0,6 m/s |
| e = 0,5         |                 |

### Részfeladatok:
1. Határozza meg a rendszer sajátkörfrekvenciáját!
2. Számítsa ki az $m_1$ test ütközés hatására létrejövő rezgésének maximális elmozdulását, sebességét, és gyorsulását. Ábrázolja diagramon az elmozdulás, a sebesség és a gyorsulás időfüggvényét.

## Megoldás:

### 1. Feladat

Redukáljuk a rendszert az alábbi alakúra:

A befogott tartó végpontjának adott erőhatásra történő $f$ elmozdulása az alábbi módon számítható:
$$f = \frac{Fl^3}{3I_{\mathrm{z}}E},$$

melyben $I_{\mathrm{z}}$ a tartó keresztmetszetének $z$ tengelyre vett másodrendű nyomatéka. A gerenda merevsége, mely a helyettesítő rugó merevségét adja meg:
$$k_r = \frac{F}{f}.$$

```python
import sympy as sp
import numpy as np
from IPython.display import Math # Hogy tudjunk LaTeX szöveget kiírni

sp.init_printing()  # a szebb kiíratásért

a,b,l,E,m1,m2,k1,k2,c1,c2,e = sp.symbols('a, b, l, E, m1, m2, k1, k2, c1, c2, e')
F = sp.symbols('F')
x,xr = sp.symbols('x, x_r')

# Készítsünk behelyettesítési listát az adatok alapján, SI-ben
adatok = ((a, 0.006), (b, 0.025), (l, 0.5), (E, 2*10**11),
          (m1, 5), (m2, 1), (k1, 100), (k2, 50), (c1, 0),
          (c2, 0.6), (e, 0.5))

Iz = a**3*b/12  # sziltan
f = F*l**3/3/Iz/E
kr = F/f
kr
```

```python
display(Math('k_r = {}'.format(sp.latex(kr))))

# Ez a sor elsőre lehet kicsit sok. Gyakorlatilag az történik, hogy a
# `display` függvény az argumentumába egy `Math` objektumot kap, amit
# LaTeX stílusban fog kiíratni. Amennyiben a beadott stringhez sze-
# retnénk hozzáfűzni egy változót is, úgy annak helyét `{}`-kel kell
# jelezni. Ezt követően a `.format()` metódussal "cseréljük ki" ezt a
# tényleges változóban tárolt kifejezésre, esetleg értékre. Mivel
# most egy kifejezést szeretnék kiíratni, nem egy egyszerű értéket,
# így nem csupán a `kr` kerül a `format()` argumentumába, hanem a kifejezés
# LaTeX kódja. Így a `display` függvény ezt is megfelelő módon formázza:
```

$\displaystyle k_r = \frac{E a^{3} b}{4 l^{3}}$

```python
# Nézzünk mi történik, ha a pusztán a `kr` kifejezést íratjuk ki

display(Math('k_r = {}'.format(kr)))
# Eredmény: csúnya, nem LaTeX :(
```

$\displaystyle k_r = E*a**3*b/(4*l**3)$

```python
# Megoldás: nézzük `kr` LaTeX kódját:
sp.latex(kr)
# Ha ezt a kódot íratjuk ki, a display függvény beformázza.
```

    '\\frac{E a^{3} b}{4 l^{3}}'

```python
# Numerikusan az eredmény N/m-ben
kr_num = kr.subs(adatok);

display(Math('k_r = {:.1f}'.format(kr_num)))
# `:.1f` -> floatot íratunk ki, egy tizedes pontossággal
```

$\displaystyle k_r = 2160.0$

Az egyenértékű merevség a potenciális energiák egyenlőségéből meghatározható:
$$ U = \frac{1}{2}k_rx_r^2+\frac{1}{2}k_1(x-x_r)^2+\frac{1}{2}k_2x^2 \leftrightarrow U = \frac{1}{2}k_ex^2,$$

ahol $x_r$ a befogott tartó szabad végének elmozdulása. A soros elrendezés miatt a $k_r$ és $k_1$ rugóállandójú rugókban ébredő erő megegyezik:

$$F_r = F_1 \rightarrow k_rx_r = k_1(x-x_r),$$

ahonnan

$$x_r = \frac{k_1}{k_1+k_r}x.$$

```python
Fr = kr*xr
F1 = k1*(x-xr)

# Megoldjuk az `F1-Fr = 0` egyenletet `xr`-re. Ebben a formában
# már `kr`-nek a kifejtett alakja van behelyettesítve
xr = sp.solve(F1-Fr,xr)[0]

display(Math('x_r = {}'.format(sp.latex(xr))))
```

$\displaystyle x_r = \frac{4 k_{1} l^{3} x}{E a^{3} b + 4 k_{1} l^{3}}$

```python
U = 0.5*kr * xr**2 + 0.5*k1 * (x-xr)**2 + 0.5*k2 * x**2

# A potenciális energia kifejezésében az egyenértékű merevség
# az x^2 együtthatójának duplája.

U = U.expand()
# Ez egy nagyon fontos parancs, csak így tudjuk az együtthatókat keresni.
# Így a kifejezésben minden zárójeles tag kifejtésre kerül

ke = 2*U.coeff(x,2)  # (x,2) -> x^2 együtthatóját keressük

display(Math('k_e = {}'.format(sp.latex(ke))))
```

$\displaystyle k_e = \frac{4.0 E a^{3} b k_{1}^{2} l^{3}}{E^{2} a^{6} b^{2} + 8 E a^{3} b k_{1} l^{3} + 16 k_{1}^{2} l^{6}} + \frac{16.0 k_{1}^{3} l^{6}}{E^{2} a^{6} b^{2} + 8 E a^{3} b k_{1} l^{3} + 16 k_{1}^{2} l^{6}} - \frac{8.0 k_{1}^{2} l^{3}}{E a^{3} b + 4 k_{1} l^{3}} + k_{1} + k_{2}$

```python
# Numerikusan az eredmény, N/m-ben:
ke_num = ke.subs(adatok).evalf(6)

display(Math('k_e = {}'.format(ke_num)))
```

$\displaystyle k_e = 145.575$

A rugórendszert így már helyettesíthetjük a kiszámolt eredő rugómerevséggel. Ebben az esetben a vizsgált rendszer mozgásegyenlete a következő alakot ölti:
$$\ddot{x} + \underbrace{\frac{k_e}{m_1}}_{\omega_n^2}x = 0.$$

```python
ω_n = sp.sqrt(ke/m1)
ω_n_num = ω_n.subs(adatok).evalf(5);

# Numerikusan az eredmény rad/s-ban
display(Math('\omega_n = {}'.format(ω_n_num)))
```

$\displaystyle \omega_n = 5.3958$

### 2. Feladat

A felvázolt differenciálegyenletet kezedeti érték híján nem tudjuk még megoldani. Azt az ütközésből számíthatjuk, mely mindkét test számára centrikus.

A testek sebességeinek normális komponense az ütközés előtt:

```python
c1_n = 0  # SI
c2_n = -0.6  # SI

# A közös súlypont sebessége

cS_n = (m1*c1_n+m2*c2_n)/(m1+m2)
cS_n = cS_n.subs(adatok)
display(Math('c_{{Sn}} = {:.1f}'.format(cS_n)))

# m/s-ban
```

$\displaystyle c_{Sn} = -0.1$

Az ütközés utáni sebességek az alábbi módon számíthatóak:

```python
v1_n = (cS_n + e*(cS_n - c1_n)).subs(adatok)
v2_n = (cS_n + e*(cS_n - c2_n)).subs(adatok)

display(Math('v_{{1n}} = {:.2f}'.format(v1_n)))
display(Math('v_{{2n}} = {:.2f}'.format(v2_n)))

# m/s-ban
```

$\displaystyle v_{1n} = -0.15$

$\displaystyle v_{2n} = 0.15$

```python
t = sp.symbols('t')
x_t = sp.Function('x')(t) # meg kell különbözteteni az eddig használt `x`-től,
                          # mert ez egy függvény.

mozg_egy = x_t.diff(t,2) + ω_n_num**2*x_t
display(Math('{} = 0'.format(sp.latex(mozg_egy))))

# A mozgásegyenlet
```

$\displaystyle 29.115 x{\left(t \right)} + \frac{d^{2}}{d t^{2}} x{\left(t \right)} = 0$

```python
megold_sym = sp.dsolve(mozg_egy,x_t)
megold_sym
```

```python
# A kezdeti értékek felvétele:

kezdeti_ert = {x_t.subs(t,0): 0, x_t.diff(t).subs(t,0): v1_n}
kezdeti_ert
```

Részletekért lásd: [AMSZ Python gyorstalplaló](https://youtu.be/1KdMsje1I-I?t=3448).

```python
x_t_megold = sp.dsolve(mozg_egy,x_t,ics=kezdeti_ert)
x_t_megold
```

```python
# Nézzük a sebességet és a gyorsulást (deriváltak)
v_t = x_t_megold.rhs.diff(t) # az egyenlet jobb oldalát (rhs) deriváljuk
a_t = x_t_megold.rhs.diff(t,2)

display(Math('\dot x(t) = {}'.format(sp.latex(v_t))))
display(Math('\ddot x(t) = {}'.format(sp.latex(a_t))))
```

$\displaystyle \dot x(t) = - 0.15 \cos{\left(5.3958364754254 t \right)}$

$\displaystyle \ddot x(t) = 0.809375471313811 \sin{\left(5.3958364754254 t \right)}$

```python
# Keressük az elmozdulás maximumát!

szelsoertek_helyek = sp.solve(v_t,t)

szelsoertek_ertekek = [x_t_megold.rhs.subs(t,szelsoertek_helyek[i])
                       for i
                       in range(len(szelsoertek_helyek))]  # list comprehension: lsd. Google, vagy
                                                           # AMSZ Python gyorstalpaló 30:31

display(szelsoertek_helyek)
display(szelsoertek_ertekek)
```

```python
x_max = max(szelsoertek_ertekek)
display(Math('x_{{max}} = {:.5f}'.format(x_max)))

# méterben
```

$\displaystyle x_{max} = 0.02780$

```python
# Ez egy példa volt, hogyan lehet ezt automatizálni sympyban. A többit inkább leolvasás
# alapján. Léteznek egyéb numerikus szélsőérték kereső algoritmusok, de
# most így volt a legegyszerűbb.

# Plottoláshoz rengeteg eszköz áll rendelkezésre (matplotlib, seaborn, plotly ...)
# Most használjuk a sympy sajátját (ami elég hiányos, de egyszerű),
# kiegészítve a `seaborn` könyvtár funkcióival. Jelen esetben ezt arra
# használjuk, hogy legyen rácsvonal.

import seaborn as sns
sns.set()
sns.set_style("whitegrid", {'grid.linestyle': '--'})

plot_x = sp.plot(x_t_megold.rhs,
                 (t,0,5),
                 ylabel='x(t) (m)',
                 xlabel='t (s)',
                 adaptive=False,
                 nb_of_points = 300)  # az eredeti plot kicsit szakaszos volt
plot_v = sp.plot(v_t,
                 (t,0,5),
                 xlabel='t (s)',
                 ylabel='v(t) (m/s)')
plot_a = sp.plot(a_t,
                 (t,0,5),
                 xlabel='t (s)',
                 ylabel='a(t) $(m/s^2)$')

# Ez a parancs majd csak az 1.8-as verziójú sympyban fog működni.
# sp.PlotGrid(3, 1 , plot_x, plot_v, plot_a)
```

Készítette:

            Csuzdi Domonkos (Alkalmazott Mechanika Szakosztály)
            Takács Dénes (BME MM) kidolgozása és ábrái alapján.

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

            2021.02.16


# 机器学习练习 2 - 逻辑回归

这个笔记包含了以Python为编程语言的Coursera上机器学习的第二次编程练习。请参考 [作业文件](ex2.pdf) 详细描述和方程。
在这一次练习中，我们将要实现逻辑回归并且应用到一个分类任务。我们还将通过将正则化加入训练算法，来提高算法的鲁棒性，并用更复杂的情形来测试它。

代码修改并注释：黄海广，haiguang2000@qq.com

## 逻辑回归

在训练的初始阶段，我们将要构建一个逻辑回归模型来预测，某个学生是否被大学录取。设想你是大学相关部分的管理者，想通过申请学生两次测试的评分，来决定他们是否被录取。现在你拥有之前申请学生的可以用于训练逻辑回归的训练样本集。对于每一个训练样本，你有他们两次测试的评分和最后是被录取的结果。为了完成这个预测任务，我们准备构建一个可以基于两次测试评分来评估录取可能性的分类模型。

让我们从检查数据开始。

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

```python
path = 'ex2data1.txt'
data = pd.read_csv(path, header=None, names=['Exam 1', 'Exam 2', 'Admitted'])
data.head()
```

      Exam 1
      Exam 2
      Admitted

      0
      34.623660
      78.024693
      0

      1
      30.286711
      43.894998
      0

      2
      35.847409
      72.902198
      0

      3
      60.182599
      86.308552
      1

      4
      79.032736
      75.344376
      1

让我们创建两个分数的散点图，并使用颜色编码来可视化，如果样本是正的（被接纳）或负的（未被接纳）。

```python
positive = data[data['Admitted'].isin([1])]
negative = data[data['Admitted'].isin([0])]

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(positive['Exam 1'], positive['Exam 2'], s=50, c='b', marker='o', label='Admitted')
ax.scatter(negative['Exam 1'], negative['Exam 2'], s=50, c='r', marker='x', label='Not Admitted')
ax.legend()
ax.set_xlabel('Exam 1 Score')
ax.set_ylabel('Exam 2 Score')
plt.show()
```

看起来在两类间，有一个清晰的决策边界。现在我们需要实现逻辑回归，那样就可以训练一个模型来预测结果。方程实现在下面的代码示例在"exercises" 文件夹的 "ex2.pdf" 中。

# sigmoid 函数
g 代表一个常用的逻辑函数（logistic function）为S形函数（Sigmoid function），公式为： \\[g\left( z \right)=\frac{1}{1+{{e}^{-z}}}\\]
合起来，我们得到逻辑回归模型的假设函数：
	\\[{{h}_{\theta }}\left( x \right)=\frac{1}{1+{{e}^{-{{\theta }^{T}}X}}}\\]

```python
def sigmoid(z):
    return 1 / (1 + np.exp(-z))
```

让我们做一个快速的检查，来确保它可以工作。

```python
nums = np.arange(-10, 10, step=1)

fig, ax = plt.subplots(figsize=(12,8))
ax.plot(nums, sigmoid(nums), 'r')
plt.show()
```

棒极了！现在，我们需要编写代价函数来评估结果。
代价函数：
$J\left( \theta  \right)=\frac{1}{m}\sum\limits_{i=1}^{m}{[-{{y}^{(i)}}\log \left( {{h}_{\theta }}\left( {{x}^{(i)}} \right) \right)-\left( 1-{{y}^{(i)}} \right)\log \left( 1-{{h}_{\theta }}\left( {{x}^{(i)}} \right) \right)]}$

```python
def cost(theta, X, y):
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)
    first = np.multiply(-y, np.log(sigmoid(X * theta.T)))
    second = np.multiply((1 - y), np.log(1 - sigmoid(X * theta.T)))
    return np.sum(first - second) / (len(X))
```

现在，我们要做一些设置，和我们在练习1在线性回归的练习很相似。

```python
# add a ones column - this makes the matrix multiplication work out easier
data.insert(0, 'Ones', 1)

# set X (training data) and y (target variable)
cols = data.shape[1]
X = data.iloc[:,0:cols-1]
y = data.iloc[:,cols-1:cols]

# convert to numpy arrays and initalize the parameter array theta
X = np.array(X.values)
y = np.array(y.values)
theta = np.zeros(3)
```

让我们来检查矩阵的维度来确保一切良好。

```python
theta
```

    array([ 0.,  0.,  0.])

```python
X.shape, theta.shape, y.shape
```

    ((100, 3), (3,), (100, 1))

让我们计算初始化参数的代价函数(theta为0)。

```python
cost(theta, X, y)
```

    0.69314718055994529

看起来不错，接下来，我们需要一个函数来计算我们的训练数据、标签和一些参数thata的梯度。

# gradient descent(梯度下降)
* 这是批量梯度下降（batch gradient descent）
* 转化为向量化计算： $\frac{1}{m} X^T( Sigmoid(X\theta) - y )$
$$\frac{\partial J\left( \theta  \right)}{\partial {{\theta }_{j}}}=\frac{1}{m}\sum\limits_{i=1}^{m}{({{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}})x_{_{j}}^{(i)}}$$

```python
def gradient(theta, X, y):
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)

    parameters = int(theta.ravel().shape[1])
    grad = np.zeros(parameters)

    error = sigmoid(X * theta.T) - y

    for i in range(parameters):
        term = np.multiply(error, X[:,i])
        grad[i] = np.sum(term) / len(X)

    return grad
```

注意，我们实际上没有在这个函数中执行梯度下降，我们仅仅在计算一个梯度步长。在练习中，一个称为“fminunc”的Octave函数是用来优化函数来计算成本和梯度参数。由于我们使用Python，我们可以用SciPy的“optimize”命名空间来做同样的事情。

我们看看用我们的数据和初始参数为0的梯度下降法的结果。

```python
gradient(theta, X, y)
```

    array([ -0.1       , -12.00921659, -11.26284221])

现在可以用SciPy's truncated newton（TNC）实现寻找最优参数。

```python
import scipy.optimize as opt
result = opt.fmin_tnc(func=cost, x0=theta, fprime=gradient, args=(X, y))
result
```

    (array([-25.1613186 ,   0.20623159,   0.20147149]), 36, 0)

让我们看看在这个结论下代价函数计算结果是什么个样子~

```python
cost(result[0], X, y)
```

    0.20349770158947464

接下来，我们需要编写一个函数，用我们所学的参数theta来为数据集X输出预测。然后，我们可以使用这个函数来给我们的分类器的训练精度打分。
逻辑回归模型的假设函数：
	\\[{{h}_{\theta }}\left( x \right)=\frac{1}{1+{{e}^{-{{\theta }^{T}}X}}}\\]
当${{h}_{\theta }}$大于等于0.5时，预测 y=1

当${{h}_{\theta }}$小于0.5时，预测 y=0 。

```python
def predict(theta, X):
    probability = sigmoid(X * theta.T)
    return [1 if x >= 0.5 else 0 for x in probability]
```

```python
theta_min = np.matrix(result[0])
predictions = predict(theta_min, X)
correct = [1 if ((a == 1 and b == 1) or (a == 0 and b == 0)) else 0 for (a, b) in zip(predictions, y)]
accuracy = (sum(map(int, correct)) % len(correct))
print ('accuracy = {0}%'.format(accuracy))
```

    accuracy = 89%

我们的逻辑回归分类器预测正确，如果一个学生被录取或没有录取，达到89%的精确度。不坏！记住，这是训练集的准确性。我们没有保持住了设置或使用交叉验证得到的真实逼近，所以这个数字有可能高于其真实值（这个话题将在以后说明）。

## 正则化逻辑回归

在训练的第二部分，我们将要通过加入正则项提升逻辑回归算法。如果你对正则化有点眼生，或者喜欢这一节的方程的背景，请参考在"exercises"文件夹中的"ex2.pdf"。简而言之，正则化是成本函数中的一个术语，它使算法更倾向于“更简单”的模型（在这种情况下，模型将更小的系数）。这个理论助于减少过拟合，提高模型的泛化能力。这样，我们开始吧。

设想你是工厂的生产主管，你有一些芯片在两次测试中的测试结果。对于这两次测试，你想决定是否芯片要被接受或抛弃。为了帮助你做出艰难的决定，你拥有过去芯片的测试数据集，从其中你可以构建一个逻辑回归模型。

和第一部分很像，从数据可视化开始吧！

```python
path =  'ex2data2.txt'
data2 = pd.read_csv(path, header=None, names=['Test 1', 'Test 2', 'Accepted'])
data2.head()
```

      Test 1
      Test 2
      Accepted

      0
      0.051267
      0.69956
      1

      1
      -0.092742
      0.68494
      1

      2
      -0.213710
      0.69225
      1

      3
      -0.375000
      0.50219
      1

      4
      -0.513250
      0.46564
      1

```python
positive = data2[data2['Accepted'].isin([1])]
negative = data2[data2['Accepted'].isin([0])]

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(positive['Test 1'], positive['Test 2'], s=50, c='b', marker='o', label='Accepted')
ax.scatter(negative['Test 1'], negative['Test 2'], s=50, c='r', marker='x', label='Rejected')
ax.legend()
ax.set_xlabel('Test 1 Score')
ax.set_ylabel('Test 2 Score')
plt.show()
```

哇，这个数据看起来可比前一次的复杂得多。特别地，你会注意到其中没有线性决策界限，来良好的分开两类数据。一个方法是用像逻辑回归这样的线性技术来构造从原始特征的多项式中得到的特征。让我们通过创建一组多项式特征入手吧。

```python
degree = 5
x1 = data2['Test 1']
x2 = data2['Test 2']

data2.insert(3, 'Ones', 1)

for i in range(1, degree):
    for j in range(0, i):
        data2['F' + str(i) + str(j)] = np.power(x1, i-j) * np.power(x2, j)

data2.drop('Test 1', axis=1, inplace=True)
data2.drop('Test 2', axis=1, inplace=True)

data2.head()
```

      Accepted
      Ones
      F10
      F20
      F21
      F30
      F31
      F32
      F40
      F41
      F42
      F43

      0
      1
      1
      0.051267
      0.002628
      0.035864
      0.000135
      0.001839
      0.025089
      0.000007
      0.000094
      0.001286
      0.017551

      1
      1
      1
      -0.092742
      0.008601
      -0.063523
      -0.000798
      0.005891
      -0.043509
      0.000074
      -0.000546
      0.004035
      -0.029801

      2
      1
      1
      -0.213710
      0.045672
      -0.147941
      -0.009761
      0.031616
      -0.102412
      0.002086
      -0.006757
      0.021886
      -0.070895

      3
      1
      1
      -0.375000
      0.140625
      -0.188321
      -0.052734
      0.070620
      -0.094573
      0.019775
      -0.026483
      0.035465
      -0.047494

      4
      1
      1
      -0.513250
      0.263426
      -0.238990
      -0.135203
      0.122661
      -0.111283
      0.069393
      -0.062956
      0.057116
      -0.051818

现在，我们需要修改第1部分的成本和梯度函数，包括正则化项。首先是成本函数：

# regularized cost（正则化代价函数）
$$J\left( \theta  \right)=\frac{1}{m}\sum\limits_{i=1}^{m}{[-{{y}^{(i)}}\log \left( {{h}_{\theta }}\left( {{x}^{(i)}} \right) \right)-\left( 1-{{y}^{(i)}} \right)\log \left( 1-{{h}_{\theta }}\left( {{x}^{(i)}} \right) \right)]}+\frac{\lambda }{2m}\sum\limits_{j=1}^{n}{\theta _{j}^{2}}$$

```python
def cost(theta, X, y, learningRate):
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)
    first = np.multiply(-y, np.log(sigmoid(X * theta.T)))
    second = np.multiply((1 - y), np.log(1 - sigmoid(X * theta.T)))
    reg = (learningRate / (2 * len(X))) * np.sum(np.power(theta[:,1:theta.shape[1]], 2))
    return np.sum(first - second) / len(X) + reg
```

请注意等式中的"reg" 项。还注意到另外的一个“学习率”参数。这是一种超参数，用来控制正则化项。现在我们需要添加正则化梯度函数：

如果我们要使用梯度下降法令这个代价函数最小化，因为我们未对${{\theta }_{0}}$ 进行正则化，所以梯度下降算法将分两种情形：
\begin{align}
  & Repeat\text{ }until\text{ }convergence\text{ }\!\!\{\!\!\text{ } \\
 & \text{     }{{\theta }_{0}}:={{\theta }_{0}}-a\frac{1}{m}\sum\limits_{i=1}^{m}{[{{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}}]x_{_{0}}^{(i)}} \\
 & \text{     }{{\theta }_{j}}:={{\theta }_{j}}-a\frac{1}{m}\sum\limits_{i=1}^{m}{[{{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}}]x_{j}^{(i)}}+\frac{\lambda }{m}{{\theta }_{j}} \\
 & \text{          }\!\!\}\!\!\text{ } \\
 & Repeat \\
\end{align}

对上面的算法中 j=1,2,...,n 时的更新式子进行调整可得：
${{\theta }_{j}}:={{\theta }_{j}}(1-a\frac{\lambda }{m})-a\frac{1}{m}\sum\limits_{i=1}^{m}{({{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}})x_{j}^{(i)}}$

```python
def gradientReg(theta, X, y, learningRate):
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)

    parameters = int(theta.ravel().shape[1])
    grad = np.zeros(parameters)

    error = sigmoid(X * theta.T) - y

    for i in range(parameters):
        term = np.multiply(error, X[:,i])

        if (i == 0):
            grad[i] = np.sum(term) / len(X)
        else:
            grad[i] = (np.sum(term) / len(X)) + ((learningRate / len(X)) * theta[:,i])

    return grad
```

就像在第一部分中做的一样，初始化变量。

```python
# set X and y (remember from above that we moved the label to column 0)
cols = data2.shape[1]
X2 = data2.iloc[:,1:cols]
y2 = data2.iloc[:,0:1]

# convert to numpy arrays and initalize the parameter array theta
X2 = np.array(X2.values)
y2 = np.array(y2.values)
theta2 = np.zeros(11)
```

让我们初始学习率到一个合理值。，果有必要的话（即如果惩罚太强或不够强）,我们可以之后再折腾这个。

```python
learningRate = 1
```

现在，让我们尝试调用新的默认为0的theta的正则化函数，以确保计算工作正常。

```python
costReg(theta2, X2, y2, learningRate)
```

    0.6931471805599454

```python
gradientReg(theta2, X2, y2, learningRate)
```

    array([ 0.00847458,  0.01878809,  0.05034464,  0.01150133,  0.01835599,
            0.00732393,  0.00819244,  0.03934862,  0.00223924,  0.01286005,
            0.00309594])

现在我们可以使用和第一部分相同的优化函数来计算优化后的结果。

```python
result2 = opt.fmin_tnc(func=costReg, x0=theta2, fprime=gradientReg, args=(X2, y2, learningRate))
result2
```

    (array([  1.22702519e-04,   7.19894617e-05,  -3.74156201e-04,
             -1.44256427e-04,   2.93165088e-05,  -5.64160786e-05,
             -1.02826485e-04,  -2.83150432e-04,   6.47297947e-07,
             -1.99697568e-04,  -1.68479583e-05]), 96, 1)

最后，我们可以使用第1部分中的预测函数来查看我们的方案在训练数据上的准确度。

```python
theta_min = np.matrix(result2[0])
predictions = predict(theta_min, X2)
correct = [1 if ((a == 1 and b == 1) or (a == 0 and b == 0)) else 0 for (a, b) in zip(predictions, y2)]
accuracy = (sum(map(int, correct)) % len(correct))
print ('accuracy = {0}%'.format(accuracy))
```

    accuracy = 77%

虽然我们实现了这些算法，值得注意的是，我们还可以使用高级Python库像scikit-learn来解决这个问题。

```python
from sklearn import linear_model#调用sklearn的线性回归包
model = linear_model.LogisticRegression(penalty='l2', C=1.0)
model.fit(X2, y2.ravel())
```

    LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
              intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
              penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
              verbose=0, warm_start=False)

```python
model.score(X2, y2)
```

    0.66101694915254239

这个准确度和我们刚刚实现的差了好多，不过请记住这个结果可以使用默认参数下计算的结果。我们可能需要做一些参数的调整来获得和我们之前结果相同的精确度。

这就是练习2的全部！ 敬请期待下一个练习：多类图像分类。

```python

```


# Quadratic Hamiltonians and Slater determinants

A quadratic Hamiltonian is a Hamiltonian of the form
$$
    H =
    \sum_{j, k} M_{jk} a^\dagger_j a_k
    + \frac12 \sum_{j, k}
        (\Delta_{jk} a^\dagger_j a^\dagger_k + \Delta^*_{jk} a_k a_j)
    + \text{constant}
$$
where $M$ is a Hermitian matrix ($M^\dagger = M$) and $\Delta$ is an antisymmetric matrix ($\Delta^T = -\Delta$), and the $\{a_j^\dagger\}$ are fermionic creation operators which satisfy the anticommutation relations
$$
    \begin{align}
        a_j a_k + a_k a_j &= 0 \\
        a_j a^\dagger_k + a^\dagger_k a_j &= \delta_{pq}
    \end{align}.
$$

Quadratic Hamiltonians are an important class of Hamiltonians that are classically tractable. Their eigenstates are called fermionic Gaussian states, and they can be efficiently prepared on a quantum computer. Qiskit Nature includes the `QuadraticHamiltonian` class for representing quadratic Hamiltonians.

Of course, the `FermionicOp` class can also be used to represent any quadratic Hamiltonian. The reason to have a class specifically for quadratic Hamiltonians is that they support special numerical routines that involve performing linear algebra on the matrices $M$ and $\Delta$. The internal representation format of `FermionicOp` is not suitable for these routines.

A quadratic Hamiltonian is initialized like so:

```python
import numpy as np
from qiskit_nature.operators.second_quantization import QuadraticHamiltonian

# create Hamiltonian
hermitian_part = np.array(
    [
        [1.0, 2.0, 0.0, 0.0],
        [2.0, 1.0, 2.0, 0.0],
        [0.0, 2.0, 1.0, 2.0],
        [0.0, 0.0, 2.0, 1.0],
    ]
)
antisymmetric_part = np.array(
    [
        [0.0, 3.0, 0.0, 0.0],
        [-3.0, 0.0, 3.0, 0.0],
        [0.0, -3.0, 0.0, 3.0],
        [0.0, 0.0, -3.0, 0.0],
    ]
)
constant = 4.0

hamiltonian = QuadraticHamiltonian(
    hermitian_part=hermitian_part,
    antisymmetric_part=antisymmetric_part,
    constant=constant,
)

# convert it to a FermionicOp and print it
hamiltonian_ferm = hamiltonian.to_fermionic_op()
hamiltonian_ferm.set_truncation(0)
print(hamiltonian_ferm)
```

    Fermionic Operator
    register length=4, number terms=29
      4.0
    + 1.0 * ( +_0 -_0 )
    + 2.0 * ( +_0 -_1 )
    + 2.0 * ( +_1 -_0 )
    + 3.0 * ( +_0 +_1 )
    + 3.0 * ( -_1 -_0 )
    + 0.0 * ( +_0 -_2 )
    + 0.0 * ( +_2 -_0 )
    + 0.0 * ( +_0 +_2 )
    + 0.0 * ( -_2 -_0 )
    + 0.0 * ( +_0 -_3 )
    + 0.0 * ( +_3 -_0 )
    + 0.0 * ( +_0 +_3 )
    + 0.0 * ( -_3 -_0 )
    + 1.0 * ( +_1 -_1 )
    + 2.0 * ( +_1 -_2 )
    + 2.0 * ( +_2 -_1 )
    + 3.0 * ( +_1 +_2 )
    + 3.0 * ( -_2 -_1 )
    + 0.0 * ( +_1 -_3 )
    + 0.0 * ( +_3 -_1 )
    + 0.0 * ( +_1 +_3 )
    + 0.0 * ( -_3 -_1 )
    + 1.0 * ( +_2 -_2 )
    + 2.0 * ( +_2 -_3 )
    + 2.0 * ( +_3 -_2 )
    + 3.0 * ( +_2 +_3 )
    + 3.0 * ( -_3 -_2 )
    + 1.0 * ( +_3 -_3 )

## Diagonalization and state preparation
A quadratic Hamiltonian can always be rewritten in the form
$$
    H = \sum_{j} \varepsilon_j b^\dagger_j b_j + \text{constant}
$$
where $\varepsilon_0 \leq \varepsilon_1 \leq \cdots \leq \varepsilon_N$ are non-negative real numbers called orbitals energies and
the $\{b_j^\dagger\}$ are a new set of fermionic creation operators
that also satisfy the canonical anticommutation relations.
These new creation operators are linear combinations of the original creation and annihilation operators:
$$
    \begin{pmatrix}
    b^\dagger_1 \\
    \vdots \\
    b^\dagger_N \\
    \end{pmatrix}
    = W
    \begin{pmatrix}
    a^\dagger_1 \\
    \vdots \\
    a^\dagger_N \\
    a_1 \\
    \vdots \\
    a_N
    \end{pmatrix},
$$
where $W$ is an $N \times 2N$ matrix. Given a basis of eigenvectors of the Hamiltonian, each eigenvector is labeled by a subset of $\{0, \ldots, N - 1\}$, which we call the occupied orbitals. The corresponding eigenvalue is the sum of the corresponding values of $\varepsilon_j$, plus the constant.

```python
# get the transformation matrix W and orbital energies {epsilon_j}
(
    transformation_matrix,
    orbital_energies,
    transformed_constant,
) = hamiltonian.diagonalizing_bogoliubov_transform()

print(f"Shape of matrix W: {transformation_matrix.shape}")
print(f"Orbital energies: {orbital_energies}")
print(f"Transformed constant: {transformed_constant}")
```

    Shape of matrix W: (4, 8)
    Orbital energies: [0.29826763 4.38883678 5.5513683  5.64193745]
    Transformed constant: -1.9402050758492795

The transformation matrix $W$ is used to construct a circuit to prepare an eigenvector of the Hamiltonian. The circuit is constructed using the `FermionicGaussianState` class. Currently, only the [Jordan-Wigner Transform](https://qiskit.org/documentation/nature/stubs/qiskit_nature.mappers.second_quantization.JordanWignerMapper.html)
is supported. The circuit for the Jordan-Wigner Transform has linear depth and uses only linear qubit connectivity. The algorithm is from [Phys. Rev. Applied 9, 044036](https://journals.aps.org/prapplied/abstract/10.1103/PhysRevApplied.9.044036).

```python
from qiskit_nature.circuit.library import FermionicGaussianState

occupied_orbitals = (0, 2)
eig = np.sum(orbital_energies[list(occupied_orbitals)]) + transformed_constant
print(f"Eigenvalue: {eig}")

circuit = FermionicGaussianState(transformation_matrix, occupied_orbitals=occupied_orbitals)
circuit.draw("mpl")
```

The following code cell simulates the circuit and verifies that the output state is indeed an eigenstate of the Hamiltonian with the expected eigenvalue.

```python
from qiskit.quantum_info import Statevector
from qiskit_nature.mappers.second_quantization import JordanWignerMapper

# simulate the circuit to get the final state
state = np.array(Statevector(circuit))

# convert the Hamiltonian to a matrix
hamiltonian_jw = JordanWignerMapper().map(hamiltonian_ferm).to_matrix()

# check that the state is an eigenvector with the expected eigenvalue
np.testing.assert_allclose(hamiltonian_jw @ state, eig * state, atol=1e-8)
```

## Slater determinants

When the antisymmetric part $\Delta = 0$, then the Hamiltonian conserves the number of particles. In this case, the basis change only needs to mix creation operators, not annihilation operators:
$$
    \begin{pmatrix}
    b^\dagger_1 \\
    \vdots \\
    b^\dagger_N \\
    \end{pmatrix}
    = W
    \begin{pmatrix}
    a^\dagger_1 \\
    \vdots \\
    a^\dagger_N \\
    \end{pmatrix},
$$
where now $W$ is an $N \times N$ matrix. Furthermore, the orbital energies $\{\varepsilon_j\}$ are allowed to be negative.

```python
# create Hamiltonian
hermitian_part = np.array(
    [
        [1.0, 2.0, 0.0, 0.0],
        [2.0, 1.0, 2.0, 0.0],
        [0.0, 2.0, 1.0, 2.0],
        [0.0, 0.0, 2.0, 1.0],
    ]
)
constant = 4.0

hamiltonian = QuadraticHamiltonian(
    hermitian_part=hermitian_part,
    constant=constant,
)

print(f"Hamiltonian conserves particle number: {hamiltonian.conserves_particle_number()}")
```

    Hamiltonian conserves particle number: True

```python
# get the transformation matrix W and orbital energies {epsilon_j}
(
    transformation_matrix,
    orbital_energies,
    transformed_constant,
) = hamiltonian.diagonalizing_bogoliubov_transform()

print(f"Shape of matrix W: {transformation_matrix.shape}")
print(f"Orbital energies: {orbital_energies}")
print(f"Transformed constant: {transformed_constant}")
```

    Shape of matrix W: (4, 4)
    Orbital energies: [-2.23606798 -0.23606798  2.23606798  4.23606798]
    Transformed constant: 4.0

In this special case, the eigenstates are called Slater determinants, and a more efficient algorithm is used to prepare them. This algorithm is accessed by using the `SlaterDeterminant` class instead of `FermionicGaussianState`. `SlaterDeterminant` does not take the occupied orbitals as input. Instead, the shape of the transformation matrix is allowed to vary. It should be an $\eta \times N$ matrix where $\eta$ is the number of particles.

```python
from qiskit_nature.circuit.library import SlaterDeterminant

occupied_orbitals = (0, 2)
eig = np.sum(orbital_energies[list(occupied_orbitals)]) + transformed_constant
print(f"Eigenvalue: {eig}")

circuit = SlaterDeterminant(transformation_matrix[list(occupied_orbitals)])
circuit.draw("mpl")
```

## Time evolution

Time evolution under a quadratic Hamiltonian can be easily performed by changing into the diagonal basis of the Hamiltonian. The state preparation circuits shown above effect this basis change, but they are optimized for state preparation from a computational basis state (assumed to be the all zeros state), and they do not work on arbitrary states. The general unitary basis change which does work on arbitrary states is called the Bogoliubov transform, and it is also implemented in Qiskit Nature.

The code block below demonstrates the use of the Bogoliubov transform to implement time evolution for a quadratic Hamiltonian.

```python
from qiskit_nature.circuit.library import BogoliubovTransform
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.quantum_info import random_hermitian, random_statevector, state_fidelity
from scipy.linalg import expm

# create Hamiltonian
n_modes = 5
hermitian_part = np.array(random_hermitian(n_modes))
hamiltonian = QuadraticHamiltonian(hermitian_part=hermitian_part)

# diagonalize Hamiltonian
(
    transformation_matrix,
    orbital_energies,
    _,
) = hamiltonian.diagonalizing_bogoliubov_transform()

# set simulation time and construct time evolution circuit
time = 1.0
register = QuantumRegister(n_modes)
circuit = QuantumCircuit(register)
bog_circuit = BogoliubovTransform(transformation_matrix)
# change to the diagonal basis of the Hamiltonian
circuit.append(bog_circuit.inverse(), register)
# perform time evolution by applying z rotations
for q, energy in zip(register, orbital_energies):
    circuit.rz(-energy * time, q)
# change back to the original basis
circuit.append(bog_circuit, register)

# simulate the circuit
initial_state = random_statevector(2**n_modes)
final_state = initial_state.evolve(circuit)

# compute the correct state by direct exponentiation
hamiltonian_jw = JordanWignerMapper().map(hamiltonian.to_fermionic_op()).to_matrix()
exact_evolution_op = expm(-1j * time * hamiltonian_jw)
expected_state = exact_evolution_op @ np.array(initial_state)

# check that the simulated state is correct
fidelity = state_fidelity(final_state, expected_state)
np.testing.assert_allclose(fidelity, 1.0, atol=1e-8)
```

```python
import qiskit.tools.jupyter

%qiskit_version_table
%qiskit_copyright
```

Version Information

Qiskit Software	Version

qiskit-terra	0.21.0.dev0+cbc801e

qiskit-aer	0.11.0

qiskit-nature	0.4.0

System information

Python version	3.9.6

Python compiler	GCC 8.5.0 20210514 (Red Hat 8.5.0-3)

Python build	default, Aug 11 2021 06:39:25

OS	Linux

CPUs	4

Memory (Gb)	15.314849853515625

Thu Jun 09 11:38:02 2022 EDT

This code is a part of Qiskit

© Copyright IBM 2017, 2022.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
 of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.