# Measurements for single-qubit systems

This tutorial introduces you to measurements done on single-qubit systems. The concept of a measurement is a central part of quantum mechanics, as well as quantum algorithms. Single-qubit measurements, as their name implies are measurements on single qubits. The outcomes of a measurement in quantum mechanics are probabilistic, and in general, change the state of the qubit depending on the outcome of the measurement.

We recommend to go through the [tutorial that introduces single qubit gates](../SingleQubitGates/SingleQubitGates.ipynb) before starting this one.

This tutorial covers the following topics:

* Computational basis measurements
* Pauli basis measurements
* Measurements in arbitrary orthogonal bases
* Representing measurements as projector operators
$\newcommand{\ket}[1]{\left|#1\right>}$
$\newcommand{\bra}[1]{\left<#1\right|}$

# Computational basis measurements
In this section, we will discuss the simplest type of qubit measurements - measurements in the computational basis. (This is the "default" type of measurements - unless otherwise specified, "measurement" refers to this type.)

The state $\ket{\psi}$ of a single qubit can always be expressed in [Dirac notation](../Qubit/Qubit.ipynb#Dirac-Notation) as
$$\ket{\psi} = \alpha \ket{0} + \beta \ket{1},$$
where $\alpha$ and $\beta$ are complex numbers, and the state is normalized, i.e., $|\alpha|^2 + |\beta|^2 = 1$.

We can examine the qubit to get some information about its state - *measure* its state. Similar to the classical case of examining a bit, the outcome of a measurement can be $0$ or $1$. But, unlike the classical case, quantum measurement is a probabilistic process.
The probabilities of the measurement outcomes being $0$ and $1$ are $|\alpha|^2$ and $|\beta|^2$, respectively. Additionally, the state of the qubit is modified by the measurement: if the outcome of the measurement is $0$, then the post-measurement state of the qubit is $\ket{0}$, and if the outcome is $1$, the state is $\ket{1}$. In quantum mechanics, this is referred to as the [collapse of the wave function](https://en.wikipedia.org/wiki/Wave_function_collapse).

Computational basis measurement outcomes and their probabilities are summarized in the table below:

    <col width=150>
    <col width=150>
    <col width=150>

        Measurement outcome
        Probability of outcome
        State after measurement

        $0$
        $|\alpha|^2$
        $\ket 0$

        $1$
        $|\beta|^2$
        $\ket 1$

> Unlike quantum gates which are unitary and reversible operations, measurements are neither unitary nor reversible. Since the outcomes of a measurement are probabilistic, any two isolated qubits which are initially prepared in identical superposition states are in general not guaranteed to have the same measurement outcomes after each qubit has been measured separately. As we will see below, measurements are modeled by projection operators instead of unitary operators.
>
> Additionally, the assumption of the wave function being **normalized** is important, since the probability outcomes must sum up to $1$. If the wave function is not normalized, it is important to normalize it first in order to obtain the correct measurement probabilities.

### Exercise 1: The probability outcomes for a specific state

The qubit is in the following state:
$$\ket \psi = 0.6 \ket 0 + 0.8 \ket 1 \equiv \begin{bmatrix} 0.6 \\ 0.8\end{bmatrix}.$$

If this qubit is measured in the computational basis, what are the outcome probabilities?

*Can't come up with a solution? See the explained solution in the [Single Qubit System Measurements Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-1:-The-probability-outcomes-for-a-specific-state).*

### Demo: Implementing measurement in Q# using the M operation

In this demo, we prepare a qubit in the state we've seen in Exercise 1, and then measure it in the computational basis. In Q#, single-qubit measurements in the computational basis can be implemented using the [M operation](https://docs.microsoft.com/en-us/qsharp/api/qsharp/microsoft.quantum.intrinsic.m). It will return the constant `Zero` if measurement result was $0$ or the constant `One` if the measurement result was $1$. `Zero` and `One` are constants of type `Result`.

```qsharp
// Run this cell using Ctrl+Enter (⌘+Enter on Mac)
// Run the next cell to see the output

open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;

operation SimpleMeasurementDemo () : Unit {
    using (q = Qubit()) {
        // Prepare the qubit in the superposition state
        // |𝜓❭ = 0.6 |0❭ + 0.8 |1❭
        Ry(2.0 * ArcTan2(0.8, 0.6), q);

        Message("Qubit in state |𝜓❭:");
        DumpMachine();

        Message("Measuring the qubit...");
        let outcome = (M(q) == One ? 1 | 0);

        Message($"The measurement outcome is {outcome}.");
        Message("Post-measurement state of the qubit:");
        DumpMachine();
    }
}
```

```qsharp
%simulate SimpleMeasurementDemo
```

> If you run the cell above multiple times, you will notice that whenever the measurement outcome is $1$, the post-measurement state of the qubit is $\ket 1$, and similarly for $0$. This is in line with our expectation that after the measurement the wave function 'collapses' to the corresponding state.

### Demo: Measurement statistics

The following cell contains code demonstrating that the theoretical and experimental values of the probability outcomes indeed match with each other. We repeatedly prepare the same state $\ket \psi = 0.6 \ket 0 + 0.8 \ket 1$ and measure it in the computational basis $100$ times. At the end, we expect $0$ to be measured approximately $36$ times, and $1$ to be measured approximately $64$ times. Note that since measurements are probabilistic, we do not expect the results to match these values exactly.

```qsharp
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;

operation MeasumentStatisticsDemo () : Unit {
    mutable countZero = 0;
    let numRuns = 100;
    using (q = Qubit()) {
        for (i in 1 .. numRuns) {
            // Prepare the qubit in the superposition state
            // |𝜓❭ = 0.6 |0❭ + 0.8 |1❭
            Ry(2.0 * ArcTan2(0.8, 0.6), q);

            // Measure in the computational basis, and update the counts according to the outcomes
            if (M(q) == Zero) {
                set countZero += 1;
            }
            // Reset the qubit for use in the next iteration
            Reset(q);
        }
    }
    let countOne = numRuns - countZero;

    Message($"Simulated probability of measuring 0 is 0.{countZero}.");
    Message($"Theoretical probability of measuring 0 is 0.36.");

    Message($"Simulated probability of measuring 1 is 0.{countOne}.");
    Message($"Theoretical probability of measuring 0 is 0.64.");
}
```

```qsharp
%simulate MeasumentStatisticsDemo
```

Measurements can be used to distinguish orthogonal states. We start with an exercise for distinguishing between the computational basis states, and discuss the general case of arbitrary basis measurements later in the tutorial.

### Exercise 2: Distinguish $|0\rangle$ and $|1\rangle$

**Input:** A qubit which is guaranteed to be in either the $|0\rangle$ or the $|1\rangle$ state.

**Output:**  `true` if the qubit was in the $|0\rangle$ state, or `false` if it was in the $|1\rangle$ state. The state of the qubit at the end of the operation does not matter.

```qsharp
%kata T2_IsQubitZero

operation IsQubitZero (q : Qubit) : Bool {
    // The operation M will measure a qubit in the computational basis (|0⟩ and |1⟩ basis)
    // and return Zero if the observed state was |0⟩ or One if the state was |1⟩.
    // Measuring a basis state will yield the result matching that state deterministically.
    // To answer the question, you need to perform the measurement and check whether the result
    // equals One - either directly or using library function IsResultOne.
    //
    // Type the following: return M(q) == Zero;
    // Then run the cell using Ctrl+Enter (⌘+Enter on macOS).

    // ...
}
```

*Can't come up with a solution? See the explained solution in the [Single Qubit System Measurements Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-2:-Distinguish-$|0\rangle$-and-$|1\rangle$).*

## Measurements in the Pauli bases
So far, we have discussed measurements done in the computational basis, i.e., the $\{ \ket 0, \ket 1\}$ basis.

It is also possible to implement measurements in other orthogonal bases, such as the [Pauli X basis](../SingleQubitGates/SingleQubitGates.ipynb#Pauli-Gates), which consists of the two vectors $\ket + = \frac1{\sqrt2} \big(\ket 0 +\ket 1\big)$, and $\ket - = \frac1{\sqrt2} \big(\ket 0 -\ket 1\big)$. Q# has a built-in operation [Measure](https://docs.microsoft.com/en-us/qsharp/api/qsharp/microsoft.quantum.intrinsic.measure) for measurements in the Pauli bases.

> The `Measure` operation can be used for measuring multiple qubits in a multi-qubit system; however, in this tutorial we only consider measurements for single-qubit systems.

The eigenvalues of a Pauli matrix are $\pm 1$, with one eigenvector corresponding to each eigenvalue. For any chosen Pauli basis, the `Measure` operation returns `Zero` if the measurement outcome corresponds to the eigenvalue $+1$, and returns `One` if the measurement outcome corresponds to the eigenvalue $-1$. As in the case of the computational basis measurements, the wave function of the qubit collapses to the corresponding state after the measurement is executed.

The probabilities of the outcomes are defined using a similar rule: to measure a state $\ket \psi$ in a Pauli basis $\{ \ket {b_0}, \ket {b_1}\}$, we represent it as a linear combination of the basis vectors
$$\ket \psi = c_0 \ket {b_0} + c_1 \ket {b_1}$$

The probabilities of outcomes $0$ and $1$ will be defined as $|c_0|^2$ and $|c_1|^2$, respectively.

> Computational basis measurement is often referred to as measurement in Pauli Z basis. Indeed, the eigenvectors of the Z gate are $\ket 0$ and $\ket 1$, with eigenvalues $+1$ and $-1$, respectively.

### Exercise 3: Distinguish $|+\rangle$ and $|-\rangle$ using the `Measure` operation

**Input**: A qubit which is guaranteed to be in either the $\ket +$ state, or the $\ket -$ state.

**Output**: `true` if the qubit is in the $\ket -$ state, or `false` if it was in the $\ket +$ state.

> To perform a single-qubit measurement in a certain Pauli basis using the `Measure` operation,
> you need to pass it two parameters: first, an array of one `Pauli` constant (`PauliX`, `PauliY` or `PauliZ`), and second, an array of one qubit you want to measure.

```qsharp
%kata T3_IsQubitMinus

operation IsQubitMinus (q : Qubit) : Bool {
    // ...
}
```

*Can't come up with a solution? See the explained solution in the [Single Qubit System Measurements Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-3:-Distinguish-$|+\rangle$-and-$|-\rangle$-using-the-Measure-operation).*

## Measurements in arbitrary orthogonal bases
It is possible to measure a qubit in orthogonal bases other than the Pauli bases. Suppose one wants to measure a qubit in an orthonormal basis $\ket {b_0}$ and $\ket {b_1}$. Let the state of the qubit be represented by the normalized vector $\ket \psi$. Then, one can always express the state in terms of the basis vectors $\ket{b_0}$ and $\ket{b_1}$, i.e., there exist complex numbers $c_0, c_1$, such that
$$
\ket \psi = c_0 \ket {b_0} + c_1 \ket {b_1}.
$$
The rule for obtaining the probabilities of measurement outcomes is exactly the same as that for the computation basis measurement. For a measurement in a $\{ b_0, b_1\}$ basis we get
- Outcome $b_0$ with probability $|c_0|^2$, and the post-measurement state of the qubit $\ket {b_0}$;
- Outcome $b_1$ with probability $|c_1|^2$, and the post-measurement state of the qubit $\ket {b_1}$.

This can be summarized in the following table:

    <col width=150>
    <col width=150>
    <col width=150>

        Measurement outcome
        Probability of outcome
        State after measurement

        $b_0$
        $|c_0|^2$
        $\ket{b_0}$

        $b_1$
        $|c_1|^2$
        $\ket{b_1}$

As before, the assumption of $\ket \psi$ being normalized is important, since it guarantees that the two probabilities add to $1$.

> As you may recall, a [global phase](../Qubit/Qubit.ipynb#Relative-and-Global-Phase) is said to be hidden or unobservable.
This is explained by the fact that global phases have no impact on quantum measurements. For example, consider two isolated qubits which are in (normalized) states $\ket \psi$ and $e^{i\theta}\ket \psi$.
If both are measured in an orthogonal basis $\{ \ket{b_0},\ket{b_1}\}$, the probabilities of measuring $b_0$ or $b_1$ are identical in both cases, since $|\bra{b_i}\ket{\psi}|^2 = |\bra{b_i}e^{i\theta}\ket{\psi}|^2  $.
Similarly, for either qubit, if $b_i$ is the measurement outcome, the post-measurement state of the qubit is $\ket{b_i}$ for both qubits. Hence, the measurements are independent of the global phase $\theta$.

> ### Measurements as projection operations
Quantum measurements are modeled by orthogonal projection operators. An orthogonal projection operator is a matrix $P$ which satisfies the following property:
$$
P^2 = P^\dagger = P.
$$
(As usual, the $\dagger$ symbol denotes conjugate transposition.)
>
> Using the [ket-bra representation](../SingleQubitGates/SingleQubitGates.ipynb#Ket-bra-Representation), one can represent a projection matrix in the Dirac notation.
For example, one may construct a projector onto the $\ket{0}$ subspace as:
$$
P = \ket 0 \bra 0 \equiv \begin{bmatrix} 1 & 0 \\ 0 & 0\end{bmatrix}.
$$
>
>A measurement in an orthogonal basis $\{ \ket{b_0}, \ket{b_1}\}$ is described by a pair of projectors $P_0 = \ket{b_0}\bra{b_0}$ and $P_1 = \ket{b_1}\bra{b_1}$. Since $\ket{b_0}$ and $\ket{b_1}$ are orthogonal, their projectors are also orthogonal, i.e., $P_0 P_1 = P_1 P_0 = 0$. The rules for measurements in this basis can then be summarized as follows:
- Measuring a qubit in a state $\ket \psi$ is done by picking one of these projection operators at random.
- Projection $P_0$ is chosen with probability $|P_0 \ket{\psi}|^2$, and the projector $P_1$ is chosen with probability $|P_1\ket{\psi}|^2$.
- If projector $P_0$ is chosen, the post-measurement state of the qubit is given by
$$
\frac1{|P_0 \ket{\psi}|}P_0 \ket\psi,
$$
and similarly for $P_1$.
>
>Although this formalism looks different from the previous sections, it is in fact equivalent. If $\ket \psi = c_0 \ket{b_0} + c_1 \ket{b_1}$, we have
$$
P_0 \ket \psi = c_0 \ket{b_0}, \text{so that } | P_0\ket \psi| = c_0,
$$
and similarly,
$$
P_1 \ket \psi = c_1 \ket{b_1}, \text{so that } |P_1\ket \psi| = c_1.
$$
>
>Thus, as before, the probability of measuring $b_0$ is $|P_0\ket\psi|^2 = |c_0|^2$, and the probability of measuring $b_1$ is $|P_1\ket\psi|^2 = |c_1|^2$. Similarly, one can verify that the post-measurement outcomes are also $\ket{b_0}$ and $\ket{b_1}$ respectively (up to unobservable global phases).
>
>Although the projector formalism for single-qubit systems may seem superfluous, its importance will become clear later, while considering measurements for multi-qubit systems.

### Arbitrary basis measurements implementation
In the previous section, we discussed measurements in Pauli bases using the built-in `Measure` operation. We will now show that using just unitary rotation matrices and computation basis measurements it is always possible to measure a qubit in any orthogonal basis.

Consider a state $ \ket \psi = c_0 \ket {b_0} + c_1 \ket {b_1} $ which we would like to measure in an orthonormal basis $\{ \ket{b_0}, \ket{b_1}\}$. First, we construct the following unitary matrix:
$$
U = \ket{0} \bra{b_0} + \ket{1} \bra{b_1}
$$

The conjugate transpose of this unitary is the operator
$$
U^\dagger = \ket{b_0} \bra{0} + \ket{b_1} \bra{1}
$$

(One may verify that $U$ is indeed a unitary matrix, by checking that $U^\dagger U = U U^\dagger = I$.)

Note that the effect of these matrices on the two bases is the following:
\begin{align}
U\ket{b_0} &= \ket{0}; & U\ket{b_1} &= \ket{1}\\
U^\dagger \ket{0} &= \ket{b_0}; & U^\dagger \ket 1 &= \ket{b_1}.
\end{align}

In order to implement a measurement in the $\{ \ket{b_0}, \ket{b_1}\}$ basis, we do the following:

1. Apply $U$ to $\ket \psi$.
   The resulting state is $U\ket \psi = c_0 \ket 0 + c_1 \ket 1 $.
2. Measure the state $U\ket{\psi}$ in the computational basis.
   The outcomes $0$ and $1$ occur with probabilities $|c_0|^2$ and $|c_1|^2$.
3. Apply $U^\dagger$ to the post-measurement state.
   This transforms the states $\ket 0$ and $\ket 1$ to the states $\ket{b_0}$ and $\ket{b_1}$, respectively.

Thus, $b_0$ and $b_1$ are measured with probabilities $|c_0|^2$ and $|c_1|^2$, respectively, with the end state being $\ket{b_0}$ and $\ket{b_1}$ - which is exactly the measurement we want to implement.

This procedure can be used to distinguish arbitrary orthogonal states as well, as will become clear from the following exercises.

### Exercise 4: The outcome probabilities for a measurement in a specified basis

What are the outcome probabilities of measuring a qubit in the $\ket{0}$ state in the Pauli X basis, i.e., the $\{ \ket +, \ket -\}$ basis?

What are the outcome probabilities of measuring a qubit in the $0.6\ket{0} + 0.8 \ket{1}$ state in the Pauli Y basis, i.e., the $\{ \ket i, \ket{-i}\}$ basis?

*Can't come up with a solution? See the explained solution in the [Single Qubit Measurement Tutorial Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-4:-The-outcome-probabilities-for-a-measurement-in-a-specified-basis).*

### Exercise 5: Distinguishing orthogonal states - 1

**Input:** A qubit which is guaranteed to be in either the $\ket {\psi_+}$ or the $\ket{\psi_-} $ state, where $\ket {\psi_+} = 0.6\ket 0 + 0.8 \ket 1 $ and $\ket {\psi_-} = -0.8\ket 0 + 0.6 \ket 1$.

**Output:** `true` if the qubit was in the $\ket {\psi_+}$ state, or `false` if it was in the $\ket{\psi_-} $ state. The state of the qubit at the end of the operation does not matter.

    Need a hint? Click here
    A suitable $R_y$ rotation can be used to go from the computational basis $\{ \ket 0, \ket 1 \}$ to the $\{ \ket{\psi_+}, \ket{\psi_-} \}$ basis and vice versa.

```qsharp
%kata T5_IsQubitPsiPlus

open Microsoft.Quantum.Math;

operation IsQubitPsiPlus (q : Qubit) : Bool {
    // ...
}
```

*Can't come up with a solution? See the explained solution in the [Single Qubit Measurement Tutorial Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-5:-Distinguishing-orthogonal-states---1).*

### Exercise 6: Distinguishing orthogonal states - 2

**Inputs:**

1. Angle $\alpha$, in radians, represented as a `Double`.
2. A qubit which is guaranteed to be in either the $|A\rangle$ or the $|B\rangle$ state, where $|A\rangle = \cos \alpha |0\rangle - i \sin \alpha |1\rangle$ and $|B\rangle = - i \sin \alpha |0\rangle + \cos \alpha |1\rangle$.

**Output:** `true` if the qubit was in the $|A\rangle$ state, or `false` if it was in the $|B\rangle$ state. The state of the qubit at the end of the operation does not matter.

    Need a hint? Click here
    An $R_x$ rotation can be used to go from the computational basis $\{ \ket 0, \ket 1 \}$ to the $\{ \ket{A}, \ket{B} \}$ basis and vice versa.

```qsharp
%kata T6_IsQubitA

operation IsQubitA (alpha : Double, q : Qubit) : Bool {
    // ...
}
```

*Can't come up with a solution? See the explained solution in the [Single Qubit Measurement Tutorial Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-6:-Distinguishing-orthogonal-states---2).*

### Exercise 7: Measurement in the $\ket A$, $\ket B$ basis

**Inputs:**
1. Angle $\alpha$, in radians, represented as a `Double`.
1. A qubit in some unknown state.

**Output:** Implement a measurement in the $\{\ket A$, $\ket B\}$ basis. Same as in the previous exercise, $|A\rangle = \cos \alpha |0\rangle - i \sin \alpha |1\rangle$ and $|B\rangle = - i \sin \alpha |0\rangle + \cos \alpha |1\rangle$. Return `Zero` if the measurement outcome is $A$, and `One` if the outcome is $B$.
The state of the qubit after the measurement should correspond to the measurement result.

    Need a hint? Click here
    An $R_x$ rotation can be used to go from the computational basis $\{ \ket 0, \ket 1 \}$ to the $\{ \ket{A}, \ket{B} \}$ basis and vice versa.

```qsharp
%kata T7_MeasureInABBasis

operation MeasureInABBasis (alpha: Double, q : Qubit) : Result {
    // ...
}
```

*Can't come up with a solution? See the explained solution in the [Single Qubit Measurement Tutorial Workbook](./Workbook_SingleQubitSystemMeasurements.ipynb#Exercise-7:-Measurement-in-the-$\ket-A$,-$\ket-B$-basis).*

## Conclusion

Congratulations! You have learned enough to try solving the first part of the [Measurements kata](../../Measurements/Measurements.ipynb).
When you are done with that, you can continue to the next tutorials in the series to learn about measurements for multi-qubit systems (coming soon...)


```python
import numpy as np
import sympy as sym
import solowpy
```

# 1 Creating an instance of the `solow.Model` class

In this notebook I will walk you through the creation of an instance of the `solow.Model` class. To create an instance of the `solow.Model` we must define two primitives: an aggregate production function and a dictionary of model parameter values.

## 1.1 Defining the production function $F$:

At each point in time the economy in a Solow growth model has some amounts of capital, $K$, labor, $L$, and knowledge (or technology), $A$, that can be combined to produce output, $Y$, according to some function, $F$:

$$ Y(t) = F(K(t), A(t)L(t)) \tag{1.1.1} $$

where $t$ denotes time. Note that $A$ and $L$ are assumed to enter multiplicatively. Typically $A(t)L(t)$ denotes "effective labor", and technology that enters in this fashion is known as labor-augmenting or "Harrod neutral."

A key assumption of the model is that the function $F$ exhibits constant returns to scale in capital and labor inputs. Specifically,

$$ F(cK(t), cA(t)L(t)) = cF(K(t), A(t)L(t)) = cY(t) \tag {1.1.2} $$

for any $c \ge 0$. For reference, the above information is contained in the docstring of the `solow.Model.output` attribute.

```python
solow.Model.output?
```

### Examples:
A common functional form for aggregate production in a Solow model that satisies the above assumptions is the [Cobb-Douglas](http://en.wikipedia.org/wiki/Cobb%E2%80%93Douglas_production_function) production function

\begin{equation}
  \lim_{\rho \rightarrow 0} Y(t) = K(t)^{\alpha}(A(t)L(t))^{1-\alpha}. \tag{1.1.3}
\end{equation}

The Cobb-Douglas production function is actually a special case of a more general class of production functions called [constant elasticity of substitution (CES)](http://en.wikipedia.org/wiki/Constant_elasticity_of_substitution) production functions.

\begin{equation}
  Y(t) = \bigg[\alpha K(t)^{\rho} + (1-\alpha) (A(t)L(t))^{\rho}\bigg]^{\frac{1}{\rho}} \tag{1.1.4}
\end{equation}

where $0 < \alpha < 1$ and $-\infty < \rho < 1$. The parameter $\rho = \frac{\sigma - 1}{\sigma}$ where $\sigma$ is the elasticity of substitution between factors of production. Taking the limit of equation 1.2 as the elasticity of subsitution goes to unity (i.e., $\sigma=1 \implies \rho=0$) recovers the Cobb-Douglas functional form.

```python
# define model variables
A, K, L = sym.symbols('A, K, L')

# define production parameters
alpha, sigma = sym.symbols('alpha, sigma')

# define a production function
cobb_douglas_output = K**alpha * (A * L)**(1 - alpha)

rho = (sigma - 1) / sigma
ces_output = (alpha * K**rho + (1 - alpha) * (A * L)**rho)**(1 / rho)
```

## 1.2 Defining model parameters

A generic Solow growth model has several parameters that need to be specified. To see which parameters are required, we can check the docstring of the `solow.Model.params` attribute.

```python
solow.Model.params?
```

In addition to the standard parameters $g, n, s, \delta$, one will also need to specify any required parameters for the production function. In order to make sure that parameter values are consistent with the models assumptions some basic validation of the `solow.Model.params` attribute is done when ever the attribute is set.

```python
# these parameters look fishy...why?
default_params = {'A0': 1.0, 'L0': 1.0, 'g': 0.0, 'n': -0.03, 's': 0.15,
                  'delta': 0.01, 'alpha': 0.33}
```

```python
# ...raises an AttributeError
model = solowpy.Model(output=cobb_douglas_output, params=default_params)
```

### Examples:
Here are some examples of how one successfully creates an instance of the `solow.Model` class...

```python
cobb_douglas_params = {'A0': 1.0, 'L0': 1.0, 'g': 0.02, 'n': 0.03, 's': 0.15,
                       'delta': 0.05, 'alpha': 0.33}

cobb_douglas_model = solow.Model(output=cobb_douglas_output,
                                 params=cobb_douglas_params)
```

```python
ces_params = {'A0': 1.0, 'L0': 1.0, 'g': 0.02, 'n': 0.03, 's': 0.15,
              'delta': 0.05, 'alpha': 0.33, 'sigma': 0.95}

ces_model = solowpy.Model(output=ces_output, params=ces_params)
```

## 1.3 Other attributes of the `solow.Model` class

### The intensive form of the production function

The assumption of constant returns to scale allows us to work with the intensive form of the aggregate production function, $F$. Defining $c=1/AL$ one can write

$$ F\bigg(\frac{K}{AL}, 1\bigg) = \frac{1}{AL}F(A, K, L) \tag{1.3.1} $$

Defining $k=K/AL$ and $y=Y/AL$ to be capital per unit effective labor and output per unit effective labor, respectively, the intensive form of the production function can be written as

$$ y = f(k). \tag{1.3.2}$$

Additional assumptions are that $f$ satisfies $f(0)=0$, is concave (i.e., $f'(k) > 0, f''(k) < 0$), and satisfies the Inada conditions: $\lim_{k \rightarrow 0} = \infty$ and $\lim_{k \rightarrow \infty} = 0$. The (Inada, 1964) conditions are sufficient (but not necessary!) to ensure that the time path of capital per effective worker does not explode. Much of the above information is actually taken straight from the docstring for the `solow.Model.intensive_output` attribute.

```python
solowpy.Model.intensive_output?
```

```python
ces_model.intensive_output
```

    (1.0**((sigma - 1)/sigma)*(-alpha + 1) + alpha*k**((sigma - 1)/sigma))**(sigma/(sigma - 1))

One can numerically evaluate the intensive output for various values of capital stock (per unit effective labor) as follows...

```python
ces_model.evaluate_intensive_output(np.linspace(1.0, 10.0, 25))
```

    array([ 1.        ,  1.11015628,  1.20064292,  1.27820127,  1.34651243,
            1.40783043,  1.46364616,  1.51500233,  1.56265965,  1.60719189,
            1.64904359,  1.68856684,  1.72604577,  1.76171326,  1.79576289,
            1.82835742,  1.85963514,  1.88971468,  1.91869861,  1.94667628,
            1.97372605,  1.99991707,  2.02531067,  2.04996155,  2.07391871])

### The marginal product of capital

The marginal product of capital is defined as follows:

$$ \frac{\partial F(K, AL)}{\partial K} \equiv f'(k) \tag{1.3.3}$$

where $k=K/AL$ is capital stock (per unit effective labor).

```python
solowpy.Model.marginal_product_capital?
```

```python
ces_model.marginal_product_capital
```

    alpha*k**((sigma - 1)/sigma)*(1.0**((sigma - 1)/sigma)*(-alpha + 1) + alpha*k**((sigma - 1)/sigma))**(sigma/(sigma - 1))/(k*(1.0**((sigma - 1)/sigma)*(-alpha + 1) + alpha*k**((sigma - 1)/sigma)))

One can numerically evaluate the marginal product of capital for various values of capital stock (per unit effective labor) as follows...

```python
ces_model.evaluate_mpk(np.linspace(1.0, 10.0, 25))
```

    array([ 0.33      ,  0.26345407,  0.22196166,  0.19325704,  0.17204444,
            0.15563456,  0.14250582,  0.13172779,  0.12269712,  0.11500417,
            0.10836032,  0.1025559 ,  0.09743467,  0.09287764,  0.08879245,
            0.08510627,  0.08176081,  0.07870886,  0.0759117 ,  0.07333729,
            0.07095884,  0.0687538 ,  0.06670301,  0.0647901 ,  0.06300099])

### Equation of motion for capital (per unit effective labor)
Because the economy is growing over time due to technological progress, $g$, and population growth, $n$, it makes sense to focus on the capital stock per unit effective labor, $k$, rather than aggregate physical capital, $K$. Since, by definition, $k=K/AL$, we can apply the chain rule to the time derative of $k$.

\begin{align}
\dot{k}(t) =& \frac{\dot{K}(t)}{A(t)L(t)} - \frac{K(t)}{[A(t)L(t)]^2}\bigg[\dot{A}(t)L(t) + \dot{L}(t)A(t)\bigg] \\
=& \frac{\dot{K}(t)}{A(t)L(t)} - \bigg(\frac{\dot{A}(t)}{A(t)} + \frac{\dot{L}(t)}{L(t)}\bigg)\frac{K(t)}{A(t)L(t)} \tag{1.3.4}
\end{align}

By definition, $k=K/AL$, and by assumption $\dot{A}/A$ and $\dot{L}/L$ are $g$ and $n$ respectively. Aggregate capital stock evolves according to

$$ \dot{K}(t) = sF(K(t), A(t)L(t)) - \delta K(t). \tag{1.3.5}$$

Substituting these facts into the above equation yields the equation of
motion for capital stock (per unit effective labor).

\begin{align}
\dot{k}(t) =& \frac{sF(K(t), A(t)L(t)) - \delta K(t)}{A(t)L(t)} - (g + n)k(t) \\
=& \frac{sY(t)}{A(t)L(t)} - (g + n + \delta)k(t) \\
=& sf(k(t)) - (g + n + \delta)k(t) \tag{1.3.6}
\end{align}

The above information is available for reference in the docstring for the `solow.Model.k_dot` attribute.

```python
solowpy.Model.k_dot?
```

```python
ces_model.k_dot
```

    -k*(delta + g + n) + s*(1.0**((sigma - 1)/sigma)*(-alpha + 1) + alpha*k**((sigma - 1)/sigma))**(sigma/(sigma - 1))

One can numerically evaluate the equation of motion for capital (per unit effective labor) for various values of capital stock (per unit effective labor) as follows...

```python
ces_model.evaluate_k_dot(np.linspace(1.0, 10.0, 25))
```

    array([ 0.05      ,  0.02902344,  0.00509644, -0.02076981, -0.04802314,
           -0.07632544, -0.10545308, -0.13524965, -0.16560105, -0.19642122,
           -0.22764346, -0.25921497, -0.29109314, -0.32324301, -0.35563557,
           -0.38824639, -0.42105473, -0.4540428 , -0.48719521, -0.52049856,
           -0.55394109, -0.58751244, -0.6212034 , -0.65500577, -0.68891219])

## 1.4 Sub-classing the `solow.Model` class

Several commonly used functional forms for aggregate production, including both the Cobb-Douglas and Constant Elasticity of Substitution (CES) production functions, have been sub-classed from `solow.Model`. For these functional forms, one only needs to specify a valid dictionary of model parameters.

```python
solowpy.cobb_douglas?
```

```python
cobb_douglas_model = solowpy.CobbDouglasModel(params=cobb_douglas_params)
```

```python
solowpy.ces?
```

```python
ces_model = solowpy.CESModel(params=ces_params)
```

Now that you understand the basics, we can move on to [finding the steady state](./Finding the steady state.ipynb) of the Solow growth model.

```python

```


# Lattice Boltzmann Method

```python
import sympy
```

```python
from sympy import exp, integrate, pi, sqrt, Symbol, symbols, oo
```

```python
from sympy import Abs, Q, periodic_argument, polar_lift, refine
```

```python
from sympy import cos
```

```python
from sympy import Rational as Rat
```

# $d2q9$

cf.Jonas Tölke.  *Implementation of a Lattice Boltzmann kernel using the Compute
Unified Device Architecture developed by nVIDIA.* **Comput. Visual Sci.** DOI 10.1007/s00791-008-0120-2

### Affine Spaces

# Lattices, that are "sufficiently" Galilean invariant, through non-perturbative algebraic theory
cf. [http://staff.polito.it/pietro.asinari/publications/preprint_Asinari_PA_2010a.pdf](http://staff.polito.it/pietro.asinari/publications/preprint_Asinari_PA_2010a.pdf), I. Karlin and P. Asinari, *Factorization symmetry in the lattice Boltzmann method*.  **Physica A 389**, 1530 (2010).  The prepaper that this seemd to be based upon and had some more calculation details is [](https://arxiv.org/abs/0911.5529)

## Maxwell Lattices in 1-dim.
### Maxwell's (M) moment relations

```python
#u = Symbol("u",assume="real")
u = Symbol("u",real=True)
#T_0 =Symbol("T_0",assume="positive")
T_0 =Symbol("T_0",real=True,positive=True)
```

```python
#v = Symbol("v",assume="real")
v = Symbol("v",real=True)
```

```python
#phi_v = sqrt( pi/(Rat(2)*T_0))*exp( - (v-u)**2/(Rat(2)*T_0))
phi_v = sqrt( pi/(2*T_0))*exp( - (v-u)**2/(2*T_0))
```

```python
integrate(phi_v,v)
```

    pi*sqrt(T_0)*sqrt(1/T_0)*erf(sqrt(2)*(-u + v)/(2*sqrt(T_0)))/2

```python
integrate(phi_v,(v,-oo,oo))
```

    Piecewise((pi*(-erfc(sqrt(2)*u/(2*sqrt(T_0))) + 2)/2 + pi*erfc(sqrt(2)*u/(2*sqrt(T_0)))/2, And(Or(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) <= pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) <= pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi)), Or(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) <= pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) <= pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi)))), (Integral(sqrt(2)*sqrt(pi)*sqrt(1/T_0)*exp(-(-u + v)**2/(2*T_0))/2, (v, -oo, oo)), True))

```python
(integrate(phi_v,v).subs(u,oo)- integrate(phi_v,v).subs(u,-oo)).expand()
```

    pi*sqrt(T_0)*sqrt(1/T_0)*erf(sqrt(2)*v/(2*sqrt(T_0)) - oo/sqrt(T_0))/2 - pi*sqrt(T_0)*sqrt(1/T_0)*erf(sqrt(2)*v/(2*sqrt(T_0)) + oo/sqrt(T_0))/2

```python
integrate(phi_v,(v,-oo,oo),conds='none')
```

    pi*(-erfc(sqrt(2)*u/(2*sqrt(T_0))) + 2)/2 + pi*erfc(sqrt(2)*u/(2*sqrt(T_0)))/2

```python
integrate(phi_v,(v,-oo,oo),conds='separate')
```

    (pi*(-erfc(sqrt(2)*u/(2*sqrt(T_0))) + 2)/2 + pi*erfc(sqrt(2)*u/(2*sqrt(T_0)))/2,
     And(Or(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) <= pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) <= pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi)), Or(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) <= pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) <= pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi))))

cf. http://stackoverflow.com/questions/16599325/simplify-conditional-integrals-in-sympy

```python
refine(integrate(phi_v,(v,-oo,oo)), Q.is_true(Abs(periodic_argument(1/polar_lift(sqrt(T_0))**2, oo)) <= pi/2))
```

    Piecewise((pi*(-erfc(sqrt(2)*u/(2*sqrt(T_0))) + 2)/2 + pi*erfc(sqrt(2)*u/(2*sqrt(T_0)))/2, And(Or(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) <= pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) <= pi/2, Abs(periodic_argument(exp_polar(2*I*pi)*polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi)), Or(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) < pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) <= pi), And(Abs(periodic_argument(1/polar_lift(T_0), oo)) <= pi/2, Abs(periodic_argument(polar_lift(u)**2/polar_lift(T_0)**2, oo)) < pi)))), (Integral(sqrt(2)*sqrt(pi)*sqrt(1/T_0)*exp(-(-u + v)**2/(2*T_0))/2, (v, -oo, oo)), True))

```python

```


# Iterative hard thresholding Tutorial

This notebook showcase a few examples of the software [MendelIHT.jl](https://github.com/OpenMendel/MendelIHT.jl).

```julia
#load necessary packages, install them if you don't have it
using MendelIHT
using SnpArrays
using DataFrames
using Distributions
using Random
using LinearAlgebra
using GLM
using DelimitedFiles
using Statistics
using BenchmarkTools
using Plots
```

# What is IHT?

Iterative hard thresholding (IHT) is a sparse approximation method that performs variable selection and parameter estimation for high dimensional datasets. IHT returns a sparse model with prespecified $k \in \mathbb{Z}_+$ (or fewer) non-zero entries. In [MendelIHT.j](), the objective function is:

\begin{align}
\text{maximize } & \quad L(\beta)\\
\text{subject to } & \quad ||\beta||_0 \le k
\end{align}

The objective is solved via **projected gradient ascent**:
$$\beta_{n+1} = P_{S_k}(\beta_n + s_n \nabla L(\beta_n)),$$
where:
+ $\nabla L(\beta)$ is the score (gradient) vector of loglikelihood
+ $J(\beta)$ is the expected information (hessian) matrix
+ $s = \frac{||\nabla L(\beta)||_2^2}{\nabla L(\beta)^tJ(\beta)\nabla L(\beta)}$ is the step size
+ $P_{S_k}(v)$ projects vector $v$ to sparsity set $S_k$ by setting all but the top $k$ entries to 0.

See [our paper](https://www.biorxiv.org/content/10.1101/697755v2) for more details and computational tricks to do each of these efficiently.

# Example: IHT vs logistic GWAS on UK Biobank

+ Data: ~200,000 samples and ~500,000 SNPs
+ Ran 5-fold cross validated IHT for sparsity levels 1~50, distributed to 5 computers. Each completed with 24h. Total run time $<2$ days.
+ Found 33 significant SNPs, plotted against traditional GWAS result using [MendelPlots.jl](https://github.com/OpenMendel/MendelPlots.jl):

# Supported GLM models and Link functions

MendelIHT borrows distribution and link functions implementationed in [GLM.jl](http://juliastats.github.io/GLM.jl/stable/) and [Distributions.jl](https://juliastats.github.io/Distributions.jl/stable/).

| Distribution | Canonical Link | Status |
|:---:|:---:|:---:|
| Normal | IdentityLink | $\checkmark$ |
| Bernoulli | LogitLink |$\checkmark$ |
| Poisson | LogLink |  $\checkmark$ |
| NegativeBinomial | LogLink |  $\checkmark$ |
| Gamma | InverseLink | experimental |
| InverseGaussian | InverseSquareLink | experimental |

Examples of these distributions in their default value is visualized in [this post](https://github.com/JuliaStats/GLM.jl/issues/289).

### Available link functions

    CauchitLink
    CloglogLink
    IdentityLink
    InverseLink
    InverseSquareLink
    LogitLink
    LogLink
    ProbitLink
    SqrtLink

**Note:** Adding your favorite distribution only requires implementation of loglikelihood, score, and expected information!

# Example 1: How to Import Data

## Simulate example data (to be imported later)

First we simulate an example PLINK trio (`.bim`, `.bed`, `.fam`) and non-genetic covariates, then we illustrate how to import them. For genotype matrix simulation, we simulate under the model:

$$x_{ij} \sim \rm Binomial(2, \rho_j)$$

$$\rho_j \sim \rm Uniform(0, 0.5)$$

```julia
# rows and columns
n = 1000
p = 10000

#random seed
Random.seed!(2020)

# simulate random `.bed` file
x = simulate_random_snparray(n, p, "./data/tmp.bed")

# create accompanying `.bim`, `.fam` files (randomly generated)
make_bim_fam_files(x, ones(n), "./data/tmp")

# simulate non-genetic covariates (in this case, we include intercept and sex)
z = [ones(n, 1) rand(0:1, n)]
writedlm("./data/tmp_nongenetic_covariates.txt", z)
```

### Reading Genotype data and Non-Genetic Covariates from disk

```julia
x = SnpArray("./data/tmp.bed")
z = readdlm("./data/tmp_nongenetic_covariates.txt")
```

### Standardizing Non-Genetic Covariates.

We recommend standardizing all genetic and non-genetic covarariates (including binary and categorical), except for the intercept. This ensures equal penalization for all predictors. For genotype matrix, `SnpBitMatrix` efficiently achieves this standardization. For non-genetic covariates, we use the built-in function `standardize!`.

```julia
# SnpBitMatrix can automatically standardizes .bed file (without extra memory) and behaves like a matrix
xbm = SnpBitMatrix{Float64}(x, model=ADDITIVE_MODEL, center=true, scale=true);

# using view is important for correctness
standardize!(@view(z[:, 2:end]))
z
```

# Example 2: Running IHT on Quantitative Traits

In this example, our model is simulated as:

$$y_i \sim \mathbf{x}_i^T\mathbf{\beta} + \epsilon_i$$

$$x_{ij} \sim \rm Binomial(2, \rho_j)$$

$$\rho_j \sim \rm Uniform(0, 0.5)$$

$$\epsilon_i \sim \rm N(0, 1)$$

$$\beta_i \sim \rm N(0, 1) \quad (\text{for 10 different } i)$$

```julia
# Define model dimensions, true model size, distribution, and link functions
n = 1000
p = 10000
k = 10
dist = Normal
link = canonicallink(dist())

# set random seed for reproducibility
Random.seed!(2020)

# simulate SNP matrix, store the result in a file called tmp.bed
x = simulate_random_snparray(n, p, "./data/tmp.bed")

#construct the SnpBitMatrix type (needed for L0_reg() and simulate_random_response() below)
xbm = SnpBitMatrix{Float64}(x, model=ADDITIVE_MODEL, center=true, scale=true);

# intercept is the only nongenetic covariate
z = ones(n, 1)

# simulate response y, true model b, and the correct non-0 positions of b
y, true_b, correct_position = simulate_random_response(x, xbm, k, dist, link);
```

## Step 1: Run `q`-fold cross validation to determine best model size

To run `cv_iht`, you must specify `path` and `q`, defined below:

+ **`path`**: all the model sizes you wish to test.
+ **`q`**: number of disjoint partitions of your data.

By default, we partition the training/testing data randomly, but you can change this by inputing the `fold` vector as optional argument. In this example we tested $k = 5, 6, ..., 15$ across 3 fold cross validation. This is equivalent to running IHT across 30 different models, and hence, is ideal for parallel computing (which you specify by `parallel=true`).

```julia
path = collect(5:15)      # various sparsity levels to test (in this case 5 through 15)
q    = 3                  # number of fold cross validation

# Inputs: x, z, y = genotype matrix, other covariates, and response, 1 = group info (none)
@time mses = cv_iht(Normal(), IdentityLink(), x, z, y, 1, path, q, parallel=false);
```

```julia
plot(path, mses, label="Mean Squared Error", xlabel="Sparsity level",
        ylabel="Mean Squared Error", linewidth=5)
```

## Step 2: Run IHT on full model for best estimated k

```julia
best_k = path[argmin(mses)]                                        # k = 7 minimizes MSE
result = L0_reg(x, xbm, z, y, 1, best_k, Normal(), IdentityLink()) # run IHT on full data
```

## Check final model against simulation

Since all our data and model was simulated, we can see how well `cv_iht` combined with `L0_reg` estimated the true model. For this example, we find that IHT found every simulated predictor, with 0 false positives.

```julia
compare_model = DataFrame(
    true_β      = true_b[correct_position],
    estimated_β = result.beta[correct_position])
```

**Conclusion:** IHT found 7/10 true predictors, with superb parameter estimates. The remaining 3 predictors cannot be identified by IHT because they have very small effect sizes.

# Example 3: Negative Binomial regression with group information

Now we show how to include group information to perform **doubly sparse** projections. This results in a model with at most $j$ groups where each group contains at most $k$ SNPs. This is useful for:

+ Data with extensive LD blocks (i.e. correlated covariates)
+ Prior knowledge on genes/pathways

## Simulation: IHT on extensive LD blocks
In this example, we simulated:
+ 10,000 SNPs, each belonging to 1 of 500 disjoint groups. Each group contains 20 SNPs
+ $j = 5$ groups are each assigned $1,2,...,5$ causal SNPs with effect sizes randomly chosen from $\{−0.2,0.2\}$.
+ Within group correlation: $\rho = 0.95$

**We assume perfect group information**. That is, the selected groups containing 1∼5 causative SNPs are assigned maximum within-group sparsity $\lambda_g = 1,2,...,5$. The remaining groups are assigned $\lambda_g = 1$ (i.e. only 1 active predictor are allowed).

```julia
# define problem size
n = 1000
p = 10000
dist = NegativeBinomial
link = LogLink()
block_size = 20                  #simulation parameter
num_blocks = Int(p / block_size) #simulation parameter

# set seed
Random.seed!(2020)

# assign group membership
membership = collect(1:num_blocks)
g = zeros(Int64, p + 1)
for i in 1:length(membership)
    for j in 1:block_size
        cur_row = block_size * (i - 1) + j
        g[block_size*(i - 1) + j] = membership[i]
    end
end
g[end] = membership[end]

#simulate correlated snparray
x = simulate_correlated_snparray(n, p, "./data/tmp2.bed", prob=0.95)
z = ones(n, 1) # the intercept
x_float = convert(Matrix{Float64}, x, model=ADDITIVE_MODEL, center=true, scale=true)

#simulate true model, where 5 groups each with 1~5 snps contribute
true_b = zeros(p)
true_groups = randperm(num_blocks)[1:5]
sort!(true_groups)
within_group = [randperm(block_size)[1:1], randperm(block_size)[1:2],
                randperm(block_size)[1:3], randperm(block_size)[1:4],
                randperm(block_size)[1:5]]
correct_position = zeros(Int64, 15)
for i in 1:5
    cur_group = block_size * (true_groups[i] - 1)
    cur_group_snps = cur_group .+ within_group[i]
    start, last = Int(i*(i-1)/2 + 1), Int(i*(i+1)/2)
    correct_position[start:last] .= cur_group_snps
end
for i in 1:15
    true_b[correct_position[i]] = rand(-1:2:1) * 0.2
end
sort!(correct_position)

# simulate phenotype
r = 10 #nuisance parameter
μ = GLM.linkinv.(link, x_float * true_b)
clamp!(μ, -20, 20)
prob = 1 ./ (1 .+ μ ./ r)
y = [rand(dist(r, i)) for i in prob] #number of failures before r success occurs
y = Float64.(y);
```

## IHT without group information

```julia
# x_float = genotype matrix in floating point numbers
# z       = other covariates
# y       = response vector

j = 1      # 1 active group = entire dataset
k = 15     # 15 active predictors
ungrouped_IHT = L0_reg(x_float, z, y, j, k, NegativeBinomial(), LogLink())
```

## IHT with group information

```julia
# x_float = genotype matrix in floating point numbers
# z       = other covariates
# y       = response vector
# g       = group membership vector (simulated above)

j = 5                           # maximum number of active groups
dist = NegativeBinomial()       # distribution
link = LogLink()                # link function

# within-group sparsity for each group
max_group_snps = ones(Int, num_blocks)
max_group_snps[true_groups] .= collect(1:5)

# run grouped IHT
grouped_IHT = L0_reg(x_float, z, y, j, max_group_snps, NegativeBinomial(), LogLink(), group=g)
```

## Check variable selection against true data

```julia
compare_model = DataFrame(
    correct_positions = correct_position,
    ungrouped_IHT_positions = findall(!iszero, ungrouped_IHT.beta),
    grouped_IHT_positions = findall(!iszero, grouped_IHT.beta))
```

## Check estimated parameters against true data

```julia
correct_position = findall(!iszero, true_b)
compare_model = DataFrame(
    position = correct_position,
    correct_β = true_b[correct_position],
    ungrouped_IHT_β = ungrouped_IHT.beta[correct_position],
    grouped_IHT_β = grouped_IHT.beta[correct_position])
```

** Conclusion:** by asking for "top entries" in each group, we (somewhat) disentangle the correlation structure in our data and achieved better model selection!

## More examples:

Please visit our [documentation](https://openmendel.github.io/MendelIHT.jl/latest/).

## Other functionalities

+ Built-in support for [PLINK binary files](https://www.cog-genomics.org/plink/1.9/input#bed) via [SnpArrays.jl](https://github.com/OpenMendel/SnpArrays.jl) and [VCF files](https://en.wikipedia.org/wiki/Variant_Call_Format) via [VCFTools.jl](https://github.com/OpenMendel/VCFTools.jl).
+ Out-of-the-box parallel computing routines for `q-fold` cross-validation.
+ Fits a variety of generalized linear models with any choice of link function.
+ Computation directly on raw genotype files.
+ Efficient handlings for non-genetic covariates.
+ Optional acceleration (debias) step to dramatically improve speed.
+ Ability to explicitly incorporate weights for predictors.
+ Ability to enforce within and between group sparsity.
+ Naive genotype imputation.
+ Estimates nuisance parameter for negative binomial regression using Newton or MM algorithm.


**Grupo:**

Davi Richards\
Maria Eduarda Ferreira\
Pedro Wong

# Equações diferenciais

Uma equação diferencial, de forma bastante abstrata, é dada por
$$ \begin{cases}
    y'(t)  & = F(t, y(t)) \\
    y(t_0) & = y_0.
\end{cases} $$
Uma solução é **uma função** $y(t)$ satisfazendo tanto a _condição inicial_ $y(t_0) = y_0$
quanto a _dinâmica_ $y'(t) = F(t, y(t))$.

Por questões práticas, também é comum dar um _intervalo de solução_, ou seja,
pedir que $y$ seja definida (pelo menos) num intervalo $[a,b]$ onde $t_0 \in [a,b]$.
Aqui, vamos usar $a = t_0$,
pensando "fisicamente" que a equação diferencial modela
a evolução de alguma quantidade $y$ que conhecemos agora ($t_0$)
e que queremos saber o quanto restará num momento futuro ($t_f = b$).

## Parte 1: O método de Euler

Continuando nesta linha, é razoável discretizar o problema da seguinte forma:

1. Definimos uma sequência de pontos $a = t_0 < t_1 < t_2 < \cdots < t_n = t_f = b$ no intervalo $[a,b]$.
2. Como $f$ é derivável, temos a aproximação $y(t_{i+1}) \sim y(t_i) + (t_{i+1} - t_i)y'(t_i)$.
3. A equação diferencial permide calcular $y'(t_i) = F(t_i, y(t_i))$.
4. Isso define (recursivamente) uma sequência de aproximações $y_i$ para $y(t_i)$ pela fórmula
$$ y_{i+1} = y_i + (t_{i+1} - t_i) F(t_i, y_i).$$

A sequência mais simples de ser usada é, claro, a de $n+1$ pontos igualmente espaçados no intervalo $[a,b]$.

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

### 1. Implemente o método de Euler

Escreva a função `euler` que divide o intervalo $[t_0, t_f]$ em `nsteps` intervalos de mesmo comprimento,
e retorna
- as `nsteps+1` extremidades `t_i`,
- as `nsteps+1` aproximações `y_i` de $y$ nestes pontos.

Dica: use `append` para construir os valores de `y_i`, mas retorne um `np.array()` para simplificar as contas.

```python
def euler(F, t0, tf, y0, nsteps=100):
    """Recebe uma função F(t,y) e dois pontos t0 e tf, retornando um array com n+1
    pontos igualmente espaçados do intervalo [t0,tf] e as aproximações dos y's
    nos nesses pontos."""
    ts, h = np.linspace(t0, tf, num=nsteps+1, retstep=True)
    ys = [y0]
    for t in ts[:-1]:
      yt = ys[-1]
      ys.append(yt + h * F(t, yt))
    return ts, np.array(ys)
```

Resolvendo uma EDO simples: $f' = f$.

```python
def F1(t,y):
    return y

t_100, y_100 = euler(F1, 0, 1, 1)
assert len(t_100) == 101
assert t_100[0] == 0
assert t_100[-1] == 1
```

```python
t_100, y_100 = euler(F1, 0, 1, 1)
assert len(y_100) == 101
assert y_100[0] == 1
assert( abs(y_100[-1] - 2.704813829421526) < 5e-15 )
```

O que é melhor é que o método de Euler permite resolver equações diferenciais bastante (arbitrariamente!) complicadas:

```python
def F_dificil(t, y):
    return t*np.sin(y) - t**2*np.cos(y)

_, y_dificil = euler(F_dificil, 0, 2, 1, nsteps=100)
assert( abs(y_dificil[-1] - 3.553673725510754) < 5e-15 )
```

### 2. Qualidade da aproximação

Agora, faça um gráfico da solução numérica com mil pontos $f_{1000}$ para $f' = f$,
e da solução exata $f(t) = e^t$ no intervalo $[0,1]$.
Faça também um gráfico da diferença entre estas soluções.

```python
t_1000, f_1000 = euler(F1, 0, 1, 1, nsteps=1000)
f_ex = np.exp(t_1000)

fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(13, 4.8))

axs[0].plot(t_1000, f_1000, label="Aproximação", color="C1")
axs[0].plot(t_1000, f_ex, label='"Solução exata" (Numpy)', linestyle=":", color="k")

axs[0].set_xlabel("$t$")
axs[0].set_ylabel("$f(t)$")

axs[0].legend()
axs[0].set_title("Solução numérica de $f'=f$ aprox. pelo método de Euler")
axs[0].grid()

axs[1].plot(t_1000, np.abs(f_1000 - f_ex))

axs[1].set_xlabel("$t$")
axs[1].set_ylabel("$|f(t) - e^t|$")

axs[1].set_title("Erro absoluto da solução numérica")
axs[1].grid();
```

Faça o gráfico agora do erro **relativo**.

```python
fig, ax = plt.subplots()

ax.plot(t_1000, np.abs(f_1000 - f_ex)/f_ex)

ax.set_xlabel("$t$")
ax.set_ylabel("$|f(t) - e^t| \cdot e^{-t}$")

ax.set_title("Erro relativo da solução numérica")
ax.grid()

ax = plt.gca()
plt.show()
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 1
```

```python
xdata = ax.lines[0].get_xdata()
assert min(xdata) == 0.0
assert max(xdata) == 1.0

ydata = ax.lines[0].get_ydata()
assert min(ydata) > -0.001
assert max(ydata) <  0.001
```

```python
ax = None
```

### 3. Medindo erros

Suponha que conhecemos a solução exata da EDO, que chamaremos aqui de $g(t)$ para evitar confundir com $y_i$.
Mesmo que em muitos casos seja bastante razoável usar o "erro final" $g(t_n) - y_n$,
onde $g$ é a solução exata e $y_n$ a aproximação,
vamos nos concentrar no "erro total".
E, para ficar relativamente simples, vamos usar a **média dos valores absolutos das diferenças** entre $g(t_j)$ e $y_j$.
Usamos a média, e não a soma, porque ao variar o número de passos esta quantidade se mantém homogênea.

```python
def erro(g, ys, ts):
    """ Calcula o erro entre os valores de ys e g(ts).  ys e ts  devem ser vetores numpy, e  g uma função. """
    # return np.sum(np.abs(g(ts) - ys))/len(ys)
    return np.average(np.abs(g(ts) - ys))
```

Como o erro total diminui ao aumentar o número de passos efetuados?
Faça um gráfico.

```python
%%time
ns = np.logspace(1,5,num=21, dtype=int)
err = []

for n in ns:
  ts,ys = euler(F1, 0, 1, 1, nsteps = n)
  err.append(erro(np.exp, ys, ts))

fig, ax = plt.subplots()

ax.loglog(ns, err)

ax.set_xlabel("Número de passos")
ax.set_ylabel("$|f(t) - e^t|$")

ax.set_title("Erro absoluto médio da solução numérica")
ax.grid()

ax = plt.gca()
plt.show();
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 1

ydata = ax.lines[0].get_ydata()
assert 1e-6 < min(ydata) < 1e-4
assert 1e-2 < max(ydata) < 1e-1
```

E comente o resultado.

O gráfico do erro total $\varepsilon_T$ em função do número de passos $n$ é linear na escala loglog, o que indica um decaimento exponencial do erro com $\log(n)$. Mais precisamente, temos $\log(\varepsilon_T)$ um pouco menor do que $-\log(n)$.

Ademais, a regularidade da curva sugere que pouco ruído foi introduzido devido ao erro numérico. Isso indica que, nas condições dadas para o problema e nessa escala de valores de $n$ (entre $10^1$ e $10^5$), a magnitude do erro numérico foi, provavelmente, bastante menor do que a do erro de aproximação "analítico".

### 4. Sistemas de Equações

Se você usar funções "vetorizadas" para `F`,
o seu método de Euler é capaz de resolver (aproximar) até _sistemas_ de equações diferenciais.

Por exemplo, o sistema
$$ \begin{align}
v_0' & =  v_1 \\
v_1' & = -v_0
\end{align} $$
que é equivalente à equação diferencial trigonométrica básica $y'' = -y$,
pode ser simulado assim:

```python
def F2(t, v):
    return np.array([v[1],-v[0]])

ans = np.array([-1.0505597866729235, -0.0010851561020816455])
ts, vs = euler(F2, 0, np.pi, np.array([1.0,0.0]), nsteps=100)

assert( all(abs(vs[-1]/ans - 1) < 1e-15) )
```

Dependendo de como você tenha implementado `euler()`, os valores de `vs` serão todos iguais,
pois você terá construído uma lista com `nsteps+1` vezes o mesmo `array`.
Para corrigir isso, modifique sua função `euler` para criar uma cópia de cada valor de $f_i$ antes de incluir na lista.

```python
# def euler(F, t0, tf, v0, nsteps=100):
#     pass
```

*Foi esclarecido pelo prof. Pedro no Discord que, caso não tivéssemos o problema dos valores de `vs` repetidos, poderíamos substituir essa definição por um `pass`. Como isso teria o problema de "sobrescrever" a definição anterior, comentamos a célula.*

Para usar as coordenadas da solução separadamente, ou seja, $f_1$ e $f_2$,
temos que transpor `v`.
(Note que os métodos constróem ao longo do tempo, e não por coordenadas)

```python
ts, vs = euler(F2, 0, np.pi, np.array([1.0,0.0]), nsteps=100)
vs = np.transpose(vs)
vs[:,:10]
```

    array([[ 1.        ,  1.        ,  0.99901304,  0.99703912,  0.99407921,
             0.99013527,  0.9852102 ,  0.97930792,  0.97243327,  0.96459208],
           [ 0.        , -0.03141593, -0.06283185, -0.09421677, -0.12553968,
            -0.1567696 , -0.18787562, -0.21882691, -0.24959277, -0.28014267]])

### 5. Mais gráficos

Observe que a sua solução numérica realmente está próxima da solução analítica,
fazendo um gráfico para a função e sua derivada,
tanto numéricas como analíticas.

```python
ts, vs = euler(F2, 0, np.pi, np.array([1.0,0.0]), nsteps=100)
vs = np.transpose(vs)

fig, ax = plt.subplots()

ax.plot(ts, vs[0], label="Aproximação de $\cos(t)$")
ax.plot(ts, vs[1], label="Aproximação de $-\sin(t)$")
ax.plot(ts, np.cos(ts), label='$\cos(t)$ (Numpy)', linestyle=":", color="r")
ax.plot(ts, -np.sin(ts), label='$-\sin(t)$ (Numpy)', linestyle=":", color="k")

ax.set_xlabel("$t$")
ax.set_ylabel("$f(t)$")

ax.legend()
ax.set_title("Solução numérica aprox. pelo método de Euler")
ax.grid()
ax = plt.gca()
plt.show();
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 4
assert len(ax.legend().texts) == 4
```

```python
xdata = ax.lines[0].get_xdata()
assert min(xdata) == 0.0
assert max(xdata) == np.pi

ydata = [l.get_ydata() for l in ax.lines]
assert -1.1 < np.min(ydata) < -1.0
assert np.max(ydata) == 1.0
```

```python
ax = None
```

Agora, aumente o intervalo de integração para $6\pi$, e use 600 passos.

```python
ts, vs = euler(F2, 0, 6*np.pi, np.array([1.0,0.0]), nsteps=600)
ts2, vs2 = euler(F2, 5*np.pi, 6*np.pi, np.array([-1.0,0.0]), nsteps=100)
vs = np.transpose(vs)
vs2 = np.transpose(vs2)

fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(13, 5.5), gridspec_kw={'bottom': .15})

axs[0].plot(ts, vs[0])#, label="Aproximação de $\cos(t)$")
axs[0].plot(ts, vs[1])#, label="Aproximação de $-\sin(t)$")
axs[0].plot(ts, np.cos(ts), linestyle=":", color="r")#, label='$\cos(t)$ (Numpy)'
axs[0].plot(ts, -np.sin(ts), linestyle=":", color="k")# label='$-\sin(t)$ (Numpy)'

axs[0].set_title("Solução numérica aprox. pelo método de Euler")

axs[1].plot(ts2, vs2[0], label="Aproximação de $\cos(t)$")
axs[1].plot(ts2, vs2[1], label="Aproximação de $-\sin(t)$")
axs[1].plot(ts2, np.cos(ts2), label='$\cos(t)$ (Numpy)', linestyle=":", color="r")
axs[1].plot(ts2, -np.sin(ts2), label='$-\sin(t)$ (Numpy)', linestyle=":", color="k")

axs[1].set_title("Intervalo reduzido para efeito de comparação")
# axs[1].legend()

for i in range(2):
  axs[i].set_xlabel("$t$")
  axs[i].set_ylabel("$f(t)$")
  axs[i].grid()

handles, labels = axs[1].get_legend_handles_labels()
fig.legend(handles, labels, loc='lower center', ncol=len(handles))

plt.show();
```

O que aconteceu?

É possível observar que a solução numérica da EDO diverge mais da solução analítica à medida que $t$ aumenta. O plot auxiliar à direita (acima) mostra que não se trata de um problema associado aos valores específicos de $t$ envolvidos, pois, partindo-se de $t_0 = 5 \pi$, a solução, a princípio, parece tão precisa quanto em $t_0 = 0$ no caso anterior, e então vai se "descolando" da solução verdadeira com o aumento de $t$, de maneira análoga à observada anteriormente no intervalo $[0, \pi]$.

Cabe ressaltar que, nos três casos, o número de passos utilizado foi proporcional ao comprimento de cada intervalo, de forma que os "subintervalos" ao longo dos quais a função era aproximada por sua reta tangente em um extremo tinham sempre o mesmo comprimento.

Desse modo, concluímos que a origem do erro está no aumento do número de iterações realizadas pelo método de Euler. Cada iteração envolve uma aproximação de um valor de $y(t)$, que, por sua vez, é usada na iteração seguinte. Isso significa que os erros gerados pela aproximação a cada passo acumulam-se ao longo do processo, gerando uma aproximação progressivamente pior da solução.

### 6. Erros multidimensionais

Voltemos ao intervalo $[0, \pi]$.
Como se comporta o erro no caso do sistema acima, conforme o número de pontos aumenta?
Qual erro é mais importante: o da derivada ($f_2$) ou o da função original ($f_1$)?
Você vê uma razão para isso?

```python
%%time
ns = np.logspace(1,5,num=21, dtype=int)
err = [[],[]]

for n in ns:
  ts, vs = euler(F2, 0, np.pi, np.array([1.0,0.0]), nsteps=n)
  vs = np.transpose(vs)

  err[0].append(erro(np.cos,vs[0],ts))
  err[1].append(erro(lambda x: -np.sin(x),vs[1],ts))

fig, ax = plt.subplots()

ax.loglog(ns, err[0], label = "Erro de $f$", color="C1")
ax.loglog(ns, err[1], label = "Erro de $f'$", linestyle=":", color="k")
ax.set_xlabel("Número de passos")
ax.set_ylabel("Erro total")

ax.legend()
ax.set_title("Erro total da solução numérica em $[0, \pi]$")
ax.grid()

ax = plt.gca()
plt.show()
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 2
assert len(ax.legend().texts) == 2
```

```python
xdata = ax.lines[0].get_xdata()
assert min(xdata) == 1e1
assert max(xdata) == 1e5

ydata = [l.get_ydata() for l in ax.lines]
assert 1e-5 < np.min(ydata) < 5e-5
assert 1e-1 < np.max(ydata) < 5e-1
```

Comente abaixo

```python
diff_dos_erros = []
for pos, er in enumerate(err[1]):
    diff_dos_erros.append(abs(er - err[0][pos]))

plt.semilogx(ns,diff_dos_erros)
plt.grid()
plt.xlabel("Número de passos")
plt.title("Módulo da diferença entre os erros de $f$ e $f'$");
```

Analisando o gráfico **'Erro total da solução numérica'**, percebemos que o erro da $f'$ e da $f$ são bem próximos no intervalo $[0,\pi]$, sendo só um pouco diferentes no início como mostrado no gráfico acima quando o número de passos é pequeno e o erro na escala loglog decai linearmente com o número de passos.

O erro certamente depende do intervalo "de integração" da equação diferencial.
Será que a importância relativa também depende? Faça o gráfico do erro para o intervalo $[0, \pi/2]$ e conclua.

```python
%%time
ns = np.logspace(1,5,num=21, dtype=int)
err = [[],[]]
for n in ns:
  ts, vs = euler(F2, 0, np.pi/2, np.array([1.0,0.0]), nsteps=n)
  vs = np.transpose(vs)

  err[0].append(erro(np.cos,vs[0],ts))
  err[1].append(erro(lambda x: -np.sin(x),vs[1],ts))
fig, ax = plt.subplots()

ax.loglog(ns, err[0], label = "Erro de $f$" )
ax.loglog(ns, err[1], label = "Erro de $f'$" )
ax.set_xlabel("Número de passos")
ax.set_ylabel("Erro")

ax.legend();
ax.set_title("Erro total da solução numérica")
ax.grid()
```

Comente

A seguir, exceto nos gráficos em que o número de passos aparece na abscissa, nos quais a grid está em cinza, linhas cinzas são múltiplos de $\frac{\pi}{2}$ e pontilhados cinzas são os demais múltiplos de $\frac{\pi}{6}$.

```python
fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(20, 4.8))

# Erro total em intervalos diferentes, em função
# de nsteps:

ns = np.logspace(1,5,num=21, dtype=int)

err = [[[],[]], [[],[]]]

pts = [[np.pi / 2, np.pi], [np.pi / 3, 2 * np.pi / 3]]

for n in ns:
    for i in range(2):
        a, b = pts[i][0], pts[i][1]
        ts, vs = euler(F2, a, b, np.array([np.cos(a),
                        -np.sin(a)]), nsteps=n)
        vs = np.transpose(vs)

        err[i][0].append(erro(np.cos, vs[0], ts))
        err[i][1].append(erro(lambda x: -np.sin(x), vs[1], ts))

for i in range(2):
    axs[2*i].loglog(ns, err[i][0], label = "Erro de $f$")
    axs[2*i].loglog(ns, err[i][1], label = "Erro de $f'$")
    axs[2*i].set_xlabel("Número de passos")
    axs[2*i].set_ylabel("Erro total")
    axs[2*i].grid()

axs[0].set_title("Erro total em $[\pi/2, \pi]$")
axs[2].set_title("Erro total em $[\pi/3, 2\pi/3]$")

# Aproximação da solução plotada com markers:

ts, ys = euler(F2, 0, np.pi, np.array([1.0,0.0]), nsteps=100)

ys = np.transpose(ys)

axs[1].plot(ts, ys[0], linestyle="", marker=".", label="$f(t)$")
axs[1].plot(ts, ys[1], linestyle="", marker=".", label="$f'(t)$")
axs[1].set_title("Aproximação do método de Euler")

axs[1].axhline(color="k", linewidth=.5)

for k in range(3):
    axs[k].legend()
    axs[1].axvline(x=(k/2)*np.pi, color="gray", linewidth=.5)

for k in range(6):
    axs[1].axvline(x=(k/6)*np.pi, color="gray", linestyle=":", linewidth=.5)

axs[1].set_xlabel("$t$")
axs[1].set_ylabel("Aprox. da sol. em $t$");
```

Plotar a solução aproximada fornecida pelo método de Euler com marcadores em vez de uma curva contínua permite observar mais claramente onde a variação no valor de $y$ entre pontos adjacentes é maior, o que corresponde a maiores valores absolutos da derivada de cada função. Como as aproximações são feitas para valores de $t$ igualmente espaçados, de modo a manter uma distância horizontal constante entre pontos consecutivos, uma maior proximidade dos marcadores no gráfico indica uma variação de $y$ menor. (É claro que a inclinação da reta tangente pode ser mentalmente estimada através do próprio gráfico, mas quando há "buracos" isso se torna bastante evidente, facilitando a análise visual.)

Experimentando intervalos diversos, casos simples como o de $\left[\frac{\pi}{3}, \frac{2\pi}{3}\right]$, mostrado acima como exemplo, sugeriram que, quando a derivada de uma das duas funções assume, em média, valores absolutos menores num intervalo, o erro na aproximação dela é maior; caso de $f'(t) = -\sin(t)$ no exemplo.

Casos como $\left[0, \frac{\pi}{2}\right]$, $\left[\frac{\pi}{2}, \pi\right]$ e $\left[0, \pi\right]$ são mais sutis, porque os valores absolutos médios assumidos são iguais. (Todos esses três casos têm gráficos feitos acima.)

Em $\left[0, \frac{\pi}{2}\right]$, o valor absoluto da derivada de $f_2$ termina num mínimo, e o de $f_1'$, num máximo; o maior erro total é de $f_2$. Em $\left[\frac{\pi}{2}, \pi\right]$, temos o contrário, como maior erro total de $f_1$. No intervalo $\left[0, \pi\right]$, temos novamente o valor absoluto de $f_1'$ terminando num mínimo, mas o erro apresentado pelas duas funções foi igual, o que pode ter relação com o fato de as funções não serem monótonas no intervalo (comportamento mais simétrico?).

*(Continua abaixo dos gráficos a seguir)*

```python
ts, vs = euler(F2, 0, np.pi, np.array([1.0,0.0]), nsteps=100)
ts2, vs2 = euler(F2, 0, 6*np.pi, np.array([1.0,0.0]), nsteps=600)
vs = np.transpose(vs)
vs2 = np.transpose(vs2)

T = [ts, ts2]
V = [vs, vs2]
```

```python
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(13, 4.8))

axs[0].plot(T[1], np.abs(np.cos(T[1]) - V[1][0]),
               label="$f$")
axs[0].plot(T[1], np.abs(-np.sin(T[1]) - V[1][1]),
               label="$f'$")
axs[0].set_ylabel("Erro")

avgs = [np.zeros(T[1].shape), np.zeros(T[1].shape)]

for i, t in enumerate(T[1]):
    avgs[0][i] = np.average(np.abs(np.cos(T[1]) - V[1][0])[:i+1])
    avgs[1][i] = np.average(np.abs(-np.sin(T[1]) - V[1][1])[:i+1])

axs[0].plot(T[1], avgs[0], color="C0", linestyle=":")
axs[0].plot(T[1], avgs[1], color="C1", linestyle=":")

axs[0].set_title("Erro absoluto ponto a ponto;\nerro total acumulado pontilhado");

axs[1].loglog(T[1], avgs[0], label="$f$")
axs[1].loglog(T[1], avgs[1], label="$f'$")

axs[1].set_ylabel("Erro total")
axs[1].set_title("Erro total em $[0, t]$");

for i in range(2):
    axs[i].legend()
    axs[i].set_xlabel("$t$")
    for k in range(12):
        axs[i].axvline(x=(k/2)*np.pi, color="gray", linewidth=.5)

    for k in range(36):
        axs[i].axvline(x=(k/6)*np.pi, color="gray", linestyle=":", linewidth=.5)
```

No gráfico acima, à esquerda, vemos que, exceto para valores de $t$ muito pequenos, os máximos do erro absoluto de $f_1$ e mínimos do de $f_2$ ocorrem em múltiplos pares de $\frac{\pi}{2}$, e vice-versa para os múltiplos ímpares. Os pontos de máximo correspondem, para cada função $g$, aos máximos de $|g| = |g''|$ e mínimos de $|g'|$.

Soma-se a isso a observação, nos gráficos que contrastam as funções do Numpy com as aproximações do método de Euler, de que uma aproximação parece "descolar-se" mais da solução "exata" nos pontos onde seu gráfico é mais curvo.

Essas observações sugeriram uma relação entre o erro da aproximação e a curvatura dos gráficos de $f_1$ e $f_2$. A curvatura do gráfico de uma função  $g$ é dada por, em cada ponto $t$:

$$\kappa (t) = \frac{\left| g''(t) \right|}{ \left[ 1 + \left( g'(t) \right)^2 \right]^\frac{3}{2} } $$

Como estamos tratando de funções trigonométricas, vale ainda que:

$$\kappa (t) = \frac{ \sqrt{1 - \left( g'(t) \right)^2} }{ \left[ 1 + \left( g'(t) \right)^2 \right]^\frac{3}{2} } $$

Sendo, neste caso, $0 \leq \left( g'(t) \right)^2 \leq 1$, temos $0 \leq \kappa (t) \leq 1$, com máximos onde $g'(t) = 0$, o que corresponde aos mínimos de $|g'(t)|$.

Uma vez que, no método de Euler, a função é aproximada, em cada subintervalo, pela reta tangente ao seu gráfico, parece razoável que, de fato, essa aproximação seja tanto pior em cada ponto quanto mais a função se afastar localmente de sua reta tangente, i.e., quanto maior sua curvatura $\kappa (t)$.

Já havia sido observado num item anterior que, independentemente da curvatura, o erro aumenta progressivamente à medida que o número de iterações realizadas pelo método de Euler aumenta. Sendo assim, é possível que um fator envolvido na predominância dos erros de $f_1$ e $f_2$ em $\left[\frac{\pi}{2}, \pi\right]$  e $\left[0, \frac{\pi}{2}\right]$, respectivamente, seja o fato de o gráfico de cada uma das funções ter assumido suas curvaturas máxima e mínima uma única vez em cada intervalo, mas, em cada um deles, a de maior erro ter atingido esse máximo *por último*. O efeito combinado do erro devido à curvatura e ao índice da iteração simultaneamente máximos pode ter gerado a predominância do erro total da função em questão. Essa ideia também é reforçada pelo gráfico comparativo entre as soluções "exatas" e aproximadas.

Não testamos exaustivamente as variações possíveis desse tipo de intervalo, mas o comportamento se manteve com pequenos deslocamentos que preservavam o comprimento.

No intervalo $\left[0, \pi\right]$, a não bijetividade de $f_1$ e $f_2$ parece ter gerado um efeito de compensação: $f_1$ tem dois máximos de curvatura, um na iteração inicial e outro na final, ao passo que $f_2$ atinge um único máximo de curvatura, na posição média entre os de $f_1$.

Ainda que não se tratem de argumentos totalmente conclusivos ou explicativos, acreditamos que sejam esses os fatores envolvidos na predominância ou não do erro de $f_1$ ou $f_2$ em diferentes intervalos.

## Parte 2: Métodos de maior ordem para EDOs

### Equações diferenciais e Integrais

Calcular a integral $\int_a^b h(t) \, dt$ é a mesma coisa que resolver a equação diferencial
$$ \begin{cases}
  y'(t) & = h(t) \\
  f(a)  & = 0,
\end{cases} $$
pelo Teorema Fundamental do Cálculo.
Portanto, é possível implementar uma função `int_rect` usando a função `euler`.

Isso é verdade para vários métodos de integração: muitas vezes existe um método de Equações Diferenciais cujo caso particular é um método de integração quando a ED é "trivial"
(ou seja, quando a fórmula $F$ para $y'(t)$ não depende de $y(t)$).

### O método do Trapézio para EDOs: uma equação implícita

Inversamente, podemos usar métodos de integração e "generalizar" para usar em EDOs.
Por exemplo, podemos usar a aproximação do trapézio:
$$ y(h) = y(0) +  \int_0^h y'(t) \, dt \sim h \cdot \frac{y'(0) + y'(h)}{2}.$$

Substituindo a EDO $f'(t) = F(t, f(t))$, isto dá uma equação **implícita** para calcular $y(h)$.

### Trapézio 1: Equação implícita

Escreva a equação implícita que a aproximação $y_1$ de $y(h)$ satisfaz, em função de $y_0$, $h$, $F$, $\ldots$.

A aproximação para $y(h)$ dada acima é a fórmula para integração pelo método do trapézio, de forma que tivemos a impressão de que a expressão levava em conta a condição $f(a) = y(0) = 0$ dada acima.

No caso mais geral em que não se está partindo de $t_0 = 0$, teremos:

$$ y (t_{i+1}) =  y (t_i) + \int_{t_i}^{t_{i+1}} y'(t) \, dt \approx y (t_i) + h \cdot \frac{y'(t_{i+1}) + y'(t_i)}{2} = $$
$$ = y (t_i) + h \cdot \frac{F(t_{i+1}, y (t_{i+1})) + F(t_{i}, y (t_i))}{2}$$

### Trapézio 2: resolvendo a equação implícita de 1 passo

Escreva uma função que dá o passo do método do trapézio.

Obs: O método da secante precisa de dois pontos para iniciar.
Que valor de $y$ deve estar próximo à solução?
Se você tiver que "chutar" outro valor, este provavelmente estará mais próximo ou mais distante?
Qual deles você deve usar como primeiro e qual como segundo valor na secante?

```python
def secante(f,a,b, xtol=1e-8, maxiter=100):
    """ Método da secante para a função  f  no intervalo  [a,b].

        Retorna um número  z  e as listas de extremidades esquerda e direita produzidas ao longo do algoritmo,
        que pára quando o último passo é menor do que  xtol, ou depois de  maxiter  iterações.
    """
    fa = f(a)
    fb = f(b)

    def passo(a, b, fa, fb):
        beta = (a*fb - fa*b)/(fb - fa)
        return beta

    # Estrutura recursiva básica
    def dividir(a,b,fa,fb, niters):

        s = passo(a, b, fa, fb)
        if (
            niters == maxiter or
            abs(a - b) < xtol or
            abs(b - s) < xtol
        ):
            return s, [a], [b]

        fs = f(s)
        if fs == 0:
            return s, [a], [b]

        z,l,r = dividir(b, s, fb, fs, niters + 1)
        return z, [a] + l, [b] + r

    z,l,r = dividir(a,b,fa,fb, 0)
    return z, np.array(l), np.array(r)
```

```python
def trapezoidal_step_sec(F, tcurr, ycurr, h):
    """Resolve a equação implícita do método trapezoidal pelo método da secante, partindo de (tcurr,ycurr),
    retornando uma aproximação de  y  em  tcurr+h."""
    tnext = tcurr+h
    def f(y):
      return h*(F(tcurr,ycurr)+ F(tnext,y))/2  + ycurr - y

    if F(tcurr,ycurr):
      y2 = ycurr + h*F(tcurr, ycurr)
    else:
      y2 = ycurr + h
    y,_,_ = secante(f, ycurr, y2, xtol=1e-9, maxiter=100)
    return y
```

### Alguns testes simples

```python
def G(t,y):
    return t - np.cos(y)

ynext = trapezoidal_step_sec(G, 0, 1, 0.1)
assert abs(ynext - 0.9488541465905801) < 1e-14
```

```python
ynext = trapezoidal_step_sec(G, 0, 1, 0.3)
assert abs(ynext - 0.8668725901552984) < 1e-14
```

### Trapézio 3: Resolvendo a EDO

Agora, incorporamos o passo do trapézio numa função `trapezoidal_ode`, análoga à `euler`,
que resolve a EDO $y' = F(t, y(t))$ pelo método do trapézio.

```python
def trapezoidal_ode(F, t0, tf, y0, nsteps=100):
    """Calcula uma solução aproximada da equação  y' = F(t,y)  pelo método do trapézio,
    dividindo o intervalo  [t0, tf]  em  nsteps  passos,
    e usando o método da secante para resolver a equação implícita."""
    ts, h = np.linspace(t0,tf,num=nsteps + 1,retstep=True)
    ys = np.zeros(ts.shape)
    ys[0] = y0
    for i, tcurr in enumerate(ts[:-1]):
      ys[i + 1] = trapezoidal_step_sec(F, tcurr, ys[i], h)
    return ts, np.array(ys)
```

```python
def F1(t,y):
    return y

t_100, y_100 = trapezoidal_ode(F1, 0, 1, 1)
assert len(t_100) == 101
assert t_100[0] == 0
assert t_100[-1] == 1
```

```python
t_100, y_100 = trapezoidal_ode(F1, 0, 1, 1)
assert len(y_100) == 101
assert y_100[0] == 1
assert( abs(y_100[-1] - 2.7183044812417965) < 5e-15 )
```

```python
def F_dificil(t, y):
    return t*np.sin(y) - t**2*np.cos(y)

_, y_dificil = trapezoidal_ode(F_dificil, 0, 2, 1, nsteps=100)
assert( abs(y_dificil[-1] - 3.587544094834753) < 5e-15 )
```

```python
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection='3d')

uttt = np.linspace(0, 2, num=1000)
yyys = np.linspace(0, 4, num=1000)

U, Y = np.meshgrid(uttt, yyys)

Z = F_dificil(U, Y)

ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_zlabel('$F$')

ax.set_title("Bônus: a cara da F_dificil")

ax.plot_surface(U, Y, Z);
```

Tivemos curiosidade de plotar a função. No Colab as quinas da figura ficaram meio cortadas e não conseguimos arrumar, no Jupyter e no Python regular não; talvez apareçam um pouco cortadas por aí.

```python
t_100, y_100 = trapezoidal_ode(F1, 0, 1, 1)
assert np.allclose(y_100, np.exp(t_100))
assert 1e-5 < np.max(y_100 - np.exp(t_100)) < 5e-5

t_1000, y_1000 = trapezoidal_ode(F1, 0, 1, 1, nsteps=1000)
assert np.allclose(y_1000, np.exp(t_1000), rtol=1e-7)
assert 1e-7 < np.max(y_1000 - np.exp(t_1000)) < 5e-7
```

O que a caixa acima está testando?

A equação diferencial $f' = f$ com condição incial $f(0) = 1$ tem como solução $f(t) = e^t$. Por isso, o primeiro e o terceiro asserts verificam se os valores de $y$ retornados de fato estão "suficientemente" próximos de $e^t$ para $t$ correspondente. O critério de "proximidade suficiente" é mais rígido (menor tolerância) com um maior número de passos (terceiro assert). Da documentação do Numpy, a verificação feita por `np.allclose(a, b)` é (elemento a elemento)

`absolute(a - b) <= (atol + rtol * absolute(b))`,

onde os valores padrão dos parâmetros opcionais são `atol = 1e-8` e `rtol = 1e-5`. Ou seja: para cada $y$, é testado se

$$ \left| y - e^t \right| \leq \texttt{atol} + \texttt{rtol} \left| e^t \right| $$

Além dessa verificação, o segundo e quarto asserts também verificam se a *maior* dessas diferenças $y - e^t$ está dentro de um intervalo fornecido, que novamente envolve valores menores no caso com número de passos maior. Algumas observações curiosas são que: nesse caso, não é usado o valor absoluto, ou seja, é explicitamente exigido que $y > e^t$; além disso, também é fornecido um limite inferior para esse erro, de modo que a solução não poderia ser precisa "demais".

Posto que, para $t \in [0, 1]$, $e^t \leq e < 2,72$, logo $\texttt{rtol} \cdot e^t + \texttt{atol} < 2,72 \cdot \texttt{rtol} + 10^{-8} < 5 \cdot \texttt{rtol}$, em tese, os asserts com `np.allclose` já assegurariam a validade da cota superior em cada caso, mas não a positividade de $y - e^t$, nem a cota inferior.

### O método do ponto médio para integrais

Esse é um método de "duplo cálculo", e explícito:

- Andamos metade do tempo $h$, usando um passo "de Euler"
- Recalculamos o passo de Euler, mas usamos a derivada "no meio do caminho",
  que é uma aproximação melhor (porque simétrica).

Em fórmulas:
- Passo explícito, $m_i = y_i + F(t_i, y_i) \cdot h/2$
- 2º passo explícito: $y_{i+1} = y_i + F(t_i + h/2, m_i) \cdot h$

### Ponto médio 1:

Escreva uma função que dá o novo ponto usando a regra do ponto médio

```python
def midpoint_step(F, t, y, h):
  """ Recebe como parametros F(t,y), t, y e h, e retorna y + F(t+h/2,m)*h , m = y + h*F(t,y)/2 """
  m = y + F(t,y)*h/2
  return y+ F(t+h/2, m)*h
```

```python
def G(t,y):
    return t - np.cos(y)

ynext = midpoint_step(G, 0, 1, 0.1)
assert abs(ynext - 0.9487165172210511) < 1e-14
```

```python
ynext = midpoint_step(G, 0, 1, 0.3)
assert abs(ynext - 0.8630045486909678) < 1e-14
```

### Ponto médio 2: EDO

E agora, incorpore este passo num método de resolução de EDOs

```python
def midpoint_ode(F, t0, tf, y0, nsteps=100):
    """Calcula uma solução aproximada da equação y' = F(t,y) no intervalo  [t0, tf]  usando  nsteps  passos,
    usando o passo explícito no ponto médio."""
    ts, h = np.linspace(t0, tf, num=nsteps+1, retstep=True)
    ys = [y0]
    for t in ts[:-1]:
      yt = ys[-1]
      ys.append(midpoint_step(F,t,yt,h))

    return ts, np.array(ys)
```

```python
def F1(t,y):
    return y

t_100, y_100 = midpoint_ode(F1, 0, 1, 1)
assert len(t_100) == 101
assert t_100[0] == 0
assert t_100[-1] == 1
```

```python
t_100, y_100 = midpoint_ode(F1, 0, 1, 1)
assert len(y_100) == 101
assert y_100[0] == 1
assert( abs(y_100[-1] - 2.718236862559957) < 5e-15 )
```

```python
def F_dificil(t, y):
    return t*np.sin(y) - t**2*np.cos(y)

_, y_dificil = midpoint_ode(F_dificil, 0, 2, 1, nsteps=100)
assert( abs(y_dificil[-1] - 3.5874427140529868) < 5e-15 )
```

```python
t_100, y_100 = midpoint_ode(F1, 0, 1, 1)
assert np.allclose(y_100, np.exp(t_100), rtol=2e-5)
assert 2e-5 < np.max(abs(y_100 - np.exp(t_100))) < 10e-5

t_1000, y_1000 = midpoint_ode(F1, 0, 1, 1, nsteps=1000)
assert np.allclose(y_1000, np.exp(t_1000), rtol=2e-7)
assert 2e-7 < np.max(abs(y_1000 - np.exp(t_1000))) < 10e-7
```

### Gráficos

### Gráficos 1: Solução de EDOs

Resolva a EDO

$$y' = t - \cos(y)$$

com condição inicial $y(0) = 1$ no intervalo $[0,5]$, usando 30 pontos,
pelos 3 métodos (Euler, Trapézio, ponto médio).
Faça o gráfico das três soluções.
Não esqueça do título e da legenda!

```python
def F_g1(t,y):
  return t-np.cos(y)

t0,tf,y0, n = 0,5,1,30
ts,y_e = euler(F_g1,t0,tf,y0,nsteps=n)
_,y_t = trapezoidal_ode(F_g1,t0,tf,y0,nsteps=n)
_,y_m = midpoint_ode(F_g1,t0,tf,y0,nsteps=n)

fig, ax = plt.subplots()

ax.plot(ts,y_e, label = "Euler" )
ax.plot(ts,y_t,label = "Trapézio" )
ax.plot(ts,y_m,label = "Ponto médio", linestyle= ":", color = "k"  )

ax.set_xlabel("$t$")
ax.set_ylabel("Solução numérica")

ax.legend();
ax.set_title("Comparação de métodos")
ax.grid()

ax = plt.gca()
plt.show()
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 3
assert len(ax.legend().texts) == 3
```

```python
ax = None
```

Esta EDO não tem "solução analítica simples".
Como você faria para saber qual das curvas está "mais correta"?
Explique sua ideia abaixo, e faça contas / gráficos na caixa seguinte

Uma primeira observação é que as soluções dos métodos do trapézio e do ponto médio quase coincidiram — ainda que o método do ponto médio tenha sido adaptado do método de Euler, o que poderia gerar a expectativa de que fossem mais parecidos —, de modo que, de início, julgamos provável que essas soluções estivessem mais próximas da solução verdadeira.

A única informação "analítica" que temos sobre a função é sua derivada, de modo que uma ideia que pareceu plausível foi usar a derivação numérica da semana 4 para derivar a "caixa preta" retornada pelos três métodos e comparar com a derivada $y' = t - \cos(y)$. No entanto, mesmo essa expressão para $y'$ é dependente de $y$, de modo que sua utilidade é limitada por requerir o uso da própria solução numérica.

A aproximação numérica feita para a derivada a partir da solução obtida foi, para o array `y[1:-1]`:

$$ y'(t_i, y_i) \approx \frac{\texttt{y[i+1]} - \texttt{y[i-1]}}{2h} = \frac{\texttt{y[i+1]} - \texttt{y[i-1]}}{\texttt{ts[i+1]} - \texttt{ts[i-1]}}$$

Para reduzir o número de operações, foi usado o valor de $h$ retornado pela função `linspace` do Numpy, em lugar de calcular `ts[i+1] - ts[i-1]` a cada vez.

Em cada caso, para cálculo do erro, foi substituída na expressão dada para $y'$ a aproximação de $y$ produzida pelo método correspondente.

O erro de aproximação introduzido pela derivada central é conhecido (vide lema), mas envolve a terceira derivada de $y$, cujo cálculo incorporaria novamente os valores aproximados de $y$ e uma boa quantidade de operações. Desse modo, para evitar introduzir ruído que distorceria a comparação, foi considerado o resultado "não corrigido" da derivação numérica, sob a hipótese de que o erro introduzido por esse processo deveria ser similar para todos os métodos.

**Lema.** Seja $f: [a, b] \to \mathbb{R}$ com terceira derivada definida em $(a, b)$. O erro de aproximação introduzido pela derivação numérica com o método da derivada central é

$$ \frac{1}{3!}f'''(x)h^2 + \mathcal{O}(h^3). $$

\
*Dem.:* A fórmula para aproximação da derivada central de uma função é

$$ f' \approx \frac{f(x+h) - f(x-h)}{2h}, $$

para algum $h \neq 0$.

Para uma função com terceira derivada definida, podemos aproximar:

$$
\begin{aligned}
\frac{f(x+h) - f(x-h)}{2h} = \: & \frac{1}{2h} \left[ f(x) + f'(x)h + \frac{1}{2} f''(x)h^2 + \frac{1}{3!}f'''(x)h^3 + \mathcal{O}(h^4) \right. \\
& \left. - \left( f(x) - f'(x)h + \frac{1}{2} f''(x)h^2 - \frac{1}{3!}f'''(x)h^3 + \mathcal{O}(h^4)\right)\right] = \\
=& \: f'(x) + \frac{1}{3!}f'''(x)h^2 + \mathcal{O}(h^3)
\end{aligned}
$$

Assim,

$$ f'(x) - \frac{f(x+h) - f(x-h)}{2h} = \frac{1}{3!}f'''(x)h^2 + \mathcal{O}(h^3). $$

```python
a, b, y0, n = 0, 5, 1, 1000

def F_g1(t,y):
  return t-np.cos(y)

ts, h = np.linspace(t0, tf, num=n+1, retstep=True)

h = 2 * h  # para a derivada central

methods_3 = [euler, trapezoidal_ode, midpoint_ode]
names = [f.__name__ for f in methods_3]

ys = {f.__name__: f(F_g1, a, b, y0, nsteps=n)[1] for f in methods_3}
dys = {name: np.zeros(ys[name].shape) for name in names}

for name in names:
    for i, y in enumerate(ys[name][1:-1]):
        dys[name][i] = (ys[name][i+1] - ys[name][i-1]) / h
        # derivada central

errs = {name: np.abs(F_g1(ts, ys[name])[1:-1] - \
                    dys[name][1:-1]) for name in names}

fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(20, 4.8))

for i, name in enumerate(names):
    axs[i].semilogy(ts[1:-1], errs[name], label=name)
    axs[i].legend()
    axs[i].set_xlabel("t")
    axs[i].set_ylabel("Erro")

fig.suptitle("Erro absoluto da derivada central em relação à expressão para $y'(t, y)$");
```

```python
fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(20, 4.8))

for i, name in enumerate(names):
    axs[i].semilogy(ys[name][1:-1], errs[name], label=name)
    axs[i].legend()
    axs[i].set_xlabel("y")
    axs[i].set_ylabel("Erro")

fig.suptitle("Erro absoluto da derivada central em relação à expressão para $y'(t, y)$");
```

```python
fig = plt.figure(figsize = (20, 6))

for i, name in enumerate(names):
    nplot = 131 + i
    ax = fig.add_subplot(nplot, projection='3d')
    ax.plot(ts[1:-1], ys[name][1:-1], np.log(errs[name]), label=name)
    # Parece que a escala logarítimica não funciona muito
    # bem para gráficos 3D no Matplotlib, então o log foi
    # calculado
    ax.legend()
    ax.set_xlabel('$t$')
    ax.set_ylabel('$y$')
    ax.set_zlabel("$y'$")

fig.suptitle("Erro absoluto da derivada central em relação à expressão para $y'(t, y)$");
```

```python
print("Erro total:\n")

for name in names:
    errtot = np.average(errs[name])
    print(f"{name:>15}: {errtot}")

print("\nErro total (excluindo o último ponto):\n")

for name in names:
    errtot = np.average(errs[name][:-1])
    print(f"{name:>15}: {errtot}")
```

    Erro total:

              euler: 0.008657115038128844
    trapezoidal_ode: 0.004035016112636929
       midpoint_ode: 0.004016039140643551

    Erro total (excluindo o último ponto):

              euler: 0.004661407861298137
    trapezoidal_ode: 3.528519084788828e-05
       midpoint_ode: 1.6287622094803458e-05

Portanto, parece que o método do ponto médio retornou um erro total marginalmente menor, próximo daquele retornado pelo método do trapézio. Além disso, de forma condizente com nossa observação inicial a partir do gráfico pedido, o método de Euler parece bastante menos preciso do que os demais.

O cálculo sem o último valor de $y'$ foi incluído pois os gráficos sugerem a ocorrência de algo anômalo nesse ponto. As funções de cálculo do erro total e da derivada central (da semana 4) não foram usadas diretamente por conta do formato particular dos resultados.

### Gráficos 2: Velocidade de convergência

```python
methods_3 = [euler, trapezoidal_ode, midpoint_ode]
```

Faça uma função que desenha o gráfico dos erros de solução de uma EDO, dados
- a função $F$ da EDO
- o intervalo $[a,b]$
- a condição inicial $y_0$
- a solução analítica `ysol`
- o número de pontos $n$

```python
def graph_err(F, a,b, y0, ysol, n, methods=methods_3):
  """
  Plota as curvas dos erros de solução de uma EDO do método do Trapézio, de Euler
  e do Ponto médio. Como parâmetros ela recebe a função F da EDO, um intervalo
  [a,b], a condição inicial y0, a solução analítica Ysol e o número de pontos
  que se deseja plotar.
  """
  ts = methods_3[0](F, a, b, y0, nsteps=n)[0]

  err = {f.__name__: f(F, a, b, y0, nsteps=n)[1] - ysol(ts) for f in methods_3}

  fig, ax = plt.subplots()

  for n, e in err.items():
    ax.plot(ts, e, label=n)

  ax.set_xlabel("$t$")
  ax.set_ylabel("Erro")

  ax.legend();
  ax.set_title("Comparação entre métodos")
  plt.legend(title="Método")
```

Uma EDO conhecida, para comparar o erro: $$y' = -y.$$

```python
def G2(t,y): return -y
def ysol2(t): return np.exp(-t)

graph_err(G2, 0,2, 1, ysol2, 30)
plt.yscale('symlog', linthresh=1e-4) # Com "symlog", não use valores absolutos
plt.grid()
ax = plt.gca()
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 3
assert len(ax.legend().texts) == 3
```

```python
xdata = ax.lines[0].get_xdata()
assert min(xdata) == 0.0
assert max(xdata) == 2.0

ydata = [l.get_ydata() for l in ax.lines]
assert -2e-2 < np.min(ydata) < -1e-2
assert  1e-4 < np.max(ydata) <  4e-4
```

### Gráficos 3: Erro em função de $n$

Usando a função `erro` definida anteriormente, faça o gráfico do erro, para os 3 métodos,
em função de $n$, para a EDO $y' = y$, no intervalo $[0,2]$.

```python
%%time
ns = np.logspace(1,5,num=21, dtype=int)

def G3(t,y): return y
def ysol3(t): return np.exp(t)

errs = {f.__name__: np.zeros(ns.shape) for f in methods_3}

a, b, y0 = 0, 2, 1

for i, n in enumerate(ns):
  ts = methods_3[0](G3, a, b, y0, nsteps=n)[0]
  err = {f.__name__: erro(ysol3, f(G3, a, b, y0, nsteps=n)[1], ts)
           for f in methods_3}
  for k, e in err.items():
    errs[k][i] = e

fig, ax = plt.subplots()

for f, es in errs.items():
  ax.loglog(ns, es, label=f)

ax.set_xlabel("$n$")
ax.set_ylabel("Erro total")

ax.grid()

ax.set_title("Comparação entre métodos")
ax.legend(title="Método");
```

```python
assert ax.title.get_text() != ""
assert len(ax.lines) == 3
assert len(ax.legend().texts) == 3
```

O que você observa?

Que o método de **Euler** foi a "pior" aproximação dos três métodos implementados, pois é a função que menos convergiu e ainda teve o maior erro mesmo com a variação do **nsteps**, não só para a **EDO:** $y' = y$, mas também para outras funções como a $y' = -y$. Visualmente, na escala **loglog**, erro dos três métodos têm o comportamento próximo ao decaimento linear e as inclinações das "retas" **midpoint_ode** e da **trapezoidal_ode** são aproximadamente o dobro da reta de **Euler**, ou seja, o erro decai duas vezes mais rápido para esses métodos na comparação com **Euler**. Além disso, também é perceptível no gráfico que o comportamento dos erros **midpoint_ode** e da **trapezoidal_ode** são bem semelhantes, o que já foi notado no **Gráficos 2: Velocidade de convergência** e **Gráficos 1: Solução de EDOs**.