```python
import numpy as np
import torch
from torch import nn
from sklearn import datasets
import matplotlib.pyplot as plt

w0 = 0.125
b0 = 5.
x_range = [-20, 60]

def load_dataset(n=150, n_tst=150):
    np.random.seed(43)

    def s(x):
        g = (x - x_range[0]) / (x_range[1] - x_range[0])
        return 3 * (0.25 + g**2.)

    x = (x_range[1] - x_range[0]) * np.random.rand(n) + x_range[0]
    eps = np.random.randn(n) * s(x)
    y = (w0 * x * (1. + np.sin(x)) + b0) + eps
    y = (y - y.mean()) / y.std()
    idx = np.argsort(x)
    x = x[idx]
    y = y[idx]
    return y[:, None], x[:, None]

y, x = load_dataset()
```

```python
plt.plot(x,y,'.')
plt.show()
```

```python
len(x)
```

    150

```python
X = torch.tensor(x, dtype=torch.float)
Y = torch.tensor(y, dtype=torch.float)
```

- First we’ll model a neural network $g_{\theta}(x)$ with maximum likelihood estimation. Where we assume a Gaussian likelihood.
$$\begin{equation}
y \sim \mathcal{N}(g_{\theta}(x), \sigma^2)
\end{equation}$$
$$ \begin{equation}\hat{\theta}_{\text{MLE}} = \text{argmax}_\theta \prod_i^nP(y_i|\theta) \end{equation}$$

```python
class MaximumLikelihood(nn.Module):
    def __init__(self):
        super().__init__()
        self.out = nn.Sequential(
            nn.Linear(1, 20),
            nn.ReLU(),
            nn.Linear(20, 1)
        )
    def forward(self, x):
        return self.out(x)
```

```python
epochs = 200
m = MaximumLikelihood()
optim = torch.optim.Adam(m.parameters(), lr=0.01)
```

```python
for epoch in range(epochs):
    optim.zero_grad()
    y_pred = m(X)
    loss = (0.5 * (y_pred - Y)**2).mean()
    loss.backward()
    optim.step()
```

```python
m.eval()
y_estimate = m(X)
```

```python
plt.figure(figsize=(10, 5))
plt.plot(x,y, 'b.', alpha=0.8)
plt.plot(x, y_estimate.detach().numpy(), 'r', alpha=0.6)
plt.show()
```

- We are able to predict the expectation of $y$, but we are not able to make a statement about the uncertainty of our predictions.
- In variational inference, we accept that we cannot obtain the true posterior $P(y|x)$, but we try to approximate this distribution with another distribution $Q_{\theta}(y)$, where $\theta$ are the variational parameters. This distribution we call a variational distribution.
- If we choose a factorized (diagonal) Gaussian variational distribution, we define a function $g_{\theta}: x \mapsto \mu, \sigma$. The function $g_{\theta}$ will be a neural network that predicts the variational parameters.
- The total model can thus be described as:
$$ \begin{equation}P(y) = \mathcal{N}(0, 1) \end{equation}$$
where we set a unit Gaussian prior $P(y)$.
- from now on we will generalize to a notation that is often used. We’ll extend $y|x$ to any (latent) stochastic variable $Z$.

--------------------------------
- Variational inference is done by maximizing the ELBO:
$$ \begin{equation}\text{argmax}_{Z} = E_{Z \sim Q}[\underbrace{\log P(D|Z)}_{\text{likelihood}}] - D_{KL}(Q(Z)||\underbrace{P(Z)}_{\text{prior}}) \label{eq:elbo} \end{equation}$$
- Let's rewrite this ELBO definition so that it is more clear how we can use it to optimize the model.
$$E_{Z \sim Q}[\log P(D|Z)] + E_{Z \sim Q}[ \frac{P(Z)}{Q(Z)}]dZ$$
$$E_{Z \sim Q}[\log P(D|Z)] + E_{Z \sim Q}[\log P(Z) - \log Q(Z)]$$

#### Monte Carlo ELBO and reparameterization trick
- Deriving those expectations maybe not possible, thus we can get estimates of the true expectation by taking samples from $Q(Z)$ and average over those results.
- If we start taking samples from a $Q(Z)$ we leave the deterministic world, and the gradient can not flow through the model anymore. We avoid this problem by reparameterizing the samples from the distribution.
- Instead of sampling directly from the variational distribution, $z \sim Q(\mu, \sigma^2)$, we sample from a unit gaussian and recreate samples from the variational distribution. Now the stochasticity of $\epsilon$ is external and will not prevent the flow of gradients.

```python
class VI(nn.Module):
    def __init__(self):
        super().__init__()

        self.q_mu = nn.Sequential(
            nn.Linear(1, 20),
            nn.ReLU(),
            nn.Linear(20, 10),
            nn.ReLU(),
            nn.Linear(10, 1)
        )
        self.q_log_var = nn.Sequential(
            nn.Linear(1, 20),
            nn.ReLU(),
            nn.Linear(20, 10),
            nn.ReLU(),
            nn.Linear(10, 1)
        )

    def reparameterize(self, mu, log_var):
        sigma = torch.exp(0.5 * log_var) + 1e-5
        epsilon = torch.randn_like(sigma)
        return mu + sigma * epsilon

    def forward(self, x):
        mu = self.q_mu(x)
        log_var = self.q_log_var(x)
        return self.reparameterize(mu, log_var), mu, log_var
```

$$ \begin{aligned}  \log p(\mathbf{y}|\mathbf{X, \mu, \sigma}) &= \sum_{i=1}^N \log N(y_i;\mathbf{\mu,\sigma^2}) \\  &= \sum_{i=1}^N \log \frac{1}{\sqrt{2\pi\sigma^2_e}}\exp (-\frac{(y_i - \mathbf{\mu})^2}{2\sigma^2_e}) \\  &= -\frac{N}{2}\log 2\pi\sigma^2_e - \sum_{i=1}^N \frac{(y_i-\mathbf{\mu)^2}}{2\sigma^2_e}  \end{aligned}$$

```python
def ll_gaussian(y, mu, log_var):   #log-likelihood of gaussian
    sigma = torch.exp(0.5 * log_var)
    return -0.5 * torch.log(2 * np.pi * sigma**2) - (1 / (2 * sigma**2))* (y-mu)**2
```

```python
def elbo(y_pred, y, mu, log_var):
    # likelihood of observing y given Variational mu and sigma
    likelihood = ll_gaussian(y, mu, log_var)

    # prior probability of y_pred
    log_prior = ll_gaussian(y_pred, 0, torch.log(torch.tensor(1.)))

    # variational probability of y_pred
    log_p_q = ll_gaussian(y_pred, mu, log_var)

    # by taking the mean we approximate the expectation
    return (likelihood + log_prior - log_p_q).mean()
```

```python
def det_loss(y_pred, y, mu, log_var):
    return -elbo(y_pred, y, mu, log_var)
```

```python
epochs = 1500

m = VI()
optim = torch.optim.Adam(m.parameters(), lr=0.005)

for epoch in range(epochs):
    optim.zero_grad()
    y_pred, mu, log_var = m(X)
    loss = det_loss(y_pred, Y, mu, log_var)
    loss.backward()
    optim.step()
```

```python
# draw samples from Q(theta)
with torch.no_grad():
    y_pred = torch.cat([m(X)[0] for _ in range(1000)], dim=1)

# Get some quantiles
q1, mu, q2 = np.quantile(y_pred, [0.05, 0.5, 0.95], axis=1)

plt.figure(figsize=(10, 5))
plt.scatter(X, Y, s=10)
plt.plot(X, mu, 'r', alpha=0.6)
plt.fill_between(X.flatten(), q1, q2, alpha=0.2)
```

#### Analytical KL-divergence
- Above we have implemented ELBO by sampling from the variational posterior. It turns out that for the KL-divergence term, this isn’t necessary as there is an analytical solution:
$$D_{KL}(Q(Z)||P(Z)) = \frac{1}{2}\sum_{i=1}^n(1+\log \sigma_i^2 - \mu_i^2 - \sigma_i^2)$$
- For the likelihood term, we did implement Guassian log likelihood, this term can also be replaced with a similar loss functions. For Gaussian likelihood we can use squared mean error loss.
- We can simplify the loss function as defined below:
```
def det_loss(y, y_pred, mu, log_var):
    reconstruction_error = (0.5 * (y - y_pred)**2).sum()
    kl_divergence = (-0.5 * torch.sum(1 + log_var - mu**2 - log_var.exp()))

    return (reconstruction_error + kl_divergence).sum()
```

#### Aleatoric and epistemic uncertainty
- In the example above we were able to model the *aleatoric uncertainty*.
- This is the *inherent variance* in the data which we have to accept because the underlaying data generation process is stochastic in nature. (e.g. throwing the dice, cards you get in a poker game)
- Aleatory can have two flavors, being *homoscedastic* and *heteroscedastic*.
    - homoscedastic: For example in the model definition of linear regression $y = X \beta + \epsilon$ we incorporate $\epsilon$ for the noise in the data. In linear regression, $\epsilon$ is not dependent on $X$ and is therefore assumed to be constant.
    - heteroscedastic: If the aleatoric uncertainty is dependent on $X$, we speak of heteroscedastic uncertainty.

- Epistemic uncertainty can be reduced by designing new model, acquiring more data, etc.
- In the above example data has inherent noise which can't be reduced but when we generate model ensemble using dropout and measure only the *model inconsistency*, that epistemic uncertainty is low as shown in below. (because most of the uncertainty comes from aleatoric uncertainty in this case)

```python

```

deep_learning/uncertainty-deep-learning-master/06. Aleatoric-epistemic uncertainty (toy example).ipynb


# Likevektskonsentrasjoner (eksempel 15.6)
Her skal vi gjøre eksempel 15.6 (side 527) fra læreboken. Her har vi gitt likevekten,

$$\text{H}_2(\text{g}) + \text{I}_2 (\text{g}) \rightleftharpoons   2\text{HI} (\text{g}), \quad K_{\text{c}} = 54.3,$$

og vi har gitt antall mol før likevekten innstilles:
- H$_2$: 0.750 mol
- I$_2$: 0.750 mol.

Videre har vi gitt at volumet til beholderen der reaksjonen skjer er 1.00 L og at temperaturen er 430 °C.
Vi blir så bedt om å finne $[\text{H}_2]$, $[\text{I}_2]$ og $[\text{HI}]$ ved likevekt.

Vi skal løse denne oppgaven ved å bruke Python. For å kunne regne symbolsk skal vi bruke et bibliotek som heter [SymPy](https://www.sympy.org/).

(Merk: Her er volumet 1.00 L. For å spare litt skriving deler vi ikke eksplisitt på 1.00 L for å gjøre om til konsentrasjoner.)

```python
import sympy as sym  # Importer SymPy
```

```python
# Definer størrelsene vi kjenner
START_H2 = 0.750
START_I2 = 0.750
KC = 54.3
```

Over har vi listet opp hva vi kjenner. La oss også liste opp alle de ukjente som vi skal bestemme:
- $[\text{H}_2]$ ved likevekt,
- $[\text{I}_2]$ ved likevekt,
- $[\text{HI}]$ ved likevekt.

Vi har altså tre ukjente. La oss definere de som størrelser (spesifikt som [SymPy-symboler](https://docs.sympy.org/latest/tutorial/intro.html#a-more-interesting-example)) slik at vi kan regne med de (dette blir litt som når vi introduserer $x$ osv. for ukjente størrelser i ligninger vi skriver for hånd):

```python
# Vi definerer de ukjente størrelsene:
c_H2, c_I2, c_HI = sym.symbols('c_H2 c_I2 c_HI')
```

Vi har nå definert konsentrasjonene. Disse er foreløpig ukjente. For å bestemme de, så trenger vi noen ligninger som relaterer de til hverandre. Mulige slike ligninger er:
- likevektskonstanten
- massebalanser

```python
# La oss begynne med likevektskonstanten:
ligning1 = sym.Eq((c_HI * c_HI)/(c_H2 * c_I2), KC)
```

Vi kan be SymPy skrive ut hva denne ligningen er, for å sjekke at den ser ut som vi hadde tenkt:

```python
ligning1
```

Vi lager så molbalanser for de to grunnstoffene. Her bruker vi at:
- Ved starten er det 0.750 mol H$_2$: Det er altså $2 \times 0.750 = 1.5$ mol H i systemet. Vi "mister" ikke noen H-atomer ved reaksjonen, slik at det fortsatt må være 1.5 mol H når likevekten er innstilt.
- Ved starten er det 0.750 mol I$_2$: Det er altså $2 \times 0.750 = 1.5$ mol I i systemet. Vi "mister" ikke noen I-atomer ved reaksjonen, slik at det fortsatt må være 1.5 mol I når likevekten er innstilt.

Vi kan derfor her lage to molbalanser:
- En for antall mol hydrogen:
  $$2[\text{H}_2]_{\text{start}} = 2[\text{H}_2]_{\text{likevekt}} + [\text{HI}]_{\text{likevekt}}.$$

- En for antall mol jod:
  $$2[\text{I}_2]_{\text{start}} = 2[\text{I}_2]_{\text{likevekt}} + [\text{HI}]_{\text{likevekt}}.$$

La oss formulere dette som ligninger:

```python
ligning2 = sym.Eq(2 * START_H2, 2 * c_H2 + c_HI)
ligning3 = sym.Eq(2 * START_I2, 2 * c_I2 + c_HI)
```

```python
# Skriv ut for dobbeltsjekk:
ligning2
```

```python
ligning3
```

Vi har nå tre ligninger og vi har tre ukjente. Dette kan vi (eller i dette tilfellet, SymPy) løse:

```python
løsninger = sym.solve([ligning1, ligning2, ligning3], [c_H2, c_I2, c_HI], dict=True)
```

```python
løsninger
```

En av løsningene SymPy fant gir negative konsentrasjoner. Dette er en ugyldig løsning og vi beholder bare den som har kun positive løsninger:

```python
# Vis gyldige løsninger:
gyldige = []
for løsning in løsninger:
    if all(i > 0 for i in løsning.values()):
        gyldige.append(løsning)
        print('Gyldig løsning:')
        print(f'- [H2] = {løsning.get(c_H2):.3g} M')
        print(f'- [I2] = {løsning.get(c_I2):.3g} M')
        print(f'- [HI] = {løsning.get(c_HI):.3g} M')
```

Eksempelet i læreboken finner til sammenligning:
- $[\text{H}_2] = 0.160$ M,
- $[\text{I}_2] = 0.160$ M,
- $[\text{HI}] = 1.18$ M.


# Markov Decision Process (MDP)
Reinforcement learning problems are usually representend with the MDP framework, meaning those:
- Set of State
- Set of actions
- Set of rewards
- Discount Factor ( Gamma )
- State-transition probabilites ( because not all the environements are deterministic )

One way of solving it is by using
## Dynamic Programming :
2 Iteratives functions way :
1. Policy Evaluation :
Bellman's Equation allow us to define the value function ( for a state ) recursively :
\begin{align}
V_\pi (s) = \sum_a \pi (a|s) \sum_s' \sum_r p(s', r|s, a){r+ \gamma V_\pi (s') }
\end{align}
and because it is a linear equation, we can develop it and obtain a **policy evaluation**
2. Policy Improvement ( take the argmax over Q(s, a))

### Value Iteration
- Instead of waiting for the policy evaluation to converge, just do 1 iteration
- It will still converge
- We don't need to do Policy Improvement step at all
- Since it's argmax, then taking the max in the policy evaluation step is equivalent
\begin{align}
    V_k+1 (s) = max_a \sum_s' \sum_r p(s', r|s, a){r + \gamma V_k (s')}
\end{align}

But Dynamic Programming requires us to know all of the transitions probabilites which is impossible in real life problems.
Also we need to loop through all states on every iteration which is not practible on large domain of S.

## Monte Carlo (MC) :
MC is all about learning from experience, where expected values can be approximated by sample means
Play a bunch of episodes, gather returns, average them
\begin{align}
        V(s) = E[G(t)|S(t)=s] \approx \frac{1}{N} \sum_{i=1}^{N} G_{i,s}
\end{align}

It gives us only Value function for the states that we met.
And the problem is that :
- it won't always work.
- requires many episodes.
- don't work on not episodic task.
- the episode might never end if it bumps into a wall.
- agent won't do it again, since policy choose argmax.
- it can leave many states unexplored ( one possible solution is the "random start" or the "epsilon-greedy" method)

## Temporal Difference Learning (TD) :
- Unique to RL
- MC: Sample returns based on an episode
- TD : Estimate returns based on current value function estimate
- We looked at TD(0).
- Instead of using G (which is the sampled return ), we use $ r + \gamma V(s') $ which estimates the return from the next state and onward r.
- Its like calculating the mean ( which is space consuming because we need to store all of the samples episodes ) but in a more optimized way : we can calculate current sample mean from last sample mean.
\begin{align}
V_t (s) = V_{t-1} (s) + frac{1}{t} [G_t - V_{t-1} (s)] = \frac{1}{t} \sum_{t'=1}^{t} G_{t'}
\end{align}

Which can be generalized like that (looking like Gradient Descent) :
\begin{align}
V(s) = V(s) + \alpha [G - V(s)]
\end{align}

**TD(0)**
- Instead of using G, we us current reward r and the next state value V(s)
- We only need to wait t+1 to update value for state at time t while MC need to wait the until the end of entire episode.

**TD Control**
\begin{align}
Q(s, a) \leftarrow Q(s, a) + \alpha [r + \gamma max_{a'} Q(s',a') - Q(s,a)]
\end{align}

But those methos do now work on larger problems with billions of states and actions ( because here we use a dictionary of state/action ), that's why we will need approximation methods

- Knowing that supervised-learning is used a function approximator
- We want to estimate V(s) or Q(s, a) :
\begin{align}
Input : x = \phi (s), Target : G
\end{align}

- Since reward is a real number, so is G, therefore we do regression, and the appropriate loss is a squared error
\begin{align}
E = \sum_{n=1}^{N} (G_n - V ( \phi ( S_n); 0))^2
\end{align}

- we need a differentiable model so we can use gradient descent
\begin{align}
V ( \phi ( S_n);\theta) = \theta^T \phi (S_n)
\end{align}

- $\theta$ is the model parameter, so that's what we need to update
\begin{align}
\theta = \theta - \alpha ( \frac{\delta E}{\delta \theta} ) = \theta + \alpha (G - V) \phi(S)
\end{align}

```python

```

Advanced AI : Deep Reinforcement Learning UDEMY/Introduction.ipynb


# Visualizing stationary paths of a  functional

Last revised: 02-Feb-2019 by Dick Furnstahl [furnstahl.1@osu.edu]

Consider the functional

$\begin{align}
  S = \int_{x_1}^{x_2} f[y(x), y'(x), x] \, dx
\end{align}$

with $y_1 = y(x_1)$ and $y_2 = y(x_2)$ fixed.  We denote by $y^*(x)$ the path that minimizes $S$ (or, more generally, makes it stationary).  Then we consider the class of candidate paths $y(x)$ given by

$\begin{align}
  y(x) = y^*(x) + \alpha \eta(x)
\end{align}$

where $\eta(x)$ is some function that vanishes at the endpoints: $\eta(x_1) = \eta(x_2) = 0$.  We can derive the Euler-Lagrange equations by minimizing $S(\alpha)$ with respect to $\alpha$.

Here we visualize this problem by considering a particular $S$, choosing among some possible $\eta(x)$ definitions, and seeing how $S$ is minimized with respect to $\alpha$.  We will also allow for an incorrect determination of $y^*(x)$, in which case we expect that the minimum alpha will give us a reasonable reproduction of the true $y^*(x)$.  The variation of $\alpha$ and the choice of functions will be made using widgets from `ipywidgets`.

## Looking at a plot of the functional evaluation versus $\alpha$

\[We'll use `%matplotlib notebook` so that we can modify figures without redrawing them.\]

```python
%matplotlib notebook
```

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

import ipywidgets as widgets
from IPython.display import display

```

### Functional from Taylor problem 6.9

This problem states: "Find the equation of the path from the origin $O$ to the point $P(1,1)$ in the $xy$ plane that makes the integral $\int_O^P (y'^2 + y y' + y^2)$ stationary.  The answer from solving the Euler-Lagrange equation is $y^*(x) = \sinh(x)/\sinh(1)$.

```python
def y_star(x):
    """Path that minimizes the functional in Taylor problem 6.9."""
    return np.sinh(x) / np.sinh(1.)
```

```python
delta_x = 0.001
x_pts = np.arange(0., 1., delta_x)

fig = plt.figure(figsize=(6,3),
                 num='Visualizing stationary paths of a functional')
ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)

def setup_figure():
    ax1.set_title('Show paths')
    ax1.plot(x_pts, y_star(x_pts), color='black', lw=2)
    ax1.set_xlabel('x')
    ax1.set_ylabel('y(x)')

    ax2.set_title('Evaluate functional')
    ax2.set_xlabel(r'$\alpha$')
    ax2.set_ylabel('functional')
    ax2.set_xlim(-0.4, 0.4)
    ax2.set_ylim(1.5, 3.)
    #ax2.axvline(0., color='black', alpha=0.3)
    ax2.axhline(evaluate_functional(x_pts, y_star(x_pts)),
                                    color='black', alpha=0.3)

    fig.tight_layout()

def evaluate_functional(x_pts, y_pts):
    """Given arrays of x and y points, evaluate the functional from 6.9."""
    # The numpy gradient function takes the derivative of an array y_pts
    #  that is a function of points x in x_pts.
    y_deriv_pts = np.gradient(y_pts, x_pts)
    f = y_deriv_pts**2 + y_pts * y_deriv_pts + y_pts**2

    # Use the numpy trapezoid rule (trapz) to do the integral over f.
    return np.trapz(f, x_pts)

def make_path(alpha, ax1_passed, ax2_passed,
              base_function='exact', eta_function='sine'):
    """Given a base function, which may be the exact y^*(x) or a guess that
       is not correct, generate and plot the path corresponding to adding
       alpha*eta(x) to the base function, with eta(x) chosen among some
       functions that vanish at the endpoints in x.
    """
    # map x_pts to zero to 1 (it may already be there)
    x_mapped_pts = (x_pts - x_pts[0]) / (x_pts[-1] - x_pts[0])

    # Choices for the base function
    if (base_function == 'exact'):
        base = lambda x : y_star(x)
    elif (base_function == 'guess 1'):
        base = lambda x : np.sinh(2.*x) / np.sinh(2.)
    elif (base_function == 'guess 2'):
        base = lambda x : x**3

    if (eta_function == 'sine'):
        eta = lambda x : np.sin(np.pi * x)
    elif (eta_function == 'parabola'):
        eta = lambda x : 4. * x * (1. - x)

    y_new_pts = base(x_pts) + alpha * eta(x_mapped_pts)

    ax1_passed.plot(x_pts, y_new_pts, color='red', lw=1)
    ax2_passed.plot(alpha, evaluate_functional(x_pts, y_new_pts), '.',
                    color='red')

def reset_graph(event):
    ax1.clear()
    ax2.clear()
    setup_figure()

button = widgets.Button(
    description='reset graph'
)
button.on_click(reset_graph)

widgets.interact(make_path,
                 alpha=widgets.FloatSlider(min=-1., max=1., step=.05,
                                           value=0.0, description=r'$\alpha$',
                                           continuous_update=False),
                 ax1_passed=widgets.fixed(ax1),
                 ax2_passed=widgets.fixed(ax2),
                 base_function=widgets.Dropdown(options=['exact', 'guess 1',
                                                         'guess 2'],
                                                value='exact',
                                                description='base function'),
                 eta_function=widgets.Dropdown(options=['sine', 'parabola'],
                                               value='sine',
                                               description=r'$\eta(x)$')
                )

setup_figure()
button
```

    <IPython.core.display.Javascript object>

    interactive(children=(FloatSlider(value=0.0, continuous_update=False, description='$\\alpha$', max=1.0, min=-1…

    Button(description='reset graph', style=ButtonStyle())

```python

```


```python
from sympy import *
```

```python
import matplotlib.pyplot
matplotlib.pyplot.rcParams['figure.figsize'] = 5, 5
```

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

```python
# Average elementary hue angles
R = 0
Y = 67
G = 140
C = 180
B = 239
M = 303

k_r = (0.299*0.2126)**0.5
k_g = (0.587*0.7152)**0.5
k_b = (0.114*0.0722)**0.5

# Delta hue
d_RY = Y - R
d_YG = G - Y
d_GC = C - G
d_CB = B - C
d_BM = M - B
d_MR = 360 - M

# Luminosity
R_L = k_r
Y_L = k_r + k_g - 0.2
G_L = k_g - 0.1
C_L = k_g + k_b - 0.1
B_L = k_b
M_L = k_b + k_r

# Slope
s_RY = (Y_L - R_L)/d_RY
s_YG = (G_L - Y_L)/d_YG
s_GC = (C_L - G_L)/d_GC
s_CB = (B_L - C_L)/d_CB
s_BM = (M_L - B_L)/d_BM
s_MR = (R_L - M_L)/d_MR

plot(
    (-(-R*s_RY + R_L + h*s_RY - 0.5), (h, R, Y)),
    (-(-Y*s_YG + Y_L + h*s_YG - 0.5), (h, Y, G)),
    (-(-G*s_GC + G_L + h*s_GC - 0.5), (h, G, C)),
    (-(-C*s_CB + C_L + h*s_CB - 0.5), (h, C, B)),
    (-(-B*s_BM + B_L + h*s_BM - 0.5), (h, B, M)),
    (-(-M*s_MR + M_L + h*s_MR - 0.5), (h, M, 360)),
    (0.5, (h, 0, 360)),
    axis_center=(0, 0),
);
```

```python
[R_L, Y_L, G_L, C_L, B_L, M_L]
```

    [0.25212576226954675,
     0.7000627962473862,
     0.5479370339778395,
     0.6386607905138207,
     0.09072375653598125,
     0.342849518805528]

```python
R, Y, G, C, B, M, d_RY, d_YG, d_GC, d_CB, d_BM, d_MR, R_L, Y_L, G_L, C_L, B_L, M_L, s_RY, s_YG, s_GC, s_CB, s_BM, s_MR = \
    symbols("R Y G C B M d_RY d_YG d_GC d_CB d_BM d_MR R_L Y_L G_L C_L B_L M_L s_RY s_YG s_GC s_CB s_BM s_MR")
```

```python
[
-(-R*s_RY + R_L + h*s_RY - 0.5),
-(-Y*s_YG + Y_L + h*s_YG - 0.5),
-(-G*s_GC + G_L + h*s_GC - 0.5),
-(-C*s_CB + C_L + h*s_CB - 0.5),
-(-B*s_BM + B_L + h*s_BM - 0.5),
-(-M*s_MR + M_L + h*s_MR - 0.5),
]
```

    [R*s_RY - R_L - h*s_RY + 0.5,
     Y*s_YG - Y_L - h*s_YG + 0.5,
     G*s_GC - G_L - h*s_GC + 0.5,
     C*s_CB - C_L - h*s_CB + 0.5,
     B*s_BM - B_L - h*s_BM + 0.5,
     M*s_MR - M_L - h*s_MR + 0.5]

```python
plot((x, (x, 0, 0.5)),
     (1 - x, (x, 0.5, 1)),
     xlim=(-2, 2), ylim=(-2, 2),
     axiscenter=(0.0));
```

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

```python
plot(-(2*x - 1)**2/2 + 1/2, xlim=(-2, 2), ylim=(-2, 2), axiscenter=(0.0));
```

```python
(-(2*x - 1)**2 + 1)/2
```

    -(2*x - 1)**2/2 + 1/2

```python
plot(2*x**2,
     2*(x - 1)**2,
     xlim=(-2, 2), ylim=(-2, 2), axiscenter=(0.0));
```

```python

```


# Laboratorium

```python
import numpy as np
```

```python

```

#### Zadanie 1
Napisz metodę, która oblicza pole między osią OX a wykresem funkcji na danym przedziale za pomocą sum Riemanna (przybliżeń prostokątami). Sprawdź jej działanie na wybranej przez siebie funkcji porównując z wynikiem całkownia symbolicznego.

Korzystając z symbolicznych pochodnych i szukania ekstremów dobierz takie parametry do swojej funkcji, aby błąd całkowania był mniejszy niż $10^{-5}$.

```python
import numpy as np
from sympy import*
import math

def calka(funkcja,a,b,N):
  podzial=np.linspace(a,b,N)
  podstawa=(b-a)/(N-1)
  wartosc=0
  for i in range(N-1):
    wartosc+=podstawa*abs(funkcja(podzial[i]))
  return wartosc

def supremum(f, a, b):
  pochodna  = diff(f(x), x)
  druga_pochodna = diff(pochodna, x)
  kandydaci = (solve(druga_pochodna, x))
  kandydaci.extend([a, b])  #Funkcja może być malejąca na danym odcinku, zatem wtedy wartości maksymalnej szukamy na krańcach.
  sup = []
  for i in kandydaci:
    if a<=i<=b:
      sup.append(abs(pochodna.subs(x, i)))
  return (max(sup))

print("Podaj przedział całkowania: ")
a = int(input("a = "))
b = int(input("b = "))

x, N_ = sym.symbols("x, N_")

f = lambda x: 4/(1 + x**2)

M = supremum(f, a, b).evalf()

N_ = ((solve((b-a)**2 * M/(2*N_) - 10**(-5), N_)))
N_ = int((math.ceil(N_[0])) + 1)

blad = abs(integrate(f(x), (x, a, b)) - calka(f, a, b, N_))

if blad <= 10**(-5):
  print("Yes")

```

    Podaj przedział całkowania:
    a = 0
    b = 1
    Yes

#### Zadanie 2
a) Zaimplementuj w postaci funkcji algorytm obliczania pierwiastka kwadratowego z $x>0$ z wykorzystaniem algorytmu Herona. Funkcja powinna przyjmować jako argumenty liczby: $x$, punkt startowy $x_0$ oraz liczbę kroków (wyrazów ciągu do obliczenia).

b) Następnie, dodaj argument show_step (domyślnie ustawiony jako False), który będzie pozwalał wyświetlać kolejne kroki przybliżenia aż do zadanego.

c) Na bazie a) stwórz nową funkcję, która wykona odpowiednią liczbę kroków by przybliżenie miało podaną dokładność

```python

```

```python

```

```python

```

```python

```

# NumPy

np.ndarray (N-dimensional array)

Najważniejsze atrybuty:
* shape
* ndim
* size
* dtype (int,float,bool,complex)
* nbytes

```python
tab=np.array([[1,2],[3,4],[4,1]])
```

```python
tab.nbytes
```

    48

```python

```

```python

```

#### Zadanie 3

Stwórz dowolną tablicę liczb całkowitych ndarray z zagnieżdżonej listy. Sprawdź jej różne atrybuty. Spróbuj zamienić wszystkie liczby całkowite na zmiennoprzecinkowe.

```python
tab2=np.array(tab,dtype=float)
```

```python
tab2
```

    array([[1., 2.],
           [3., 4.],
           [4., 1.]])

```python
tab3=np.array([-1,0,1])
```

```python
np.sqrt(tab3)
```

    /usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in sqrt
      """Entry point for launching an IPython kernel.

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

```python
tab4=tab3.astype(complex)
tab4
np.sqrt(tab4).imag
```

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

### Konstruktory:
* np.array - tworzy z różnych tablicopodobnych typów
* np.zeros - same zera
* np.ones - same jedynki
* np.eye - macierz identycznościowa
* np.diag - macierz z elementami na przekątnej

* **!** np.linspace(start,koniec (domknięty),liczba elementów)
* **!** np.arange(start,koniec (otwarty),krok)
* np.meshgrid - tworzy siatkę punktów z pojedynczych wektorów) - zwraca tablice poszczególnych współrzędnych

from scipy.linalg import block_diag

```python
np.zeros((3,3))
```

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

```python
np.meshgrid([1,2,3],['a','b','c'])
```

    [array([[1, 2, 3],
            [1, 2, 3],
            [1, 2, 3]]), array([['a', 'a', 'a'],
            ['b', 'b', 'b'],
            ['c', 'c', 'c']], dtype='<U1')]

```python

```

```python

```

#### Zadanie 4
Skonstruuj macierz:
$$
Mat1=\begin{bmatrix}
1 &1 &0&0&0&0&0\\
1&1&0&0&0&0&0\\
0&0&0&0&0&0&0 \\
0&0&0&0&0&0&0 \\
0&0&0&0&3&0&0 \\
0&0&0&0&0&2&4 \\
0&0&0&0&0&4&2 \\
\end{bmatrix}
$$

```python
from scipy.linalg import block_diag
Mat1=block_diag(np.ones((2,2)),np.zeros((2,2)),3,(4*np.ones((2,2))-2*np.eye(2)))
Mat1
```

    array([[1., 1., 0., 0., 0., 0., 0.],
           [1., 1., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 3., 0., 0.],
           [0., 0., 0., 0., 0., 2., 4.],
           [0., 0., 0., 0., 0., 4., 2.]])

```python
Mat1[0,0]=3
```

```python
Mat1
```

    array([[3., 1., 0., 0., 0., 0., 0.],
           [1., 1., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 3., 0., 0.],
           [0., 0., 0., 0., 0., 2., 4.],
           [0., 0., 0., 0., 0., 4., 2.]])

```python
Mat1[:,::-1]
```

    array([[0., 0., 0., 0., 0., 1., 3.],
           [0., 0., 0., 0., 0., 1., 1.],
           [0., 0., 0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0., 0., 0.],
           [0., 0., 3., 0., 0., 0., 0.],
           [4., 2., 0., 0., 0., 0., 0.],
           [2., 4., 0., 0., 0., 0., 0.]])

#### Zadanie 5
W macierzy Mat1 zamień miejscami lewy górny narożnik 4X4 z prawym dolnym 3x3 (nazwij wynik Mat2)

#### Zadanie 6
Stwórz macierz Mat3, która będzie zawierała tylko elementy z Mat2 o obu parzystych współrzędnych.

```python
Mat1[:4,:4]
```

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

```python
Mat1[-3:,-3:]
```

    array([[3., 0., 0.],
           [0., 2., 4.],
           [0., 4., 2.]])

```python
Mat2=block_diag(Mat1[-3:,-3:],Mat1[:4,:4])
```

```python
Mat3=Mat2[1::2,1::2]
```

### Zadanie 7
Utwórz tablicę

$$
\begin{bmatrix}
1&2&3&4\\
5&6&7&8 \\
9&10&11&12\\
13&14&15&16
\end{bmatrix}
$$

```python

```

```python

```

```python

```

Inne przydatne funkcje:

* łączenie macierzy ```np.concatenate((tab1,tab2),argumenty)```
* rozdzielanie macierzy ```np.array_split(tab1, \<liczba czesci\>)```
* wybieranie indeksów spełniających logiczny warunek: ```np.where(tab1>0)```
* sortowanie ```np.sort(tab1)```

```python

```

```python

```

```python

```

```python

```


# Amostragem (Sampling rate)

A amostragem é definida como,
$$ f_s = \frac{1}{dt} $$
em que $dt$ é a variação de tempo entre as observações

pelos dados abaixo, $dt$ médio das RRLyraes é $6.7884$, ou seja, a amostragem, ou sampling rate, é
$$f_s = \frac{1}{6.7884} =  0.1473$$

Agora, vou gerar um sinal senoidal simples com essa amostragem,

```
'''
gerar um sinal senoidal com a amostragem acima e o periodo médio da RRLyraes
'''

tf = 4539.4593
ti = 2152.5019
n = 351.6
dt = (tf-ti)/n
fs = 1/dt

time = np.arange(ti, tf, 1/fs)
ttime = np.arange(ti,tf,dt/4)
p= 0.576
w = 2*np.pi/p

#Modificando a amostragem

Ny_f = fs/2
time2 = np.arange(ti, tf, 1/Ny_f)
#plot(time2, np.sin(w*time2), 'ko', time2, np.sin(w*time2), 'r-')
#xlim(2500,3000)

subplot(211) #dados sintéticos com a amostragem padrao
plot(time, np.sin(w*time), 'ro', ttime, np.sin(w*ttime), 'b-')
xlim(2500,3000)

subplot(212) # dadis sintéticos com a frequencia de Nyquist (metade da amostragem padrao)
plot(time2, np.sin(w*time2), 'ko', ttime, np.sin(w*ttime), 'r-')
xlim(2500,3000)
```

# Amostragem dos Dados do OGLE III

## RRLyrae AB

De acordo com o artigo http://acta.astrouw.edu.pl/Vol59/n1/pdf/pap_59_1_1.pdf, 95% da RRLyr AB possuem periodos na faixa entre 0.45 ~ 0.75 dias com a média dos periodos sendo $<P_{ab}> 0.576$ dias.

A ideia é descobrir a quantidade média de pontos de observação para as estrelas para descobrir uma amostragem média.

A partir dos dados das estrelas, vou fazer os histogramas para determinar o ponto inicial e final de observações e quantidade de pontos entre essas observações.

```
#Pontos iniciais
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
dados = np.array(np.loadtxt('/home/glauffer/Dropbox/FURG/final_project/data/rrab_ti_tf_pontos.txt'))

#a, b, c = plt.hist(dados.T[0], bins=30)

#plt.xlim(400, 2600)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
a, b, c = ax.hist(dados.T[0], bins=90)
ax2.hist(dados.T[0], bins=90)

ax.set_xlim(0,1000) # most of the data
ax2.set_xlim(2000,2500) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d),(-d,+d), **kwargs) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal

kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal

#plt.title('Data Juliana $t_i$  RRab, max = %s'%b[np.argmax(a)])
#plt.suptitle('HJD $t_i$  max = %s'%b[np.argmax(a)], fontsize = 18)
plt.suptitle('Histograma - $t_i$', fontsize = 18)
#ax.set_ylabel('Frequência', fontsize = 14)
#ax2.set_xlabel('Tempo Inicial')

fig.text(0.5, 0.0, 'Tempo Inicial [JD]', ha='center', fontsize = 14)
fig.text(0.0, 0.5, 'Frequência', va='center', rotation='vertical', fontsize = 14)

b[np.argmax(a)]

#plt.show()
#plt.savefig('hist_ti.png', dpi=100)
```

```
#Pontos finais
a, b, c = plt.hist(dados.T[1], bins=300)
plt.xlim(4300, 4600)
#plt.title('Histograma $t_f$  RRab, max = %s'%b[np.argmax(a)])
plt.title('Histograma - $t_f$', fontsize=18)
plt.xlabel('Tempo Final [JD]', fontsize=14)
plt.ylabel('Frequência', fontsize=14)
#plt.title('HJD $t_f$  max = %s'%b[np.argmax(a)])
#plt.savefig('hist_tf.png', dpi=100)
b[np.argmax(a)]

```

```
#Quantidade de pontos
a, b, c = plt.hist(dados.T[2], bins=140)
plt.xlim(0,1400)
#plt.title('Quantidade de pontos RRab, max = %s'%b[np.argmax(a)])
#plt.title('Histograma $n$, max = %s'%b[np.argmax(a)])
plt.title('Histograma - $n$', fontsize=18)
plt.xlabel('Quantidade de pontos', fontsize=14)
plt.ylabel('Frequência', fontsize=14)
b[np.argmax(a)]
#plt.savefig('hist_n.png', dpi=100)
```

A partir dos histogramas, temos que a maior frequencia dos dados são para $t_i = 2152.5019$, $t_f = 4539.4593$ e $n = 351.6$

De acordo com o artigo, o tempo de observação é $t_i = HJD - 2450000$, assim, o valor correto para os dados são, $t_i = 247152.5019$, $t_f = 249539.4593$

Com estes dados, podemos calcular a amostragem da seguinte forma,
$$ f_s= \frac{1}{dt}$$

$$dt= \frac{t_f - t_i}{n}$$

$$f_s = \frac{n}{t_f - t_i}$$

Ou seja, quanto maior a quantidade de pontos maior a amostragem. Mantendo os pontos final e inicial fixos, posso lidar com a quantidade de pontos e a amostragem de forma simples.

```
tf = 4539.4593
ti = 2152.5019
n = 351.6

dt = (tf - ti) / n

p_med = 0.576

fs = 1 / dt

print('dt = ', dt)

print('amostragem = ', fs)

```

    dt =  6.788843572241183
    amostragem =  0.1473004922500921

Os dados podem ser gerados da seguinte forma,
\begin{align}
m(t) &= A_0 + \sum_i^3 A_n \sin \Big( \frac{2 n \pi t}{P} \Big) + \sigma
\end{align}

em que,
\begin{align}
A_0 = 15 \\
A_1 = -0.5 \\
A_2 = 0.15 \\
A_3 = -0.05 \\
P = \text{Período, } \\
\sigma = \text{ruído}
\end{align}

```
def curva_de_luz(tempo, periodo, ruido, sigma,
					a0=15, a1=-0.5,	a2=0.15, a3=-0.05):
	w = 2*np.pi*tempo/periodo

	mag = a0 + a1*np.sin(w) + a2*np.sin(2*w) + \
			a3*np.sin(3*w) + ruido*sigma
	return mag

#Gerar curva com a amostragem necessaria
a = 0.5
amos = a*fs

tau = np.arange(2152.5019, 4539.4593, 1/amos)
n = np.arange(50, 350, 100)
b = np.linspace(0.1, 1.0, 10)

n = size(tau)
sigma = np.random.normal(0.0, 1.0, tau.size)
m = curva_de_luz(tau, 0.576, 0, sigma)

#Gerar curva original com maior quantidade de pontos
ttau = np.arange(2152.5019, 4539.4593, dt/4)
nn = size(ttau)
ssigma = np.random.normal(0.0, 1.0, ttau.size)
curva_original = curva_de_luz(ttau, 0.576, 0, ssigma)

#xlim(2150, 2160)
#subplot(211)
plot(tau, m, 'ko', label=str(a)+'*fs')
plot(ttau, curva_original, 'b-', label='full data')
plt.legend()
xlim(2500,3000)

#subplot(212)
#plot(ttau,

```

```

```

```

```


# Exercise Numpy/Scipy

### 2. Solving ordinary differential equations (ODEs) using scipy

Consider the following hypothetical reaction system which was first proposed by Alfred J. Lotka (Lotka-Voltera System):
$$ \begin{align}
r1:& \;\; X \rightarrow 2 X \\
r2:& \;\; X + Y \rightarrow 2 Y \\
r3:& \;\; Y \rightarrow \varnothing
\end{align} $$

1. set up the stoichiometric matrix of the system (this can be done without Python)
2. the rate of change of the species participating in a reaction system can be written as
$$ \frac {dS} {dt} = N \cdot v(S,p) $$
with the vector of species concentrations $S$, the stoichiometric matrix N and the vector of reaction velocities $v(S,p)$ ($p$ denotes a vector of parmameters)
define a function `dSdt( S, t )` which computes the rate of change for each species, given the vector of current species concentrations $S$ and the current time $t$. Assume that all reactions follow irreversible mass-action kinetics with parameters $k_1 = 1.5$, $k_2 = 0.8 $ and $k_3 = 0.9$, each equal to
3. Use the scipy ODE integrator scipy.integrate.odeint to solve the reaction system numerically on the time interval $t \in [0, 30]$ using the following initial conditions: $X(0)= Y(0) = 1$

```python
import numpy as np
import scipy as sp
import scipy.integrate
from matplotlib import pyplot as plt
plt.style.use('ggplot')
%matplotlib inline

```

```python

```


# Dynamic Optimization With Bequest Taxation
Imports and set magics:

```python
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import ipywidgets as widgets
import time
from scipy import linalg
from scipy import optimize
import sympy as sm
from IPython.display import display

# autoreload modules when code is run
%load_ext autoreload
%autoreload 2

# local modules
import modelproject

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

To start, we consider a simple two period consumer problem without uncertainty, where the following definitions of **variables** and **parameters** apply:

* $m$ is the endowment (cash-on-hand). In this simplified version the consumer only get an endowment in period 1.
* $c$ is consumption
* $y$ is income
* $r$ is the interest rate
* $\beta > 0$, $\rho > 1$, $\alpha \in (0,1)$, $\nu > 0 $, $\kappa > 0$ are utility parameters
* Subscripts denotes periods

Furthermore, we introduce $\tau$ as the bequest taxation rate $0<\tau<1$.

The two period utility function, $U$, can then be written as

$$
\begin{align}
    U &= \max_{c_1,c_2}\frac{c_1^{1-\rho}}{1-\rho}+\beta\frac{c_2^{1-\rho}}{1-\rho} \\
    \text{s.t.} \\
    c_2 &= (1+r)(m_1-c_1) \\
    c_{1} &\in [0,m_{1}] \\
\end{align}
$$

where the constraint on consumption in period 2 is derived from the intertemporal budget constraint. Borrowing is not possible by the second constraint.

```python
# define symbols
m1 = sm.symbols('m_1')
c1 = sm.symbols('c_1')
c2 = sm.symbols('c_2')
r = sm.symbols('r')

beta = sm.symbols('beta')
kappa = sm.symbols('kappa')
nu = sm.symbols('nu')
rho = sm.symbols('rho')

# define objective
m2 = (1+r)*(m1-c1)
obj = c1**(1-rho)/(1-rho)+beta*(c2**(1-rho)/(1-rho))

# define constraint and solve for c2
cons = sm.Eq(c2,(1+r)*(m1-c1))
c2_from_cons = sm.solve(cons,c2)

# substitute for c2 and take first order conditions
obj_subs = obj.subs(c2,c2_from_cons[0])

dU_dc1 = sm.diff(obj_subs,c1)
dU_dc1
```

$\displaystyle \frac{\beta \left(- r - 1\right) \left(- c_{1} r - c_{1} + m_{1} r + m_{1}\right)^{1 - \rho}}{- c_{1} r - c_{1} + m_{1} r + m_{1}} + \frac{c_{1}^{1 - \rho}}{c_{1}}$

Before we can solve for optimal consumption in period 1, we need to help sympy understand that the above expression is equal to
$$
\begin{align}
    \frac{1}{c_1} &= \frac{\left(\beta(1+r)\right)^\frac{1}{\rho}}{(1+r)(m_1-c_1)}
\end{align}
$$

```python
# help sympy collect terms
dU_dc1_simple = sm.Eq(1/c1,(beta*(1+r))**(1/rho)/((1+r)*(m1-c1)))
display(dU_dc1_simple)
```

$\displaystyle \frac{1}{c_{1}} = \frac{\left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}}}{\left(- c_{1} + m_{1}\right) \left(r + 1\right)}$

```python
# solve for optimal c1
c1_star_solve = sm.solve(dU_dc1_simple,c1)
c1_star = c1_star_solve[0]
```

$\displaystyle \frac{m_{1} \left(r + 1\right)}{r + \left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}} + 1}$

```python
# compute optimal c2
cons_subs = cons.subs(c1,c1_star[0])
c2_star_solve = sm.solve(cons_subs,c2)
c2_star = c2_star_solve[0]
```

$\displaystyle \frac{m_{1} \left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}} \left(r + 1\right)}{r + \left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}} + 1}$

```python
# lambdify functions for c1 and c2
c1_func = sm.lambdify((m1,r,beta,rho),c1_star)
c2_func = sm.lambdify((m1,r,beta,rho),c2_star)
```

```python
# define parameter values
m1 = 2
r = 0.03
beta = 0.95
rho = 2
```

```python
# solve analytically and numerically

c1_s, c2_s = modelproject.solve_for_consumption(m1,r,beta,rho)

print(f'analytical solution is: c1 = {c1_func(m1,r,beta,rho)[0]:.3f}, c2 = {c2_func(m1,r,beta,rho)[0]:.3f}')
print(f'numerical solution is: c1 = {c1_s:.3f}, c2 = {c2_s:.3f}')
print(f'analytical and numerical solution are equal with numerical precision: c1: {np.isclose(c1_func(m1,r,beta,rho)[0],c1_s)} c2: {np.isclose(c2_func(m1,r,beta,rho)[0],c2_s)}')
```

    analytical solution is: c1 = 1.020, c2 = 1.009
    numerical solution is: c1 = 1.020, c2 = 1.009
    analytical and numerical solution are equal with numerical precision: c1: True c2: True

Thus, the value function in **period 2** is given by:
$$
\begin{aligned}
v_{2}(m_{2}) &= \max_{c_{2}}\frac{c_{2}^{1-\rho}}{1-\rho}+(1-\tau)\nu\frac{(m_{2}-c_{2}+\kappa)^{1-\rho}}{1-\rho} \\
\text{s.t.} \\
c_{2} & \in [0,m_{2}]
\end{aligned}
$$

In **period 1**, the household solves

$$
\begin{aligned}
v_{1}(m_{1}) &= \max_{c_{1}}\frac{c_{1}^{1-\rho}}{1-\rho}+\beta{v_2}(m_2)\\&\text{s.t.}&\\
m_2 &= (1+r)(m_{1}-c_{1})+y_{2} \\
c_{1} &\in [0,m_{1}]\\
\end{aligned}
$$

```python

```


```python
""" File to graph several signals using the Fourier Transform
    and Fourier Coefficients
    Author: Zander Blasingame
    Course: EE 321 """

import numpy as np
from scipy import signal
import plotly.plotly as py
import plotly.graph_objs as go
```

```python
""" Function Definitions """

# Constants
N = 2000
SIGNAL_RANGE = 40
RESOLUTION = 100E-3  # 100ms

# define discrete unit impulse
d = lambda x: 1 if x == 0 else 0

# Construct signal from fourier coefficients
def construct_signal(X_k, period=2*np.pi, N=N):
    return lambda x: np.sum(X_k(k) * np.exp(2j*np.pi*k*x/period)
                            for k in range(-N, N, 1))

def mean_squared_error(signal_1, signal_2, N=N):
    return 1/N * np.sum(np.abs(signal_1 - signal_2)**2)
```

# Problems

## Problem 1a

```latex
%%latex
Given the following defintions for fourier coefficients, $X_k$

\begin{equation*}X_k = \delta(k) + \frac{1}{4}\delta(k-1) + \frac{1}{4}\delta(k+1) + \frac{1}{j2}\delta(k-2) - \frac{1}{j2}\delta(k+2)\end{equation*}
```

Given the following defintions for fourier coefficients, $X_k$

\begin{equation*}X_k = \delta(k) + \frac{1}{4}\delta(k-1) + \frac{1}{4}\delta(k+1) + \frac{1}{j2}\delta(k-2) + \frac{1}{j2}\delta(k+2)\end{equation*}

```python
""" Problem 1a """
X_k = lambda k: d(k) + 0.25*d(k-1) + 0.25*d(k+1)- 0.5j*d(k-2) - 0.5j*d(k+2)

# Construct and plot signal
x = construct_signal(X_k, 2)
t = np.arange(-SIGNAL_RANGE/10, SIGNAL_RANGE/10, RESOLUTION)

data = [go.Scatter(x=t, y=x(t).real)]
layout = go.Layout(xaxis=dict(title='Time (s)', showticklabels=True,
                              tickmode='linear',
                              tickangle=0,
                              dtick=5),
                   yaxis=dict(title='Amplitude x(t)'),
                   title='Signal 1a')

fig = go.Figure(data=data, layout=layout)

py.iplot(fig, filename='signal_1.png')
```

## Problem 1b

```latex
%%latex
Given the following definitions for the Fourier coefficients, $X_k$

\begin{equation*}X_k = \bigg\{\begin{matrix}
               &jk \; &|k| < 3\\
               &0 &\text{otherwise}
               \end{matrix}
\end{equation*}

```

Given the following definitions for the Fourier coefficients, $X_k$

\begin{equation*}X_k = \bigg\{\begin{matrix}
               &jk \; &|k| < 3\\
               &0 &\text{otherwise}
               \end{matrix}
\end{equation*}

```python
""" Problem 1b """
X_k = lambda k: 1j*k if abs(k) < 3 else 0

# Construct and plot signal
x = construct_signal(X_k)
t = np.arange(-SIGNAL_RANGE/2, SIGNAL_RANGE/2, RESOLUTION)

data = [go.Scatter(x=t, y=x(t).real)]
layout = go.Layout(xaxis=dict(title='Time (s)', showticklabels=True,
                              tickmode='linear',
                              tickangle=0,
                              dtick=5),
                   yaxis=dict(title='Amplitude x(t)'),
                   title='Signal 1b')

fig = go.Figure(data=data, layout=layout)

py.iplot(fig, filename='signal_2.png')
```

# Problem 2

```latex
%%latex

Let the reconstructed signal, $\hat{x}(t)$ given $N$ components be defined as follows
\begin{equation}
\hat{x}(t) = \sum_{k=-N}^N X_ke^{\frac{j2\pi k}{T}t}
\end{equation}
Moreover, let the mean squared error between the original signal, $x(t)$ and the reconstructed signal be given by
\begin{equation}
\mathbb{E}(N) = \frac{1}{T} \int_0^T |x(t) - \hat{x}(t)|^2 dt
\end{equation}
In a discrete form this is represented as
\begin{equation}
\mathbb{E}(N) = \frac{1}{N} \sum_{n=1}^N |x(n) - \hat{x}(n)|^2
\end{equation}
Let the complex Fourier coefficients be expressed as
\begin{equation*}X_k = \frac{1}{T} \int_{t_0}^{t_0 + \frac{T}{2}} x(t)e^{-j\frac{2\pi kt}{T}}dt\end{equation*}
```

Let the reconstructed signal, $\hat{x}(t)$ given $N$ components be defined as follows
\begin{equation}
\hat{x}(t) = \sum_{k=-N}^N X_ke^{\frac{j2\pi k}{T}t}
\end{equation}
Moreover, let the mean squared error between the original signal, $x(t)$ and the reconstructed signal be given by
\begin{equation}
\mathbb{E}(N) = \frac{1}{T} \int_0^T |x(t) - \hat{x}(t)|^2 dt
\end{equation}
In a discrete form this is represented as
\begin{equation}
\mathbb{E}(N) = \frac{1}{N} \sum_{n=1}^N |x(n) - \hat{x}(n)|^2
\end{equation}
Let the complex Fourier coefficients be expressed as
\begin{equation*}X_k = \frac{1}{T} \int_{t_0}^{t_0 + \frac{T}{2}} x(t)e^{-j\frac{2\pi kt}{T}}dt\end{equation*}

## Problem 2a

```latex
%%latex
Let $x_1(t)$ be defined as

\begin{equation*}x_1(t) = \sum_{n=-\infty}^{\infty} u(t-3n) - u(t - 3n - 2)\end{equation*}
where $u(n)$ is the Heaviside step function. This can be rewritten as
\begin{equation*}x_1(t) = \sum_{n=-\infty}^{\infty} \text{rect}\bigg(\frac{t}{2} - \frac{3n + 1}{2}\bigg)\end{equation*}
To find the complex Fourier coefficients look at two time ranges: [-1, 0] and [0, 2] such that
\begin{equation*}X_k = \frac{1}{3} \int_{-1}^{2} x_1(t)e^{-j\frac{2\pi kt}{3}}dt\end{equation*}
This can be simplified to
\begin{equation*}X_k = \frac{1}{3}\bigg[ \int_{-1}^{0} x_1(t)e^{-j\frac{2\pi kt}{3}}dt
        + \int_{0}^{2} x_1(t)e^{-j\frac{2\pi kt}{3}} dt\bigg]\end{equation*}
\begin{equation*}X_k = \frac{1}{3}\bigg[ \int_{-1}^{0} 0e^{-j\frac{2\pi kt}{3}}dt
        + \int_{0}^{2} 1e^{-j\frac{2\pi kt}{3}} dt\bigg]\end{equation*}
\begin{equation*}X_k = \frac{1}{3} \int_{0}^{2} e^{-j\frac{2\pi kt}{3}}dt\end{equation*}
\begin{equation*}X_k = \frac{j}{2\pi k}\bigg[e^{-j\frac{4\pi k}{3}} - 1 \bigg]\end{equation*}
$X_0$ is calculated by evaluating the following limit
\begin{align*}
X_0 &= \lim_{k \to 0} \frac{j}{2\pi k}\bigg[e^{-j\frac{4\pi k}{3}} - 1\bigg]
\end{align*}
By using L'Hospital's Rule
\begin{align*}
X_0 &= \lim_{k \to 0} \frac{j}{2\pi k}\bigg[\frac{-j4\pi k}{3}e^{-j\frac{4\pi k}{3}} - 0\bigg]\\
X_0 &= \lim_{k \to 0} \frac{2}{3}e^{-j\frac{4\pi k}{3}}\\
X_0 &= \frac{2}{3}
\end{align*}
Thus $X_k$ can be rewritten as
\begin{equation*}X_k = \frac{j}{2\pi k}\bigg[e^{-j\frac{4\pi k}{3}} - 1 \bigg] + \frac{2}{3}\delta(k)\end{equation*}

```

Let $x_1(t)$ be defined as

\begin{equation*}x_1(t) = \sum_{n=-\infty}^{\infty} u(t-3n) - u(t - 3n - 2)\end{equation*}
where $u(n)$ is the Heaviside step function. This can be rewritten as
\begin{equation*}x_1(t) = \sum_{n=-\infty}^{\infty} \text{rect}\bigg(\frac{t}{2} - \frac{3n + 1}{2}\bigg)\end{equation*}
To find the complex Fourier coefficients look at two time ranges: [-1, 0] and [0, 2] such that
\begin{equation*}X_k = \frac{1}{3} \int_{-1}^{2} x_1(t)e^{-j\frac{2\pi kt}{3}}dt\end{equation*}
This can be simplified to
\begin{equation*}X_k = \frac{1}{3}\bigg[ \int_{-1}^{0} x_1(t)e^{-j\frac{2\pi kt}{3}}dt
        + \int_{0}^{2} x_1(t)e^{-j\frac{2\pi kt}{3}} dt\bigg]\end{equation*}
\begin{equation*}X_k = \frac{1}{3}\bigg[ \int_{-1}^{0} 0e^{-j\frac{2\pi kt}{3}}dt
        + \int_{0}^{2} 1e^{-j\frac{2\pi kt}{3}} dt\bigg]\end{equation*}
\begin{equation*}X_k = \frac{1}{3} \int_{0}^{2} e^{-j\frac{2\pi kt}{3}}dt\end{equation*}
\begin{equation*}X_k = \frac{j}{2\pi k}\bigg[e^{-j\frac{4\pi k}{3}} - 1 \bigg]\end{equation*}
$X_0$ is calculated by evaluating the following limit
\begin{align*}
X_0 &= \lim_{k \to 0} \frac{j}{2\pi k}\bigg[e^{-j\frac{4\pi k}{3}} - 1\bigg]
\end{align*}
By using L'Hospital's Rule
\begin{align*}
X_0 &= \lim_{k \to 0} \frac{j}{2\pi k}\bigg[\frac{-j4\pi k}{3}e^{-j\frac{4\pi k}{3}} - 0\bigg]\\
X_0 &= \lim_{k \to 0} \frac{2}{3}e^{-j\frac{4\pi k}{3}}\\
X_0 &= \frac{2}{3}
\end{align*}
Thus $X_k$ can be rewritten as
\begin{equation*}X_k = \frac{j}{2\pi k}\bigg[e^{-j\frac{4\pi k}{3}} - 1 \bigg] + \frac{2}{3}\delta(k)\end{equation*}

```python
""" Problem 2a """
# added term to avoid dividing by zero
X_k = lambda k: 1j/(2*np.pi*k + 1E-9) * (np.exp(-1j*4*np.pi*k/3) - 1) + 2/3*d(k)

# Construct and plot signal
x = construct_signal(X_k, 3)
t = np.arange(0.001, 6, 0.01)

data = [go.Scatter(x=t, y=x(t).real)]
layout = go.Layout(xaxis=dict(title='Time (s)', showticklabels=True,
                              tickmode='linear',
                              tickangle=0,
                              dtick=5),
                   yaxis=dict(title='Amplitude x(t)'),
                   title='Signal 2a')

fig = go.Figure(data=data, layout=layout)

py.iplot(fig, filename='signal_3.png')

```

```python
# ideal signal
x_ideal = lambda t: signal.square(2/3*np.pi*t, duty=2/3)/2 + 1/2
t = np.arange(0.001, 6, 0.01)

num_components = np.arange(1,500,10)
errors = [mean_squared_error(construct_signal(X_k, 3, N=n)(t), x_ideal(t), N=n)
          for n in num_components]

data = [go.Scatter(x=num_components, y=errors)]
layout = go.Layout(xaxis=dict(title='N', showticklabels=True,
                              tickmode='linear',
                              tickangle=0,
                              dtick=25),
                   yaxis=dict(title='Error',
                              type='log'),
                   title='Reconstruction Error 2a')

fig = go.Figure(data=data, layout=layout)

py.iplot(fig, filename='rec_err_1.png')
```

```latex
%%latex
Using the figure above it is found that the mean squared error equals 0.01 at roughly 80 components and it equals 0.001 at roughly 325 components.
```

Using the figure above it is found that the mean squared error equals 0.01 at roughly 80 components and it equals 0.001 at roughly 325 components.

## Problem 2b

```latex
%%latex
Let $x_2(t)$ be defined as

\begin{equation*}x_2(t) = \sum_{n=-\infty}^{\infty} \text{tri}(t-(2n+1))\end{equation*}
where $\text{tri}(t)$ is the triangular pulse. This can be expressed as
\begin{equation}
\text{tri}(t) = \text{rect}(t/2)(1-|t|)
\end{equation}
The Fourier coefficients can be expressed as
\begin{equation}
X_k = \frac{1}{2} \int_{-1}^{1} \text{tri}(t)\cdot e^{-j\pi kt}dt
\end{equation}
Further simplification yields the following
\begin{align*}
X_k &= \frac{1}{2}\bigg[\int_{-1}^{0} -t \cdot e^{-j\pi kt}dt
        + \int_{0}^{1} t \cdot e^{-j\pi kt}dt\bigg]
\end{align*}
which evaluates to
\begin{align*}
X_k &= \frac{e^{-j\pi k}\big(-j\pi k + e^{j\pi k} - 1\big)
             + e^{j\pi k}\big(j\pi k - 1\big) + 1}{-2\pi^2k^2}\\
X_k &= \frac{e^{-j\pi k}\big(j\pi k - e^{j\pi k} + 1\big)
             - e^{j\pi k}\big(j\pi k - 1\big) - 1}{2\pi^2k^2}\\
X_k &= \frac{e^{-j\pi k}\big(j\pi k + 1\big)
             - e^{j\pi k}\big(j\pi k - 1\big) - 2}{2\pi^2k^2}\\
X_k &= \frac{j\pi k\big(e^{j\pi k} - e^{-j\pi k}\big) + \big(e^{j\pi k} + e^{-j\pi k}\big) - 2}{2\pi^2k^2}\\
X_k &= \frac{j\pi k\big(2j\sin(\pi k)\big) + \big(2\cos(\pi k)\big) - 2}{2\pi^2k^2}\\
X_k &= \frac{2\cos(\pi k) -2\pi k\sin(\pi k)  - 2}{2\pi^2k^2}\\
\end{align*}
It can be shown that $\cos(\pi k) = (-1)^k\; \forall k \in \mathbb{Z}$ and $\sin(\pi k) = 0\; \forall k \in \mathbb{Z}$.
Therefore the Fourier coefficients become
\begin{equation}
X_k = \frac{-1 + (-1)^k}{\pi^2k^2}
\end{equation}

To find $X_0$ evaulate the integral definition for the Fourier coefficients
with the limit $k\to 0$ such that
\begin{align*}
X_0 &= \lim_{k\to 0} \frac{1}{2}\bigg[\int_{-1}^{0} -t \cdot e^{-j\pi kt}dt
        + \int_{0}^{1} t \cdot e^{-j\pi kt}dt\bigg]\\
X_0 &= \frac{1}{2}\bigg[\int_{-1}^{0} -t dt
        + \int_{0}^{1} t dt\bigg]\\
X_0 &= \frac{1}{2}\bigg[\frac{-t^2}{2}\bigg|_{-1}^0
        + \frac{t^2}{t}\bigg|_0^1\bigg]\\
X_0 &= \frac{1}{2}
\end{align*}
Therefore, $X_k$ can be written as
\begin{equation}
X_k = \frac{e^{-j\pi k}\big(j\pi k - e^{j\pi k} + 1\big)
             - e^{j\pi k}\big(j\pi k - 1\big) - 1}{2\pi^2k^2} + \frac{1}{2}\delta(k)
\end{equation}
```

Let $x_2(t)$ be defined as

\begin{equation*}x_2(t) = \sum_{n=-\infty}^{\infty} \text{tri}(t-(2n+1))\end{equation*}
where $\text{tri}(t)$ is the triangular pulse. This can be expressed as
\begin{equation}
\text{tri}(t) = \text{rect}(t/2)(1-|t|)
\end{equation}
The Fourier coefficients can be expressed as
\begin{equation}
X_k = \frac{1}{2} \int_{-1}^{1} \text{tri}(t)\cdot e^{-j\pi kt}dt
\end{equation}
Further simplification yields the following
\begin{align*}
X_k &= \frac{1}{2}\bigg[\int_{-1}^{0} -t \cdot e^{-j\pi kt}dt
        + \int_{0}^{1} t \cdot e^{-j\pi kt}dt\bigg]
\end{align*}
which evaluates to
\begin{align*}
X_k &= \frac{e^{-j\pi k}\big(-j\pi k + e^{j\pi k} - 1\big)
             + e^{j\pi k}\big(j\pi k - 1\big) + 1}{-2\pi^2k^2}\\
X_k &= \frac{e^{-j\pi k}\big(j\pi k - e^{j\pi k} + 1\big)
             - e^{j\pi k}\big(j\pi k - 1\big) - 1}{2\pi^2k^2}\\
X_k &= \frac{e^{-j\pi k}\big(j\pi k + 1\big)
             - e^{j\pi k}\big(j\pi k - 1\big) - 2}{2\pi^2k^2}\\
X_k &= \frac{j\pi k\big(e^{j\pi k} - e^{-j\pi k}\big) + \big(e^{j\pi k} + e^{-j\pi k}\big) - 2}{2\pi^2k^2}\\
X_k &= \frac{j\pi k\big(2j\sin(\pi k)\big) + \big(2\cos(\pi k)\big) - 2}{2\pi^2k^2}\\
X_k &= \frac{2\cos(\pi k) -2\pi k\sin(\pi k)  - 2}{2\pi^2k^2}\\
\end{align*}
It can be shown that $\cos(\pi k) = (-1)^k\; \forall k \in \mathbb{Z}$ and $\sin(\pi k) = 0\; \forall k \in \mathbb{Z}$.
Therefore the Fourier coefficients become
\begin{equation}
X_k = \frac{-1 + (-1)^k}{\pi^2k^2}
\end{equation}

To find $X_0$ evaulate the integral definition for the Fourier coefficients
with the limit $k\to 0$ such that
\begin{align*}
X_0 &= \lim_{k\to 0} \frac{1}{2}\bigg[\int_{-1}^{0} -t \cdot e^{-j\pi kt}dt
        + \int_{0}^{1} t \cdot e^{-j\pi kt}dt\bigg]\\
X_0 &= \frac{1}{2}\bigg[\int_{-1}^{0} -t dt
        + \int_{0}^{1} t dt\bigg]\\
X_0 &= \frac{1}{2}\bigg[\frac{-t^2}{2}\bigg|_{-1}^0
        + \frac{t^2}{t}\bigg|_0^1\bigg]\\
X_0 &= \frac{1}{2}
\end{align*}
Therefore, $X_k$ can be written as
\begin{equation}
X_k = \frac{e^{-j\pi k}\big(j\pi k - e^{j\pi k} + 1\big)
             - e^{j\pi k}\big(j\pi k - 1\big) - 1}{2\pi^2k^2} + \frac{1}{2}\delta(k)
\end{equation}

```python
""" Problem 2b """
X_k = lambda k: (-1 + (-1)**k)/(np.pi**2 * (k+1E-9)**2)

# Construct and plot signal
x = construct_signal(lambda k: X_k(k) + 0.5*d(k), 2)
t = np.arange(0.001, 2, 0.01)

data = [go.Scatter(x=t, y=[x(time).real for time in t])]
layout = go.Layout(xaxis=dict(title='Time (s)', showticklabels=True,
                              tickmode='linear',
                              tickangle=0,
                              dtick=5),
                   yaxis=dict(title='Amplitude x(t)'),
                   title='Signal 2b')

fig = go.Figure(data=data, layout=layout)

py.iplot(fig, filename='signal_4.png')

```

```python
# ideal signal
x_ideal = lambda t: 2 * np.abs(0.5*t - np.floor(0.5*t + 0.5))
t = np.arange(0.001, 2, 0.01)

num_components = np.arange(1,50,5)
errors = [mean_squared_error(construct_signal(lambda k: X_k(k) + 0.5*d(k), 2, N=n)(t), x_ideal(t), N=n)
          for n in num_components]

data = [go.Scatter(x=num_components, y=errors)]
layout = go.Layout(xaxis=dict(title='N', showticklabels=True,
                              tickmode='linear',
                              tickangle=0,
                              dtick=5),
                   yaxis=dict(title='Error',
                              type='log'),
                   title='Reconstruction Error 2b')

fig = go.Figure(data=data, layout=layout)

py.iplot(fig, filename='rec_err_2.png')
```

```latex
%%latex
Using the figure above it was found that the mean squared error equaled 0.01 at roughly 5 components and it equaled 0.001 at roughly 7 components.
```

Using the figure above it was found that the mean squared error equaled 0.01 at roughly 5 components and it equaled 0.001 at roughly 7 components.

```python

```


# Solving orbital equations with different algorithms

This notebook was adapted from `Orbit_games.ipynb`.

We consider energy plots and orbital solutions in polar coordinates for the general potential energy

$\begin{align}
   U(r) = k r^n
\end{align}$

for different ODE solution algorithms.  The `solve_ivp` function can itself be specified to use different solution methods (with the `method` keyword).  Here we will set it by default to use 'RK23', which is a variant on the Runge-Kutta second-order algorithm.  Second-order in this context means that the accuracy of a calculation will improve by a factor of $10^2 = 100$ if $\Delta t$ is reduced by a factor of ten.

We will compare it with the crudest algorithm, Euler's method, which is first order, and a second-order algorithm called Leapfrog, which is designed to be precisely time-reversal invariant.  This property guarantees conservation of energy, which is not true of the other algorithms we will consider.

To solve the differential equations for orbits, we have defined the $\mathbf{y}$
and $d\mathbf{y}/dt$ vectors as

$\begin{align}
  \mathbf{y} = \left(\begin{array}{c} r(t) \\ \dot r(t) \\ \phi(t)  \end{array} \right)
  \qquad
  \frac{d\mathbf{y}}{dt}
       = \left(\begin{array}{c} \dot r(t) \\ \ddot r(t) \\ \dot\phi(t) \end{array} \right)
       = \left(\begin{array}{c} \dot r(t) \\
                                 -\frac{1}{\mu}\frac{dU_{\rm eff}(r)}{dr} \\
                                 \frac{l}{\mu r^2} \end{array} \right)
\end{align}$

where we have substituted the differential equations for $\ddot r$ and $\dot\phi$.

Then Euler's method can be written as a simple prescription to obtain $\mathbf{y}_{i+1}$
from $\mathbf{y}_i$, where the subscripts label the elements of the `t_pts` array:
$\mathbf{y}_{i+1} = \mathbf{y}_i + \left(d\mathbf{y}/dt\right)_i \Delta t$, or, by components:

$\begin{align}
   r_{i+1} &= r_i + \frac{d\mathbf{y}_i[0]}{dt}  \Delta t  \\
   \dot r_{i+1} &= \dot r_{i} + \frac{d\mathbf{y}_i[1]}{dt}  \Delta t \\
   \phi_{i+1} &= \phi_i + \frac{d\mathbf{y}_i[2]}{dt} \Delta t
\end{align}$

**Look at the** `solve_ode_Euler` **method below and verify the algorithm is correctly implemented.**

The leapfrog method does better by evaluating $\dot r$ at a halfway time step before and after the $r$ evaluation,
which is both more accurate and incorporates time reversal:

$\begin{align}
   \dot r_{i+1/2} &= \dot r_{i} + \frac{d\mathbf{y}_i[1]}{dt}  \Delta t/2 \\
   r_{i+1} &= r_i +  \dot r_{i+1/2}  \Delta t  \\
   \dot r_{i+1} &= \dot r_{i+1/2} + \frac{d\mathbf{y}_{i+1}[1]}{dt}  \Delta t/2 \\
   \phi_{i+1} &= \phi_i + \frac{d\mathbf{y}_i[2]}{dt} \Delta t
\end{align}$

**Look at the** `solve_ode_Leapfrog` **method below and verify the algorithm is correctly implemented.**

A third method is the second-order Runge-Kutta algorithm, which we invoke from `solve_ivp` as `RK23`.
It does not use a fixed time-step as in our "homemade" implementations, so there is not a direct
comparison, but we can still check if it conserves energy.

**Run the notebook.  You are to turn in and comment on the "Change in energy with time" plot at the end.
Where do you see energy conserved or not conserved?  Show that Euler is first order and leapfrog is second
order by changing $\Delta t$; describe what you did and what you found.**

**Try another potential to see if you get the same general conclusions.**

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
```

```python
# Change the common font size
font_size = 14
plt.rcParams.update({'font.size': font_size})
```

```python

```

```python
class GravitationalOrbits():

    def __init__(self, m_1 = 1., m_2 = 1., G = 1.):
        self.m1 = m_1
        self.m2 = m_2
        self.G = G

    def dz_dt(self, t, z):
        r_12 = np.sqrt( (z[0]-z[4])**2 + (z[2] -z[6])**2 )

        return [z[1], self.G*self.m2* (z[4]-z[0]) / r_12**3, z[3], self.G*self.m2* (z[6] - z[2]) /r_12**3, z[5], -self.G *self.m1* (z[4] - z[0]) / r_12**3, z[7], -self.G*self.m1 * (z[6] - z[2]) /r_12**3 ]

    def solve_ode(self, t_pts, z_0, abserr = 1.0e-8, relerr = 1.0e-8):

        solution = solve_ivp(self.dz_dt, (t_pts[0], t_pts[-1]), z_0, t_eval = t_pts,
                            method ='RK23', atol = abserr, rtol =relerr)
        x_1, x_dot_1, y_1, y_dot_1, x_2, x_dot_2, y_2, y_dot_2 = solution.y

        return x_1, x_dot_1, y_1, y_dot_1, x_2, x_dot_2, y_2, y_dot_2

    def solve_ode_Leapfrog(self, t_pts, z_0):

        delta_t = t_pts[1] - t_pts[0]

        x_1_0, x_dot_1_0, y_1_0, y_dot_1_0, x_2_0, x_dot_2_0, y_2_0, y_dot_2_0 = z_0

        num = len(t_pts)

        x_1 = np.zeros(num)
        x_dot_1 = np.zeros(num)
        x_dot_1_half = np.zeros(num)

        y_1 = np.zeros(num)
        y_dot_1 = np.zeros(num)
        y_dot_1_half = np.zeros(num)

        x_2 = np.zeros(num)
        x_dot_2 = np.zeros(num)
        x_dot_2_half = np.zeros(num)

        y_2 = np.zeros(num)
        y_dot_2 = np.zeros(num)
        y_dot_2_half = np.zeros(num)

        x_1[0] = x_1_0
        x_dot_1[0] = x_dot_1_0

        y_1[0] = y_1_0
        y_dot_1[0] = y_dot_1_0

        x_2[0] = x_2_0
        x_dot_2[0] = x_dot_2_0

        y_2[0] = y_2_0
        y_dot_2[0] = y_dot_2_0

        for i in np.arange(num - 1):

            t = t_pts[i]

            z = [x_1[i], x_dot_1[i], y_1[i], y_dot_1[i], x_2[i], x_dot_2[i], y_2[i], y_dot_2[i]]
            out = self.dz_dt(t, z)

            x_dot_1_half[i] = x_dot_1[i] + out[1] * delta_t/2.
            x_1[i + 1] = x_1[i] + x_dot_1_half[i] * delta_t

            y_dot_1_half[i] = y_dot_1[i] + out[3] * delta_t/2.
            y_1[i+1] = y_1[i] + y_dot_1_half[i] *delta_t

            x_dot_2_half[i] = x_dot_2[i] + out[5] * delta_t/2.
            x_2[i+1] = x_2[i] + x_dot_2_half[i] * delta_t

            y_dot_2_half[i] = y_dot_2[i] + out[7] + delta_t/2.
            y_2[i+1] = y_2[i] + y_dot_2_half[i] * delta_t

            z = [x_1[i+1], x_dot_1[i], y_1[i+1], y_dot_1[i], x_2[i+1], x_dot_2[i], y_2[i+1], y_dot_2[i]]

            out = self.dz_dt(t, z)

            x_dot_1[i+1] = x_dot_1_half[i] + out[1] * delta_t/2.
            y_dot_1[i+1] = y_dot_1_half[i] + out[3] * delta_t/2.
            x_dot_2[i+1] = x_dot_2_half[i] + out[5] * delta_t/2.
            y_dot_2[i+1] = y_dot_2_half[i] + out[7] * delta_t/2.

        return x_1, x_dot_1, y_1, y_dot_1, x_2, x_dot_2, y_2, y_dot_2

    def solve_ode_Leapfrog_n(self, t_pts, z_0):

        delta_t = t_pts[1] - t_pts[0]

        num = len(t_pts)

        n_tot = len(z_0)

        z = np.zeros( shape = (n_tot, num))
        dot_half = np.zeros( int(n_tot/2))
        z[:, 0] = z_0

        for i in np.arange(num - 1):
            t = t_pts[i]
            z_now = z[:,i]
            out = np.asarray(self.dz_dt(t, z_now))

            dot_half = z_now[1::2] + out[1::2]* delta_t/2.
            z[0::2, i+1] = z_now[0::2] + dot_half * delta_t

            z_now[0::2] = z[0::2, i+1]
            out = np.asarray(self.dz_dt(t, z_now))
            z[1::2, i+1] = dot_half + out[1::2] * delta_t/2.

        return z

```

```python
def plot_y_vs_x(x, y, axis_labels=None, label=None, title=None,
                color=None, linestyle=None, semilogy=False, loglog=False,
                ax=None):
    """
    Generic plotting function: return a figure axis with a plot of y vs. x,
    with line color and style, title, axis labels, and line label
    """
    if ax is None:        # if the axis object doesn't exist, make one
        ax = plt.gca()

    if (semilogy):
        line, = ax.semilogy(x, y, label=label,
                            color=color, linestyle=linestyle)
    elif (loglog):
        line, = ax.loglog(x, y, label=label,
                          color=color, linestyle=linestyle)
    else:
        line, = ax.plot(x, y, label=label,
                    color=color, linestyle=linestyle)

    if label is not None:    # if a label if passed, show the legend
        ax.legend()
    if title is not None:    # set a title if one if passed
        ax.set_title(title)
    if axis_labels is not None:  # set x-axis and y-axis labels if passed
        ax.set_xlabel(axis_labels[0])
        ax.set_ylabel(axis_labels[1])

    return ax, line
```

```python
def start_stop_indices(t_pts, plot_start, plot_stop):
    start_index = (np.fabs(t_pts-plot_start)).argmin()  # index in t_pts array
    stop_index = (np.fabs(t_pts-plot_stop)).argmin()  # index in t_pts array
    return start_index, stop_index
```

```python
orbit_label = (r'$x$', r'$y$')

t_start = 0.
t_end = 20.
delta_t = 0.001

t_pts = np.arange(t_start, t_end+ delta_t, delta_t)

```

```python
t_start = 0.
t_end = 20.
delta_t = 0.001

G = 1.
m1 = 1.
m2 = 9.

#Looked up the difference between sun & earth mass

o1 = GravitationalOrbits(m1, m2, G)

x_1_0, x_dot_1_0 = 1., -1.
y_1_0, y_dot_1_0 = 1., 1.
x_2_0, x_dot_2_0 = -(m1/m2) * x_1_0, -(m1/m2) * x_dot_1_0
y_2_0, y_dot_2_0 = -(m1/m2) * y_1_0, -(m1/m2) * y_dot_1_0

z_0 = [x_1_0, x_dot_1_0, y_1_0, y_dot_1_0, x_2_0, x_dot_2_0, y_2_0, y_dot_2_0]

x_1, x_dot_1, y_1, y_dot_1, x_2, x_dot_2, y_2, y_dot_2 = o1.solve_ode(t_pts, z_0)

fig = plt.figure(figsize=(5, 5))

ax = fig.add_subplot(1, 1, 1)

start, stop = start_stop_indices(t_pts, t_start, t_end)
ax.plot(x_1, y_1, color = 'blue', label = r'$m1$')
ax.plot(x_2, y_2, color = 'red', label = r'$m2$')
ax.set_title('Simple Gravitational Orbit')
ax.legend()
#ax.set_xlim(-np.max(x_2), np.max(x_2))
ax.set_aspect(1)

fig.tight_layout()
fig.savefig('Simple_orbits.png', bbox_inches = 'tight')

```

m2 is a larger mass than m1.
m2 represents the Sun while m1 represents the Earth

The Sun orbits in a little circle while the Earth is seen to be orbiting completely around the Sun

```python
t_start = 0.
t_end = 20.
delta_t = 0.001

G = 20.
m1 = 20.
m2 = 1.

#Looked up the difference between sun & earth mass

o1 = GravitationalOrbits(m1, m2, G)

x_1_0, x_dot_1_0 = 0.1, 0.
y_1_0, y_dot_1_0 = 0., .75
x_2_0, x_dot_2_0 = -(m1/m2) * x_1_0, -(m1/m2) * x_dot_1_0
y_2_0, y_dot_2_0 = -(m1/m2) * y_1_0, -(m1/m2) * y_dot_1_0

z_0 = [x_1_0, x_dot_1_0, y_1_0, y_dot_1_0, x_2_0, x_dot_2_0, y_2_0, y_dot_2_0]

x_1, x_dot_1, y_1, y_dot_1, x_2, x_dot_2, y_2, y_dot_2 = o1.solve_ode_Leapfrog(t_pts, z_0)

fig = plt.figure(figsize=(10, 10))

ax = fig.add_subplot(1, 1, 1)

start, stop = start_stop_indices(t_pts, t_start, t_end)
ax.plot(x_1, y_1, color = 'blue', label = r'$m1$')
ax.plot(x_2, y_2, color = 'red', label = r'$m2$')
ax.set_title('Simple Gravitational Orbit')
ax.legend()
#ax.set_xlim(-np.max(x_2), np.max(x_2))
ax.set_aspect(1)

fig.tight_layout()
fig.savefig('Simple_orbits.png', bbox_inches = 'tight')

#The Leapfrog axes are off, and I do not know how to fix it

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```


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

## 1. Método de Newmark

Em 1959, N.M. Newmark desenvolveu uma família de métodos temporáis baseados nas seguintes equações:

\begin{align}
\dot{u}_{i+1} &= \dot{u}_i + [(1-\gamma)\Delta t]\ddot{u}_i + (\gamma \Delta t)\ddot{u}_{i+1}\\
u_{i+1} &= u_i +\Delta t\dot{u}_i + [(0.5-\beta)\Delta t^2]\ddot{u}_i + (\beta \Delta t^2)\ddot{u}_{i+1}
\end{align}

Os parâmetros $\beta$ e $\gamma$ definem a variação de aceleração em cada passo temporal e determinam a estabilidade e precisão do método. Para essa análise, será utilizado o método da aceleração média constante, em que  $\beta$ e $\gamma$ recebem, respectivamente, 0.25 e 0.5. O resumo do método da aceleração média pode ser visualizado na figura abaixo.

No caso de sistemas lineares, a equação de equilíbrio dinâmico pode ser escrita por:
\begin{align}
m \ddot{u}_{i+1} + c\dot{u}_{i+1} + ku+{i+1} = p_{i+1}
\end{align}

A partir da equação 2, pode-se escrever a aceleração em função do deslocamento.
\begin{align}
\ddot{u}_{i+1} =\frac{1}{\beta\Delta t^2}\left(u_{i+1}-u_i\right) - \frac{1}{\beta\Delta t}\dot{u}_i +\left(\frac{1}{2\beta}-1\right)\ddot{u}_i
\end{align}

E a partir da equação 1, escreve-se a velocidade:
\begin{align}
\dot{u}_{i+1} =\frac{1}{\beta\Delta t}\left(u_{i+1}-u_i\right) + \left(1-\frac{\gamma}{\beta}\right)\dot{u}_i + \Delta t \left(1-\frac{\gamma}{2\beta}\right)\ddot{u}_i
\end{align}

Substituindo estes valores na equação de equilíbrio dinâmico, é possível a determinação de um sistema estático equivalente, escrito por:

\begin{align}
\hat{k} u_{i+1} = \hat{p}_{i+1}
\end{align}

em que:
\begin{align}
\hat{k}_{i+1} &= k + a_1\\
\hat{p}_{i+1} &= p_{i+1} + a_1u_i + a_2\dot{u}_i + a_3\ddot{u}_i
\end{align}

E as constantes a1,a2 e a3 são definidas como:
\begin{align}
a_1 &= \frac{1}{\beta \Delta t^2}m + \frac{\gamma}{\beta \Delta t}c\\
a_2 &= \frac{1}{\beta\Delta t}m + \left(\frac{\gamma}{\beta}-1 \right)c\\
a_3 &= \left(\frac{1}{2\beta} -1 \right) m + \Delta t \left( \frac{\gamma}{2\beta-1} \right)c
\end{align}

## 2. Método de Newton-Raphson
Deixando de lado os termos relativos a inércia e ao amortecimento, teremos a seguinte equação não-linear para resolver:
\begin{align}
f_s(u) = p
\end{align}

Supondo que após $j$ ciclos de iteração, $u^{(j)}$ é uma estimativa do deslocamento que se quer descobrir e que o objetivo do processo é a determinação de um processo iterativo que possa produzir uma estimativa mais aproximada de $u^{(j+1)}$. Para isso, expande-se a força restitutiva a partir de uma série de Taylor.
\begin{align}
f_s^{(j+1)} = f_s^{(j)} + \frac{\partial f_s}{\partial u} \left( u^{(j+1)}-u^{(j)} \right) + \frac{1}{2}\frac{\partial^2 f_s}{\partial^2 u} \left( u^{(j+1)}-u^{(j)} \right)^2 +...
\end{align}

Tendo em vista que a variação de deslocamento tende a ser pequena para que o sistema possa convergir, utiliza-se a forma linearizada da equação acima.

\begin{align}
f_s^{(j+1)} &= f_s^{(j)} + \frac{\partial f_s}{\partial u} \left( u^{(j+1)}-u^{(j)} \right) = f_s^{(j)} + k^{(j)}_t \Delta u^{(j)} = p\\
k^{(j)}_t \Delta u^{(j)} &= p - f_s^{(j)} = R^{(j)}
\end{align}

Em que R é a função resíduo a qual deve ser minimizada e $k_t$ é a rigidez tangente. Resolvendo a equação linearizada, pode-se calcular o novo deslocamento.

\begin{align}
u^{(j+2)} = u^{(j+1)}\Delta u^{(j)}
\end{align}

O processo deve ser repetido até que o critério de convergência seja atingindo. Este critério pode ser atendido com base na função residual, na variação de deslocamento ou do trabalho externo aplicado na iteração. O processo adotado neste notebook consiste em minimizar a função resíduo até que esta seja menor que o erro estabelecido, como mostra a figura abaixo.

##  3. Algorítmo de Newmark com Newton-Raphson

### 1.0 Cálculos iniciais

#### 1.1 Determinação dos valores iniciais de $f_s$ e $k_{t0}$

#### 1.2 Determinar a aceleração inicial
\begin{align*}
\ddot{u_0} = \frac{p_0-c\dot{u_0} - f_{s0}}{m}
\end{align*}
#### 1.3 Determinar o passo temporal

#### 1.4 Determinação das constantes com base nas características físicas do sistema:
\begin{align*}
a_1 &= \frac{1}{\beta \Delta t^2}m + \frac{\gamma}{\beta \Delta t}c\\
a_2 &= \frac{1}{\beta\Delta t}m + \left(\frac{\gamma}{\beta}-1 \right)c\\
a_3 &= \left(\frac{1}{2\beta} -1 \right) m + \Delta t \left( \frac{\gamma}{2\beta-1} \right)c\\
\end{align*}
### 2.0 Início do cálculo no domínio do tempo

#### 2.1 Atualizar variáveis no início do loop.
\begin{align*}
u_{i+1} &= u_i\\
fs_{i+1} &= fs_i\\
kt_{i+1} &= kt_i\\
\end{align*}
#### 2.2 Calculo da força equivalente
\begin{align*}
 \hat{p}_{i+1} = p_{i+1} + a_1u_i +a_2\dot{u}_i + a_3\ddot{u}_i.
\end{align*}
### 3.0 Início da iteração de Newton-Raphson:

#### 3.1  Determinação do resíduo.
\begin{align*}
 \hat{R}_{i+1} = \hat{p}_{i+1} - fs_{i+1} - a_1u{i+1}
\end{align*}
#### 3.2 Verificar a convergência de acordo com o erro estipulado

#### 3.3 Calcular a Matriz de rigidedz tangente equivalente
\begin{align*}
 \hat{k}t_{i+1} = {kt}_{i+1} + a_1
\end{align*}
#### 3.4 Resolver o sistema de equações para o cálculo da variação de deslocamento
\begin{align*}
 \Delta u =  \frac{\hat{R}_{i+1}}{\hat{k}t_{i+1}}
\end{align*}
#### 3.5 Cálcular o novo vetor deslocamento
\begin{align*}
 u_{i+1} = u_{i+1} + \Delta u
\end{align*}
#### 3.6 Determinação do estado com base no novo deslocamento.

### 4.0 Cálculo da velocidade e da aceleração

#### 4.1 Cálculo da velocidade no instante $i+1$
\begin{align*}
 \dot{u}_{i+1} = \frac{\gamma}{\beta \Delta t}(u_{i+1}-u_i) + \left( 1 - \frac{\gamma}{\beta} \right) \dot{u}_i + \Delta t \left(1-\frac{\gamma}{2\beta}\right)\ddot{u}_i.
\end{align*}
#### 4.2 Cálculo da aceleração no instante $i+1$
\begin{align*}
 \ddot{u}_{i+1} = \frac{\gamma}{\beta \Delta t^2}(u_{i+1}-u_i) - \frac{1}{\beta\Delta t}\dot{u}_i - \left( \frac{1}{2\beta}-1 \right) \ddot{u}_i.
\end{align*}
#### Repetem-se os itens 2 a 4.

A diferença entre o método linear e o não-linear consiste na aplicação do método de Newton-Raphson para realizar a convergência do sistema estático equivalente.

## 2. Validação do método para um sistema com 1gdl

Para a validação dos códigos utilizados, resolve-se o Exemplo 5.5 De Chopra(2011). O modelo constitutivo utilizado pelo autor está exposto abaixo e os dados para a solução do problema são: m = 0.2533, c = 0.1592 e $\Delta t = 0.1$.

O carregamento externo aplicado ao sistema é exposto na célula abaixo.

```python
t = np.linspace(0,1,11)
p = 10*np.sin(np.pi*t/0.6)
p[6:11] = 0
plt.figure(1,figsize=(6,6))
plt.plot(t,p,'k')
plt.title('carregamento aplicado ao sistema')
plt.xlabel('tempo (s)')
plt.ylabel('carregamento (kips)')
plt.grid(True)
```

O modelo constitutivo empregado no problema será descrito na função 'Func'. Na solução do problema, o autor desconsidera o processo de descarga, portanto, será definida uma função simplificada.

```python
def func(u):
    umax = 0.75
    fsmax = 7.5

    if u <= umax:
        kt = 10
        fs = 10*u
    else:
        kt = 0
        fs = fsmax

    return kt,fs

```

Agora, pode-se definir a função responsável pela solução da equação de equilíbrio dinâmico.

```python
def NLNewmark(k,m,c,p,u0,v0,t,e):

# 0.0 iniciando os parâmetros

    nt = int(len(t))
    a  = np.zeros(nt)
    v  = np.zeros(nt)
    u  = np.zeros(nt)
    fs = np.zeros(nt)
    kt = np.zeros(nt)
    kht = np.zeros(nt)
    ph = np.zeros(nt)
    R = np.zeros(nt)

#1.0 Determinação dos parâmetros iniciais:
    b = 0.25
    y = 0.50

#1.1 passo temporal

    dt = t[1] - t[0]

#1.2 variáveis de estado
    kt[0] = k
    fs[0] = k * u[0]

#1.3 cálculo da aceleração inicial:

    a[0] = (p[0] - c*v[0] - fs[0])/m

# 1.4 Determinação das consntantes:

    a1 = 1/(b*dt**2)*m + y/(b*dt)*c

    a2 = 1/(b*dt)*m + (y/b -1)*c

    a3 = (1/(2*b)-1)*m + dt*(y/(2*b)-1)*c

#2.0 início da iteração temporal
    for i in range(nt-1):
# 2.1 iniciação dos valores

            u[i+1] = u[i]
            fs[i+1] = fs[i]
            kt[i+1] = kt[i]

# 2.2 Cálculo da força equivalente
            ph[i+1] = p[i+1] + a1*u[i] + a2*v[i] + a3*a[i]

# 3.0 início da iteração de Newton-Raphson

            k = 0
            while k<1:

                R[i+1] = ph[i+1] - fs[i+1] -a1*u[i+1]

                if abs(R[i+1]) <= e:
                    k = 1
                else:

                    kht[i+1] = kt[i+1] + a1

                    du = R[i+1]/kht[i+1]

                    u[i+1] += du

                    kt[i+1],fs[i+1] = func(u[i+1])

# 4.0 Determinação da velocidade e da aceleração

            v[i+1] = y/(b*dt)*(u[i+1] - u[i]) + (1-y/b)*v[i] + dt*(1-y/(2*b))*a[i]

            a[i+1] = 1/(b*dt**2)*(u[i+1] - u[i]) - 1/(b*dt)*v[i] - (1/(2*b) - 1)*a[i]

    return u,v,a,kt,fs,R

```

```python
# Dados de entrada
k = 10
m = 0.2533
c = 0.1592
t = np.linspace(0,1,11)
u0 = 0
v0 = 0
e = 0.01

# Resolução da equação de equilíbrio

u,v,a,kt,fs,R = NLNewmark(k,m,c,p,u0,v0,t,e)

#Plotagem dos resultados

data = {'p_i':p[0:7],'R_i':R[0:7],'Kt':kt[0:7],'u':u[0:7],'fs':fs[0:7],'v':v[0:7],'a':a[0:7]}
Plan = pd.DataFrame( data = data,
                   index = t[0:7])
Plan
```

      p_i
      R_i
      Kt
      u
      fs
      v
      a

      0.0
      0.000000
      0.000000e+00
      10.0
      0.000000
      0.000000
      0.000000
      0.000000

      0.1
      5.000000
      0.000000e+00
      10.0
      0.043667
      0.436666
      0.873332
      17.466639

      0.2
      8.660254
      0.000000e+00
      10.0
      0.232617
      2.326165
      2.905666
      23.180051

      0.3
      10.000000
      -7.105427e-15
      10.0
      0.612063
      6.120630
      4.683262
      12.371871

      0.4
      8.660254
      0.000000e+00
      0.0
      1.114345
      7.500000
      5.362370
      1.210283

      0.5
      5.000000
      -2.842171e-14
      0.0
      1.621424
      7.500000
      4.779211
      -12.873472

      0.6
      0.000000
      0.000000e+00
      0.0
      1.989093
      7.500000
      2.574185
      -31.227044

Comparando estes valores aos encontrados no livro, percebe-se que o algorítmo está funcionando correntamente.

## 2. Validação do método para um sistema com Múltiplos gdl

Para a validação dos códigos utilizados, resolve-se o Exemplo16.4 de Chopra(2011). O modelo constitutivo utilizado pelo autor está exposto abaixo, junto do Shear Building considerado e suas respectivas matrizes. Os dados utilizados no problema são: m = 0.2591, k = 100, $\alpha$ = 0.05, $\ddot{u}_{g0}$ = 193.05

A função para a resolução da equação de equilíbrio dinâmico é exposta abaixo.

```python
def NLNewmarkM(k,m,c,p,u0,v0,t,e):

# 0.0 iniciando os parâmetros

    nt  = int(len(t))
    n   = int(len(p[:,0]))
    a   = np.zeros((n,nt))
    v   = np.zeros((n,nt))
    u   = np.zeros((n,nt))
    fs  = np.zeros((n,nt))
    kt  = np.zeros((nt,n,n))
    kht = np.zeros((nt,n,n))
    ph  = np.zeros((n,nt))
    R   = np.zeros((n,nt))

#1.0 Determinação dos parâmetros iniciais:
    b = 0.25
    y = 0.50

#1.1 passo temporal

    dt = t[1] - t[0]

#1.2 variáveis de estado
    kt[0] = k
    fs[:,0] = np.dot(kt[0],u[:,0])

#1.3 cálculo da aceleração inicial:

    a[:,0] = np.linalg.inv(m)@(p[:,0] - c@v[:,0] - fs[:,0])

# 1.4 Determinação das consntantes:

    a1 = 1/(b*dt**2)*m + y/(b*dt)*c

    a2 = 1/(b*dt)*m + (y/b -1)*c

    a3 = (1/(2*b)-1)*m + dt*(y/(2*b)-1)*c

#2.0 início da iteração temporal
    for i in range(nt-1):
# 2.1 iniciação dos valores

            u[:,i+1]  = u[:,i]
            fs[:,i+1] = fs[:,i]
            kt[i+1]   = kt[i]

# 2.2 Cálculo da força equivalente

            ph[:,i+1] = p[:,i+1] + a1@u[:,i] + a2@v[:,i] + a3@a[:,i]

# 3.0 início da iteração de Newton-Raphson
            j = 0

            while True:

                R[:,i+1] = ph[:,i+1] - fs[:,i+1] - a1@u[:,i+1]

                j+=1

                if j>5000:

                    print('erro: não converge na iteração',i)
                    break

                if np.linalg.norm(R[:,i+1]) <= e:

                    break
                    print('Numero de iterações:',j)
                else:

                    kht[i+1] = kt[i+1] + a1

                    du = np.linalg.inv(kht[i+1])@R[:,i+1]

                    u[:,i+1] += du

                    kt[i+1],fs[:,i+1] =func2(u[:,i+1])

                    #kt[i+1] = kt[i]
                    #fs[:,i+1] = np.dot(kt[i+1],u[:,i+1])

# 4.0 Determinação da velocidade e da aceleração

            v[:,i+1] = y/(b*dt)*(u[:,i+1] - u[:,i]) + (1-y/b)*v[:,i] + dt*(1-y/(2*b))*a[:,i]

            a[:,i+1] = 1/(b*dt**2)*(u[:,i+1] - u[:,i]) - 1/(b*dt)*v[:,i] - (1/(2*b) - 1)*a[:,i]

    return u.round(4),v,a,kt,fs,R

```

A definição do modelo constitutivo empregado no exemplo é feito pela função func2.

```python
def func2(U):
    sd = np.zeros(5)
    fd = np.zeros(5)
    Fd = np.zeros(5)
    k = np.zeros(5)

#1.0 Cálculo do Story drift
    sd[0] = U[0]
    for i in range(1,5):
        sd[i] = U[i] - U[i-1]
#2.0 Determinação do estado
    for i in range(5):

        if abs(sd[i])<=1.25:
            k[i] = 100
            fd[i] = k[i]*sd[i]
        else:

            k[i] = 5
            fd[i] = np.sign(sd[i])*(125 + k[i]*(abs(sd[i])-1.25))

#3.0 Montagem da Matriz de rigidez

    K = np.array([[k[0]+k[1],-1*k[1],0,0,0],
                 [-1*k[1],k[1]+k[2],-1*k[2],0,0],
                 [0,-1*k[2],k[2]+k[3],-1*k[3],0],
                 [0,0,-1*k[3],k[3]+k[4],-1*k[4]],
                 [0,0,0,-1*k[4],1*k[4]]])

#4.0 Montagem do vetor de forças
    Fd[4] = fd[4]

    for i in range(4):
        Fd[i] = fd[i]-fd[i+1]

    return K,Fd

```

```python
#1.0 Valores iniciais do problema
e = 0.00010
t = np.linspace(0,2,21)
k = 100
u0 = v0 = np.zeros(5)
K = k* np.array([[2,-1,0,0,0],
                 [-1,2,-1,0,0],
                 [0,-1,2,-1,0],
                 [0,0,-1,2,-1],
                 [0,0,0,-1,1]])

M = 0.2591* np.diag(np.ones(5))

C= 0.01* np.array([[69.01,-19.81,-3.395,-1.37,-0.873],
                 [-19.81,65.7,-21.18,-4.268,-2.243],
                 [-3.395,-21.18,64.83,-22.05,-5.638],
                 [-1.37,-4.268,-22.05,63.46,-25.45],
                 [-0.873,-2.243,-5.638,-25.45,43.65]])

#2.0 Calculo do vetor de forças

ug = np.zeros((1,21))
ug[0] =0.5*386.1*np.sin(2*np.pi*t)
ug[0,10:21] =np.zeros(11)
P = -M@np.ones((5,1))
P = np.dot(P.round(4),ug)

#2.1 Plotagem do vetor de forças
plt.figure(1,figsize =(5,5))
plt.plot(t,P[0],'k')
plt.title('Força externa aplicada ao sistema')
plt.xlabel('tempo (s)')
plt.ylabel('F (kips)))')
plt.grid(True)

# 3.0 Resolução do sistema pelo método de Newmark e Newton-Raphson

u,v,a,kt,fs,R = NLNewmarkM(K,M,C,P,u0,v0,t,e)

```

```python
#4.0 Visualização dos valores de deslocamento
data2 = {'t_i':t[0:7], 'P_i':P[0,0:7],'u_1':u[0,0:7],'u_2':u[1,0:7],
         'u_3':u[2,0:7],'u_4':u[3,0:7],'u_5':u[4,0:7]}
Plan2 = pd.DataFrame( data = data2,
                   index = np.linspace(0,6,7))
Plan2
```

      t_i
      P_i
      u_1
      u_2
      u_3
      u_4
      u_5

      0.0
      0.0
      -0.000000e+00
      0.0000
      0.0000
      0.0000
      0.0000
      0.0000

      1.0
      0.1
      -2.940058e+01
      -0.1709
      -0.2360
      -0.2614
      -0.2712
      -0.2747

      2.0
      0.2
      -4.757114e+01
      -0.7704
      -1.1766
      -1.3754
      -1.4667
      -1.5020

      3.0
      0.3
      -4.757114e+01
      -1.8817
      -2.8984
      -3.5526
      -3.9166
      -4.0749

      4.0
      0.4
      -2.940058e+01
      -3.5367
      -5.1288
      -6.2627
      -7.0497
      -7.4407

      5.0
      0.5
      -6.125592e-15
      -5.3871
      -7.5191
      -8.7166
      -9.6944
      -10.2428

      6.0
      0.6
      2.940058e+01
      -6.4492
      -9.0772
      -10.0545
      -10.7605
      -11.1581

Comparando estes valores aos encontrados no livro, percebe-se que o algorítmo está funcionando correntamente.

```python

```


# Prova I- Computação Centífica II
> Aluno: Gil Miranda

> DRE: 118037119

> E-mail: gil.neto@ufrj.br; gilsmneto@gmail.com

## Set-up of dependencies and libraries

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

```python
##### Vectorized 4th Order Runge Kutta
### Input:  F -> Differential equation;
###         y0 -> list or scalar for initial condition;
###         ts -> list of points on time to evaluate the equation;
###         p -> list or scalar for parameters for F, default is set to 0 if F has no extra parameters;
### Output: ys -> numpy array with all solutions for each step t, ys is a Matrix
##### Gil Miranda - last revision 26/10/2019
def rk_4(F, y0, ts, p = 0):
    ys = [y0]
    t = ts[0]
    h = ts[1] - ts[0]
    for tnext in ts:
        k1 = h*F(t, ys[-1], p)
        k2 = h*F(t + h/2, ys[-1] + k1/2, p)
        k3 = h*F(t + h/2, ys[-1] + k2/2, p)
        k4 = h*F(t + h, ys[-1] + k3)
        ynext = ys[-1] + (k1/6+k2/3+k3/3+k4/6)
        ys.append(ynext)
        t = tnext
    return ys[:-1]
```

---
## Creating the 4th and 3rd order Adams-Bashforth Method

The Adams-Bashforth method can be written as
$$
u_{k} = u_{k-1} + h\left(f_{k-1} + \frac{1}{2}\nabla f_{k-1}+ \frac{5}{12}\nabla^2 f_{k-1}+ \frac{9}{24}\nabla^3 f_{k-1} + \dots \right)
$$
And we can aproximate
$$
\begin{align}
\nabla f_{k-1} &= f_{k-1} - f_{k-2}\\
\nabla^2 f_{k-1} &= f_{k-1} - 2f_{k-2} + f_{k-3}\\
\nabla^3 f_{k-1} &= f_{k-1} - 3f_{k-2} + 3f_{k-3} - f_{k-4}\\
\end{align}
$$
So we have
$$
u_{k} = u_{k-1} + h\left(f_{k-1} + \frac{1}{2}(f_{k-1} - f_{k-2})+ \frac{5}{12}(f_{k-1} - 2f_{k-2} + f_{k-3}) + \frac{9}{24}(f_{k-1} - 3f_{k-2} + 3f_{k-3} - f_{k-4})\right)
$$
And collecting terms for the 3rd order and 4th order method, we can end with
$$
u_{k} = u_{k-1} + h\left(\frac{23}{12}f_{k-1} - \frac{16}{12}f_{k-2} + \frac{5}{12}f_{k-3}\right)
$$
for the 3rd order, and
$$
u_{k} = u_{k-1} + h\left(\frac{55}{24}f_{k-1} - \frac{59}{24}f_{k-2} + \frac{37}{24}f_{k-3} - \frac{9}{24}f_{k-4}\right)
$$
for the 4th order method

We have to know the $f_{k-1},\dots, f_{k-n}$ points to solve using AB nth order method , we can solve for this $n$ points using Runge-Kutta

```python
##### Adams-Bashfort 4th & 3rd order
### Input:  F -> Differential equation;
###         t0 -> initial point in time;
###         tf -> final point in time;
###         y0 -> initial condition;
###         h -> step size;
###         order -> order of the method;
### Output: ys -> list with all solutions for each step t;
###         ts -> list with all points in time;
##### Gil Miranda - last revision 29/10/2019
def adams_bash(F, t0, tf, y0, h, order = 3):
    if order == 4:
        ws = [55/24, -59/24, 37/24, -9/24] ## setting the weights for 4th order
    else:
        order = 3
        ws = [23/12, -16/12, 5/12] ## setting the weights for 3rd order
    ### initializing the list of points in time
    ts = [t0]
    for i in range(order-1):
        ts.append(ts[-1] + h)

    ### solving for the first n points with runge-kutta 4th order, so we can initiliaze AB method
    first_ys = rk_4(F, y0, ts)

    n = len(first_ys)
    ys = first_ys[:] ## list of solutions, initiliazed with rk4

    ### Adams-Bashfort
    while(ts[-1] <= tf):
        fs = [F(ts[-i], ys[-i]) for i in range(1,n+1)] ## list with f_k-1,...,f_k-n
        ynew = ys[-1]
        for (wi,fi) in zip(ws,fs):
            ynew += h*(wi*fi)
        ys.append(ynew)
        ts.append(ts[-1] + h)
    return ys, ts
```

#### The differential Equation

Here is the function that returns the differential equation
$$
\frac{\mathrm{d}u}{\mathrm{d}t} = 10e^{-\frac{(t-2)^2}{2(0.075)^2}} - 0.6u
$$

```python
def eq_1(t,u, p=0):
    a = -(t-2)**2/(2*(0.075)**2)
    b = 10*np.e**a
    c = 0.6*u
    return b-c
```

#### Solving the Differential Equation with Adams-Bashforth 4th and 3rd Order

```python
y0 = 0.5 # Initial Condition
hs = [0.01, 0.04, 0.06, 0.08] ## step size
hs_name = [hs[0], hs[0], hs[1], hs[1], hs[2], hs[2], hs[3], hs[3]] ## step size names for plotting
ys_3 = []
ts_3 = []
ys_4 = []
ts_4 = []
for h in hs:
    y_3, t_3 = adams_bash(eq_1, 0, 4, y0, h) ## solving order 3 for each step size
    y_4, t_4 = adams_bash(eq_1, 0, 4, y0, h, order = 4) ## solving order 4 for each step size
    ys_3.append(y_3)
    ts_3.append(t_3)
    ys_4.append(y_4)
    ts_4.append(t_4)
```

```python
## plotting time
fig, ((ax1,ax2), (ax3,ax4), (ax5, ax6), (ax7, ax8)) = plt.subplots(nrows=4, ncols=2, figsize=(14,18))
fig.suptitle('Numerical Solution with Adams-Bashforth', weight='bold')
names = ['3rd', '4th', '3rd', '4th', '3rd', '4th', '3rd', '4th']
axs = [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8]
for i,j,h in zip(axs,names,hs_name):
    i.set_title(j + ' Order AB Method - step = $' + str(h) + '$')

for a in axs:
    a.grid(alpha = 0.5)
ax1.plot(ts_3[0],ys_3[0], color = 'red')
ax2.plot(ts_4[0],ys_3[0], color = 'red')
ax3.plot(ts_3[1],ys_3[1], color = 'green')
ax4.plot(ts_4[1],ys_4[1], color = 'green')
ax5.plot(ts_3[2],ys_3[2], color = 'blue')
ax6.plot(ts_4[2],ys_4[2], color = 'blue')
ax7.plot(ts_3[3],ys_3[3], color = 'black')
ax8.plot(ts_4[3],ys_4[3], color = 'black')

plt.show()
```

#### Visualizing error between 3rd and 4th orders

```python
## calculating the local error
err = []
for j in range(3):
    e = [abs(ys_4[j][i] - abs(ys_3[j][i])) for i in range(len(ys_4[j]))]
    err.append(e)

## plotting time
fig, ((ax1), (ax2), (ax3)) = plt.subplots(nrows=1, ncols=3, figsize=(20,5))
fig.suptitle('Local error between 3rd and 4th order methods')
axs = [ax1, ax2, ax3]
for i,h in zip(axs,hs):
    i.set_title('step = $' + str(h) + '$')

for a in axs:
    a.grid(alpha = 0.5)
ax1.plot(ts_3[0],err[0], color = 'red')
ax2.plot(ts_4[1],err[1], color = 'green')
ax3.plot(ts_3[2],err[2], color = 'blue')

plt.show()
```

---
## Adaptative Adams-Bashforth Method

```python
##### Adaptative Adams-Bashfort 4th & 3rd order
### Input:  F -> Differential equation;
###         t0 -> initial point in time;
###         tf -> final point in time;
###         y0 -> initial condition;
###         h -> step size;
###         tol -> error tolerance;
### Output: ys -> list with all solutions for each step t;
###         ts -> list with all points in time;
##### Gil Miranda - last revision 30/10/2019
def adams_bash_adp(F, t0, tf, y0, h, tol = 1e-3):

    ws_4 = [55/24, -59/24, 37/24, -9/24] ## setting the weights for 4th order
    ws_3 = [23/12, -16/12, 5/12] ## setting the weights for 3rd order

    ### initializing the list of points in time
    ts = [t0]
    for i in range(4):
        ts.append(ts[-1] + h)

    ### solving for the first n points with runge-kutta 4th order, so we can initiliaze AB method
    first_ys = rk_4(F, y0, ts)

    ys = first_ys[:] ## list of solutions, initiliazed with rk4

    ### Adptative Adams-Bashfort
    while(ts[-1] <= tf):
        fs = [F(ts[-i], ys[-i]) for i in range(1,5)] ## list with f_k-1,...,f_k-n

        ## Solving with 3rd order
        ynew = ys[-1]
        for (wi,i) in zip(ws_3, range(3)):
            ynew += h*(wi* fs[i])

        ## Solving with 4th order (using 3rd order points)
        ynew_til = ys[-1]
        for (wi, i) in zip(ws_4, range(4)):
            ynew_til += h*(wi * fs[i])

        ## Local error
        err = abs(ynew_til - ynew)

        ## if err < tol we accept the solution
        if err <= tol:
            ys.append(ynew)
            tnext = ts[-1] + h
            ts.append(tnext)
            h = h*(tol/err)**(1/4)
        else:
            ## else, we get a new step
            h = h/2

    return ys, ts
```

### Visualizing Solutions

```python
t0 = 0
tf = 4
h = 0.1
y0 = 0.5
```

```python
ys_1, ts_1 = adams_bash_adp(eq_1, t0, tf, y0, h, tol = 1e-3)
ys_2, ts_2 = adams_bash_adp(eq_1, t0, tf, y0, h, tol = 1e-4)
```

```python
## plotting time
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(14,5))
fig.suptitle('Numerical Solution with Adaptative Adams-Bashforth, initial step size = $' + str(h) + '$', weight='bold')
# names = ['3rd', '4th', '3rd', '4th', '3rd', '4th']
axs = [ax1, ax2]
for a in axs:
    a.grid(alpha = 0.5)

ax1.plot(ts_1,ys_1, color = 'black')
ax1.plot(ts_1,ys_1, 'o', color = 'red')
ax1.set_title('tol = $10^{-3}$')

ax2.plot(ts_2,ys_2, color = 'black')
ax2.plot(ts_2,ys_2, 'o', color = 'blue')
ax2.set_title('tol = $10^{-4}$')

plt.show()
```

---
### Bonus Method: Predictor-Corrector Adams-Bashforth-Moulton 4th order Method

Predictor equation (4th order AB)
$$
u_{k} = u_{k-1} + h\left(\frac{55}{24}f_{k-1} - \frac{59}{24}f_{k-2} + \frac{37}{24}f_{k-3} - \frac{9}{24}f_{k-4}\right)
$$
Corrector equation (4th order AM)
$$
u_{k} = u_{k-1} + h\left(\frac{9}{24}f_{k} + \frac{19}{24}f_{k-1} - \frac{5}{24}f_{k-2} + \frac{1}{24}f_{k-3}\right)
$$

We initialize the AB with RK4, and then uses AB $f_k$ to initialize the AM, and then compute the error between the steps, if $err > tol$, tol is given tolerance, calculate again

```python
##### Adams-Bashfort-Moulton 4th order
### Input:  F -> Differential equation;
###         t0 -> initial point in time;
###         tf -> final point in time;
###         y0 -> initial condition;
###         h -> step size;
###         tol -> error tolerance;
### Output: ys -> list with all solutions for each step t;
###         ts -> list with all points in time;
##### Gil Miranda - last revision 30/10/2019
def adams_bash_mou(F, t0, tf, y0, h, tol = 1e-12):
    ws_ab = [55/24, -59/24, 37/24, -9/24] ## setting the weights for adams bashforth
    ws_am = [9/24, 19/24, -5/24, 1/24] ## setting the weights for adams moulton

    ### initializing the list of points in time
    ts = [t0]
    for i in range(3):
        ts.append(ts[-1] + h)

    ### solving for the first n points with runge-kutta 4th order, so we can initiliaze AB method
    first_ys = rk_4(F, y0, ts)

    n = len(first_ys)
    ys = first_ys[:] ## list of solutions, initiliazed with rk4

    while(ts[-1] <= tf):
        ## Calculating the AB solution
        fs_ab = [F(ts[-i], ys[-i]) for i in range(1,n+1)] ## list with f_k-1,...,f_k-n
        y_ab = ys[-1]
        for (wi,fi) in zip(ws_ab,fs_ab):
            y_ab += h*(wi*fi)

        ## Generating next point in time
        tnext = ts[-1] + h

        ## Generating the f(ti,yi) point to use with AM
        f1_am = F(tnext, y_ab)
        fs_am = [f1_am] + fs_ab[:-1]

        ## Calculating the AM solution
        y_am = ys[-1]
        for (wi,fi) in zip(ws_am,fs_am):
            y_am += h*(wi*fi)

        ## error
        err = abs(y_am - y_ab)
        y_old = y_am
        y_new = y_am

        while(err > tol):
            ## Calculating another solution with AM using last AM solution
            f1_new = F(tnext, y_old)
            fs_new = [f1_new] + fs_ab[:-1]
            y_new = ys[-1]
            for (wi,fi) in zip(ws_am,fs_new):
                y_new += h*(wi*fi)
            err = abs(y_new - y_old)
            y_old = y_new
        ys.append(y_new)
        ts.append(tnext)
    return ys, ts
```

```python
y0 = 0.5 # Initial Condition
hs = [0.01, 0.04, 0.06, 0.08] ## step size
hs_name = [hs[0], hs[1], hs[2], hs[3]] ## step size names for plotting
ys_5 = []
ts_5 = []
for h in hs:
    y_5, t_5 = adams_bash_mou(eq_1, 0, 4, y0, h) ## solving for each step with predictor-corrector
    ys_5.append(y_5)
    ts_5.append(t_5)
```

```python
## plotting time
fig, ((ax1,ax2), (ax3,ax4)) = plt.subplots(nrows=2, ncols=2, figsize=(14,10))
fig.suptitle('Numerical Solution with Adams-Bashforth-Moulton', weight='bold')
axs = [ax1, ax2, ax3, ax4]

for i,h in zip(axs,hs_name):
    i.set_title('h = $' + str(h) + '$')

for a in axs:
    a.grid(alpha = 0.5)
ax1.plot(ts_5[0],ys_5[0], color = 'red')
ax2.plot(ts_5[1],ys_5[1], color = 'blue')
ax3.plot(ts_5[2],ys_5[2], color = 'green')
ax4.plot(ts_5[3],ys_5[3], color = 'orange')

plt.show()
```

Unlike the AB 3rd and 4th order methods, we can get a decent solution even with step size $h = 0.08$ with the ABM method

Plotting the solutions for each step-size with AB4, AB Adaptative, ABM

```python
## plotting time
fig, ((ax1,ax2), (ax3,ax4)) = plt.subplots(nrows=2, ncols=2, figsize=(14,10))
fig.suptitle('Comparison of solutions for all methods', weight='bold')
axs = [ax1, ax2, ax3, ax4]

for i,h in zip(axs,hs_name):
    i.set_title('h = $' + str(h) + '$')

for a in axs:
    a.grid(alpha = 0.5)
ax1.plot(ts_5[0],ys_5[0], color = 'red', label = 'ABM')
ax1.plot(ts_4[0], ys_4[0], label = 'AB 4')
ax1.plot(ts_2, ys_2, label = 'AB Adp')

ax2.plot(ts_5[1],ys_5[1], color = 'blue', label = 'ABM')
ax2.plot(ts_4[1], ys_4[1], color = 'red', label = 'AB 4')

ax3.plot(ts_4[2], ys_4[2], color = 'orange', label = 'AB 4')
ax3.plot(ts_5[2],ys_5[2], color = 'green', label = 'ABM')

ax4.plot(ts_5[3],ys_5[3], color = 'orange', label = 'ABM')
ax4.plot(ts_4[3], ys_4[3], color = 'green', label = 'AB 4')

ax1.legend()
ax2.legend()
ax3.legend()
ax4.legend()
plt.show()
```