## 8. Nonlinear ensemble filtering Lorenz 63 with adaptive Hermite map

In this notebook, we are interested in the sequential inference

References:

[1] Evensen, G., 1994. Sequential data assimilation with a nonlinear quasi‐geostrophic model using Monte Carlo methods to forecast error statistics. Journal of Geophysical Research: Oceans, 99(C5), pp.10143-10162.

[2] Asch, M., Bocquet, M. and Nodet, M., 2016. Data assimilation: methods, algorithms, and applications. Society for Industrial and Applied Mathematics.

[3] Bishop, C.H., Etherton, B.J. and Majumdar, S.J., 2001. Adaptive sampling with the ensemble transform Kalman filter. Part I: Theoretical aspects. Monthly weather review, 129(3), pp.420-436.

[4] Lorenz, E.N., 1963. Deterministic nonperiodic flow. Journal of atmospheric sciences, 20(2), pp.130-141.

[5] Spantini, A., Baptista, R. and Marzouk, Y., 2019. Coupling techniques for nonlinear ensemble filtering. arXiv preprint arXiv:1907.00389.

[6] Tang, T., & Zhou, T. (2014). On discrete least-squares projection in unbounded domain with random evaluations and its application to parametric uncertainty quantification. SIAM Journal on Scientific Computing, 36(5), A2272-A2295.

### The basic steps
To carry out sequential inference in `AdaptiveTransportMap`, we need to carry out a few basic steps:
* **Specify the problem**: Define the state-space model: initial condition, dynamical and observation models (including process and observation noise)
* **Specify the inflation parameters**: Determine the levels of covariance inflation to properly balance the dynamical system and the observations from the truth system
* **Specify the filter**: Choose the ensemble filter to assimilate the observations in the state estimate
* **Perform the sequential inference**: Perform the sequential inference

We will go through all of these here.

```julia
using Revise
using LinearAlgebra
using AdaptiveTransportMap
using Statistics
using Distributions
using OrdinaryDiffEq
```

```julia
using DelimitedFiles
```

Load some packages to make nice figures

```julia
using Plots
default(tickfont = font("CMU Serif", 9),
        titlefont = font("CMU Serif", 14),
        guidefont = font("CMU Serif", 12),
        legendfont = font("CMU Serif", 10),
        grid = false)
# pyplot()

using LaTeXStrings
# PyPlot.rc("text", usetex = "true")
# PyPlot.rc("font", family = "CMU Serif")
# gr()
using ColorSchemes
```

The Lorenz-63  model is a three dimensional system that models the atmospheric convection [4]. This system is a classical benchmark problem in data assimilation. The state $\boldsymbol{x} = (x_1, x_2, x_3)$ is governed by the following set of ordinary differential equations:

\begin{equation}
\begin{aligned}
&\frac{\mathrm{d} x_1}{\mathrm{d} t}=\sigma(x_2-x_1)\\
&\frac{\mathrm{d} x_2}{\mathrm{d} t}=x_1(\rho-x_2)-x_2\\
&\frac{\mathrm{d} x_3}{\mathrm{d} t}=x_1 x_2-\beta x_3,
\end{aligned}
\end{equation}

where $\sigma = 10, \beta = 8/3, \rho = 28$. For these values, the system is chaotic and behaves like a strange attractor. We integrate this system of ODEs with time step $\Delta t_{dyn} = 0.05$. The state is fully observed $h(t,\boldsymbol{x}) = \boldsymbol{x}$ with $\Delta t_{obs}=0.1$. The initial distribution $\pi_{\mathsf{X}_0}$ is the standard Gaussian. The process noise is Gaussian with zero mean and covariance $10^{-4}\boldsymbol{I}_3$. The measurement noise has a Gaussian distribution with zero mean and covariance $\theta^2\boldsymbol{I}_3$ where $\theta^2 = 4.0$.

### Simple twin-experiment

Define the dimension of the state and observation vectors

```julia
Nx = 3
Ny = 3
```

    3

Define the time steps $\Delta t_{dyn}, \Delta t_{obs}$  of the dynamical and observation models. Observations from the truth are assimilated every $\Delta t_{obs}$.

```julia
Δtdyn = 0.05
Δtobs = 0.2
```

    0.2

Define the time span of interest

```julia
t0 = 0.0
tf = 100.0
Tf = ceil(Int64, (tf-t0)/Δtobs)
```

    500

 Define the distribution for the initial condition $\pi_{\mathsf{X}_0}$

```julia
π0 = MvNormal(zeros(Nx), Matrix(1.0*I, Nx, Nx))
```

    FullNormal(
    dim: 3
    μ: [0.0, 0.0, 0.0]
    Σ: [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]
    )

We construct the state-space representation `F` of the system composed of the deterministic part of the dynamical and observation models.

The dynamical model is provided by the right hand side of the ODE to solve. For a system of ODEs, we will prefer an in-place syntax `f(du, u, p, t)`, where `p` are parameters of the model.
We rely on `OrdinaryDiffEq` to integrate the dynamical system with the Tsitouras 5/4 Runge-Kutta method adaptive time marching.

We assume that the state is fully observable, i.e. $h(x, t) = x$.

```julia
h(x, t) = x
F = StateSpace(lorenz63!, h)
```

    StateSpace(AdaptiveTransportMap.lorenz63!, h)

Define the additive inflation for the dynamical and observation models

```julia
### Process and observation noise
σx = 1e-5
σy = 2.0

ϵx = AdditiveInflation(Nx, zeros(Nx), σx)
ϵy = AdditiveInflation(Ny, zeros(Ny), σy)
```

    AdditiveInflation(3, [0.0, 0.0, 0.0], [4.0 0.0 0.0; 0.0 4.0 0.0; 0.0 0.0 4.0], [2.0 0.0 0.0; 0.0 2.0 0.0; 0.0 0.0 2.0])

```julia
ϵx.Σ
```

    3×3 Diagonal{Float64,Array{Float64,1}}:
     1.0e-10   ⋅        ⋅
      ⋅       1.0e-10   ⋅
      ⋅        ⋅       1.0e-10

```julia
model = Model(Nx, Ny, Δtdyn, Δtobs, ϵx, ϵy, π0, 0, 0, 0, F);
```

To perform the nonlinear ensemble filtering, we first need to estimate the transport map $\boldsymbol{S}^{\boldsymbol{\mathcal{X}}}$.

In this notebook, we are going to assume that the basis of features does not change over time, but solely the coefficients $c_{\boldsymbol{\alpha}}$ of the expansion.

To estimate the map, we generate joint samples $(\boldsymbol{y}^i, \boldsymbol{x}^i), \; i = 1, \ldots, N_e$ where $\{\boldsymbol{x}^i\}$ are i.i.d. samples from pushforward of the standard Gaussian distribution by the flow of the Lorenz-63 system.

```julia
# Time span
tspan = (0.0, 100.0)
prob = ODEProblem(lorenz63!, zeros(Nx), tspan)
```

    ODEProblem with uType Array{Float64,1} and tType Float64. In-place: true
    timespan: (0.0, 100.0)
    u0: 3-element Array{Float64,1}:
     0.0
     0.0
     0.0

Set initial condition of the true system

```julia
x0 = rand(model.π0);
```

```julia
data = generate_lorenz63(model, x0, Tf);
```

Initialize the ensemble matrix `X` $\in \mathbb{R}^{(N_y + N_x) \times N_e}$.

```julia
# Ensemble size
Ne = 500

X0 = zeros(model.Ny + model.Nx, Ne)

# Generate the initial conditions for the state.
viewstate(X0, model.Ny, model.Nx) .= rand(model.π0, Ne)
```

    3×500 view(::Array{Float64,2}, 4:6, :) with eltype Float64:
     1.03036     0.118899   0.72838    …   2.29364   -0.533393   -0.359237
     0.0280902  -0.805842  -0.206772      -1.46652   -0.0469029  -0.500246
     0.289807    1.30904    0.0264476     -0.350472   0.312731    1.61539

Use the stochastic ensemble Kalman filter for the spin-up phase. There is no reason to use the stochastic map filter over the first cycles, as the performance of the inference is determined by the quality of the ensemble, not the quality of the filter.

```julia
enkf = StochEnKF(x->x, ϵy, Δtdyn, Δtobs)
```

    Stochastic EnKF  with filtered = false

```julia
Xenkf = seqassim(F, data, Tf, model.ϵx, enkf, deepcopy(X0), model.Ny, model.Nx, t0);
```

    Progress: 100%|█████████████████████████████████████████| Time: 0:00:09

```julia
nb = 1
ne = Tf
Δ = 1
plt = plot(layout = grid(3,1), xlim = (-Inf, Inf), ylim = (-Inf, Inf), xlabel = L"t",
           size = (600, 800))

for i =1:3
    plot!(plt[i,1], data.tt[nb:Δ:ne], data.xt[i,nb:Δ:ne], linewidth =  3, color = :teal,
          ylabel = latexstring("x_"*string(i)),
         legend = (i == 1), label = "True")
    plot!(plt[i,1], data.tt[nb:Δ:ne], mean_hist(Xenkf)[i,1+nb:Δ:1+ne], linewidth = 3, grid = false,
          color = :orangered2, linestyle = :dash, label = "sEnKF")
    scatter!(plt[i,1], data.tt[nb:Δ:ne], data.yt[i,nb:Δ:ne], linewidth = 3, color = :grey,
          markersize = 5, alpha = 0.5, label  = "Observation")
end

plt
```

We assess the performance of the different filters with the root-mean-squared error defined at the assimilation step $k$ as $RMSE_k = ||\overline{\boldsymbol{x}}_k - \boldsymbol{x}^\star_k||_2/\sqrt{N_x}$, where $\overline{\boldsymbol{x}}_k$ is the sample mean, and $\boldsymbol{x}^\star_k$ is the true state. Note: we only use the samples after spin-up phase of `Tspin` steps, see Spantini et al. [5].

```julia
tspin = 50.0
Tspin = ceil(Int64, (tspin-t0)/Δtobs)
```

    250

```julia
rmse_enkf = mean(map(i->norm(data.xt[:,i]-mean(Xenkf[i+1]; dims = 2))/sqrt(Nx), Tspin:Tf))
```

    0.6040623682303972

In this notebook, we use an expansion of Hermite functions of fixed total  order to approximate the transport map. Again we face the bias-variance trade-off as a higher order provides a richer basis for estimation of the true map, but requires more samples to accurately estimate the coefficients of the expansion. As a guide, the number of samples required to estimate a map component of $N_{\psi}$ features should scaled as $O(N_{\psi}^2)$, see Thang & Zhou [6]. For a map of order $1$, we recover a linear approximation of the transport map $\boldsymbol{S}^{\boldsymbol{\mathcal{X}}}$ Spantini et al. [5].

```julia
X0 = vcat(zeros(Ny, Ne), deepcopy(Xenkf[Tspin+1]))

# Add process noise
model.ϵx(X0, Ny+1, Ny+Nx)

# Generate the observations
observe(h, X0, tspin, Ny, Nx)

# Apply the observation noise
ϵy(X0, 1, Ny)
```

Initialize the structure of the map

```julia
m = 40
```

    40

```julia
S = HermiteMap(m, X0; b = "CstLinProHermite")
```

    Hermite map of dimension 6:
    Hermite map component of dimension 1 with Nψ = 1 active features
    Hermite map component of dimension 2 with Nψ = 1 active features
    Hermite map component of dimension 3 with Nψ = 1 active features
    Hermite map component of dimension 4 with Nψ = 1 active features
    Hermite map component of dimension 5 with Nψ = 1 active features
    Hermite map component of dimension 6 with Nψ = 1 active features

```julia
optimize(S, X0, "kfolds"; withconstant = false, withqr = true,
			     verbose = false, start = Ny+1, P = serial, hessprecond = true)
```

    Hermite map of dimension 6:
    Hermite map component of dimension 1 with Nψ = 1 active features
    Hermite map component of dimension 2 with Nψ = 1 active features
    Hermite map component of dimension 3 with Nψ = 1 active features
    Hermite map component of dimension 4 with Nψ = 21 active features
    Hermite map component of dimension 5 with Nψ = 21 active features
    Hermite map component of dimension 6 with Nψ = 24 active features

For the 6-th map component, the set of features is:

```julia
plot(S[6].I.f)
```

We define a new type of nonlinear filter `FixedOrderStochMapFilter` that uses a fixed order expansion of multivariate Hermite functions to perform the inference.

```julia
smf = HermiteSMF(x->x, ϵy, S, Ny, Nx, Δtdyn, Δtobs, Δtobs, false)
```

    Hermite Stochastic Map Filter with filtered = false

```julia
tsmf = 100.0
Tsmf = ceil(Int64, (tsmf-tspin)/Δtobs)
```

    250

Generate the initial ensemble matrix `Xspin` from the run of the EnKF over the first `Tspin` steps.

```julia
Xspin = vcat(zeros(Ny, Ne), deepcopy(Xenkf[Tspin+1]))
```

    6×500 Array{Float64,2}:
      0.0       0.0       0.0       0.0      …   0.0       0.0       0.0
      0.0       0.0       0.0       0.0          0.0       0.0       0.0
      0.0       0.0       0.0       0.0          0.0       0.0       0.0
     10.3151   10.0667    9.86196  10.147       10.9705   10.8125    9.10545
     -4.16326  -6.02697  -4.73705  -5.69149     -1.98954  -2.82255  -6.62338
     41.0188   41.6308   40.7194   41.6197   …  40.8208   41.0792   40.5571

```julia
Xsmf = seqassim(F, data, Tsmf, model.ϵx, smf, Xspin, model.Ny, model.Nx, tspin);
```

We can compute the time average root-mean-squared-error with this nonlinear filter.

```julia
rmse_smf = mean(map(i->norm(data.xt[:,Tspin+i]-mean(Xsmf[i+1]; dims = 2))/sqrt(Nx), 1:Tsmf))
```

Nice improvement with respect to the EnKF.

```julia
(rmse_enkf - rmse_smf)/rmse_enkf
```

```julia
nb = 1
ne = Tspin+Tsmf
Δ = 1
plt = plot(layout = grid(3,1), xlim = (-Inf, Inf), ylim = (-Inf, Inf), xlabel = L"t",
           size = (600, 800))

for i =1:3
    plot!(plt[i,1], data.tt[nb:Δ:ne], data.xt[i,nb:Δ:ne], linewidth =  3, color = :teal,
          ylabel = latexstring("x_"*string(i)), legend = :bottomleft, label = "True")
    plot!(plt[i,1], data.tt[nb:Δ:ne], mean_hist(vcat(Xenkf[1:Tspin+1], Xsmf[2:end]))[i,1+nb:Δ:1+ne], linewidth = 3, grid = false,
          color = :orangered2, linestyle = :dash, label = "sEnKF")
    scatter!(plt[i,1], data.tt[nb:Δ:ne], data.yt[i,nb:Δ:ne], linewidth = 3, color = :grey,
          markersize = 5, alpha = 0.5, label  = "Observation")
    vline!(plt[i,1], [tspin], color = :grey2, linestyle = :dash)
end

plt
```

```julia

```

notebooks/8 Nonlinear ensemble filtering Lorenz 63 with adaptive Hermite map.ipynb


In the quantum teleportation protocol, party A will teleport a qubit state of qubit q to party B by utilizing quantum teleportation. Party A and B each have one qubit of an entangled bell pair. A first entangles their bell pair with the q, using a cnot gate with the bell pair being the target. They then do a hadmard gate on q. After that, they measure both q and their bell pair. A then classically sends the results of both measurements to B. B will then perform 0-2 quantum gates on their bell pair to get it back to q's original state based on the measurement results. If A's bell pair measured 1, B will do an X gate on their bell pair. If q measured 1, B will do a Z gate on their bell pair. B now has q's original state in their bell pair. A bell pair that both parties have one half of is needed for this to be possible.

```python
!pip install qiskit
```

    Collecting qiskit
      Downloading https://files.pythonhosted.org/packages/c8/88/e08fa24e890f57b83b219c94b633e4803aaa29984e7756e8bd030431ca8d/qiskit-0.24.0.tar.gz
    Collecting qiskit-terra==0.16.4
      Downloading https://files.pythonhosted.org/packages/89/64/d70a10bce894ec81a227aabc47305449b48bcffd7dde76d30e7cea6c9786/qiskit_terra-0.16.4-cp37-cp37m-manylinux2010_x86_64.whl (8.5MB)
         |████████████████████████████████| 8.5MB 3.8MB/s
    Collecting qiskit-aer==0.7.6
      Downloading https://files.pythonhosted.org/packages/03/98/735a869724392a8fa084a65d601a8c8940aacdc4c245aba9a65d9a2669ec/qiskit_aer-0.7.6-cp37-cp37m-manylinux2010_x86_64.whl (17.6MB)
         |████████████████████████████████| 17.6MB 250kB/s
    Collecting qiskit-ibmq-provider==0.12.1
      Downloading https://files.pythonhosted.org/packages/50/ed/937ea2141ec982e3850247f14786e28a0cb6c9bae52c3aba4a11da39a595/qiskit_ibmq_provider-0.12.1-py3-none-any.whl (198kB)
         |████████████████████████████████| 204kB 45.2MB/s
    Collecting qiskit-ignis==0.5.2
      Downloading https://files.pythonhosted.org/packages/b2/1d/d5df8cf48cf20a26deb6e0535f020cd2ad625cb27457abb0ab39c2104e38/qiskit_ignis-0.5.2-py3-none-any.whl (203kB)
         |████████████████████████████████| 204kB 46.5MB/s
    Collecting qiskit-aqua==0.8.2
      Downloading https://files.pythonhosted.org/packages/0e/82/528741676960a7fd1c75151639601826118f2729ad53034fa3b39290c056/qiskit_aqua-0.8.2-py3-none-any.whl (2.1MB)
         |████████████████████████████████| 2.1MB 15.4MB/s
    Collecting fastjsonschema>=2.10
      Downloading https://files.pythonhosted.org/packages/89/1c/8be51fa42aadc1c1611a52b866e1a5a1032a504f24789cf140b4e6d7c940/fastjsonschema-2.15.0-py3-none-any.whl
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (0.3.3)
    Requirement already satisfied: scipy>=1.4 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (1.4.1)
    Collecting python-constraint>=1.4
      Downloading https://files.pythonhosted.org/packages/37/8b/5f1bc2734ca611943e1d6733ee244238679f6410a10cd45ede55a61a8402/python-constraint-1.4.0.tar.bz2
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (2.6.0)
    Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (1.19.5)
    Collecting ply>=3.10
      Downloading https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl (49kB)
         |████████████████████████████████| 51kB 5.6MB/s
    Requirement already satisfied: networkx>=2.2 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (2.5)
    Collecting retworkx>=0.5.0
      Downloading https://files.pythonhosted.org/packages/1b/92/f007f8b9d88dcd5b90e363967e5d54431a68c5fe06d83400732e3b438084/retworkx-0.8.0-cp37-cp37m-manylinux2010_x86_64.whl (1.0MB)
         |████████████████████████████████| 1.0MB 34.2MB/s
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (1.7.1)
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (5.4.8)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.16.4->qiskit) (2.8.1)
    Collecting pybind11>=2.4
      Downloading https://files.pythonhosted.org/packages/8d/43/7339dbabbc2793718d59703aace4166f53c29ee1c202f6ff5bf8a26c4d91/pybind11-2.6.2-py2.py3-none-any.whl (191kB)
         |████████████████████████████████| 194kB 26.0MB/s
    Requirement already satisfied: cython>=0.27.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.7.6->qiskit) (0.29.22)
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.12.1->qiskit) (1.24.3)
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.12.1->qiskit) (2.23.0)
    Requirement already satisfied: nest-asyncio!=1.1.0,>=1.0.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.12.1->qiskit) (1.5.1)
    Collecting websockets>=8
      Downloading https://files.pythonhosted.org/packages/5a/0b/3ebc752392a368af14dd24ee041683416ac6d2463eead94b311b11e41c82/websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl (79kB)
         |████████████████████████████████| 81kB 8.3MB/s
    Collecting requests-ntlm>=1.1.0
      Downloading https://files.pythonhosted.org/packages/03/4b/8b9a1afde8072c4d5710d9fa91433d504325821b038e00237dc8d6d833dc/requests_ntlm-1.1.0-py2.py3-none-any.whl
    Requirement already satisfied: scikit-learn>=0.17 in /usr/local/lib/python3.7/dist-packages (from qiskit-ignis==0.5.2->qiskit) (0.22.2.post1)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ignis==0.5.2->qiskit) (54.0.0)
    Collecting quandl
      Downloading https://files.pythonhosted.org/packages/8b/2b/feefb36015beaecc5c0f9f2533e815b409621d9fa7b50b2aac621796f828/Quandl-3.6.1-py2.py3-none-any.whl
    Collecting docplex==2.15.194
      Downloading https://files.pythonhosted.org/packages/9b/76/ebf5b75a25c4357c9a4758cae7fea778dd7f282b83c2f22e150dc7e3c852/docplex-2.15.194.tar.gz (582kB)
         |████████████████████████████████| 583kB 45.1MB/s
    Requirement already satisfied: fastdtw in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.8.2->qiskit) (0.3.4)
    Collecting dlx
      Downloading https://files.pythonhosted.org/packages/54/c0/b8fb5bb727e983b6f5251433ef941b48f38c65bb0bd6ec509e9185bcd406/dlx-1.0.4.tar.gz
    Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.8.2->qiskit) (1.1.5)
    Collecting yfinance
      Downloading https://files.pythonhosted.org/packages/7a/e8/b9d7104d3a4bf39924799067592d9e59119fcfc900a425a12e80a3123ec8/yfinance-0.1.55.tar.gz
    Requirement already satisfied: h5py in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.8.2->qiskit) (2.10.0)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.7/dist-packages (from networkx>=2.2->qiskit-terra==0.16.4->qiskit) (4.4.2)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.3->qiskit-terra==0.16.4->qiskit) (1.2.1)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.8.0->qiskit-terra==0.16.4->qiskit) (1.15.0)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.12.1->qiskit) (3.0.4)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.12.1->qiskit) (2020.12.5)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.12.1->qiskit) (2.10)
    Collecting cryptography>=1.3
      Downloading https://files.pythonhosted.org/packages/f8/1f/acde6ff69864c5e78b56488e3afd93c1ccc8c2651186e2a5f93d93f64859/cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl (3.2MB)
         |████████████████████████████████| 3.2MB 27.2MB/s
    Collecting ntlm-auth>=1.0.2
      Downloading https://files.pythonhosted.org/packages/ff/84/97c550164b54942b0e908c31ef09d9469f3ba4cd7332a671e2125732f63b/ntlm_auth-1.5.0-py2.py3-none-any.whl
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from scikit-learn>=0.17->qiskit-ignis==0.5.2->qiskit) (1.0.1)
    Requirement already satisfied: more-itertools in /usr/local/lib/python3.7/dist-packages (from quandl->qiskit-aqua==0.8.2->qiskit) (8.7.0)
    Collecting inflection>=0.3.1
      Downloading https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas->qiskit-aqua==0.8.2->qiskit) (2018.9)
    Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.7/dist-packages (from yfinance->qiskit-aqua==0.8.2->qiskit) (0.0.9)
    Collecting lxml>=4.5.1
      Downloading https://files.pythonhosted.org/packages/d2/88/b25778f17e5320c1c58f8c5060fb5b037288e162bd7554c30799e9ea90db/lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl (5.5MB)
         |████████████████████████████████| 5.5MB 31.7MB/s
    Requirement already satisfied: cffi>=1.12 in /usr/local/lib/python3.7/dist-packages (from cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.12.1->qiskit) (1.14.5)
    Requirement already satisfied: pycparser in /usr/local/lib/python3.7/dist-packages (from cffi>=1.12->cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.12.1->qiskit) (2.20)
    Building wheels for collected packages: qiskit, python-constraint, docplex, dlx, yfinance
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.24.0-cp37-none-any.whl size=2918 sha256=2576c26bdeb464f624cb55bee11515423196e65bfc56a7ff9ea919e86e8ab3b4
      Stored in directory: /root/.cache/pip/wheels/22/fe/0a/26b7a409d1a9c3698b0319c7d009a97bffaae392d755158dca
      Building wheel for python-constraint (setup.py) ... done
      Created wheel for python-constraint: filename=python_constraint-1.4.0-py2.py3-none-any.whl size=24079 sha256=16679ed21c2e1fb2d90205a41cf0e47961a950b2c9e82a39ff1ac6def9f630b6
      Stored in directory: /root/.cache/pip/wheels/34/31/15/7b070b25d0a549d20ce2e9fe6d727471c2c61ef904720fd40c
      Building wheel for docplex (setup.py) ... done
      Created wheel for docplex: filename=docplex-2.15.194-cp37-none-any.whl size=645116 sha256=c8cafc2c6b700feaf7730dd2cc2e5a941270f94648ff0c5b99c267abaa1f8aad
      Stored in directory: /root/.cache/pip/wheels/ce/fa/5d/8f4d5fed1b8edb72e453cb2ac6fb75c6776b49d7174eb70457
      Building wheel for dlx (setup.py) ... done
      Created wheel for dlx: filename=dlx-1.0.4-cp37-none-any.whl size=5712 sha256=fb3da03c1a7e45473f1d2b040f33ae5272a71c781d381a16cf4d5329a9db60bb
      Stored in directory: /root/.cache/pip/wheels/bb/ba/15/fdd0deb104df3254912998150ba9245668db06b00af5912d1a
      Building wheel for yfinance (setup.py) ... done
      Created wheel for yfinance: filename=yfinance-0.1.55-py2.py3-none-any.whl size=22616 sha256=0d06d9c9864995f21dcf4645d059a9d23991d9b3b712f7f462ca57e0dfea7fd3
      Stored in directory: /root/.cache/pip/wheels/04/98/cc/2702a4242d60bdc14f48b4557c427ded1fe92aedf257d4565c
    Successfully built qiskit python-constraint docplex dlx yfinance
    Installing collected packages: fastjsonschema, python-constraint, ply, retworkx, qiskit-terra, pybind11, qiskit-aer, websockets, cryptography, ntlm-auth, requests-ntlm, qiskit-ibmq-provider, qiskit-ignis, inflection, quandl, docplex, dlx, lxml, yfinance, qiskit-aqua, qiskit
      Found existing installation: lxml 4.2.6
        Uninstalling lxml-4.2.6:
          Successfully uninstalled lxml-4.2.6
    Successfully installed cryptography-3.4.6 dlx-1.0.4 docplex-2.15.194 fastjsonschema-2.15.0 inflection-0.5.1 lxml-4.6.2 ntlm-auth-1.5.0 ply-3.11 pybind11-2.6.2 python-constraint-1.4.0 qiskit-0.24.0 qiskit-aer-0.7.6 qiskit-aqua-0.8.2 qiskit-ibmq-provider-0.12.1 qiskit-ignis-0.5.2 qiskit-terra-0.16.4 quandl-3.6.1 requests-ntlm-1.1.0 retworkx-0.8.0 websockets-8.1 yfinance-0.1.55

```python
from cmath import isclose
from math import sqrt

from qiskit import QuantumCircuit, Aer, execute
from qiskit.extensions import Initialize
from qiskit.visualization import plot_histogram
```

```python
class Node:

    def __init__(self, psi = None):
        self.psi = psi

    def __str__(self):
        return str(self.psi)
```

```python
class QuantumInternet:

    def __init__(self, nodes):
        self.__nodes = nodes
        self.__qc = None

    def __gen_bell_pair(self, ctrl, target):
        self.__qc.h(ctrl)
        self.__qc.cx(ctrl, target)

    def __sim(self):
        backend_sim = Aer.get_backend("statevector_simulator")
        sim = execute(self.__qc, backend_sim)
        return sim.result()

    def size(self):
        return len(self.__nodes);

    def teleport(self, sender, receiver):
        if sender < len(self.__nodes) and receiver < len(self.__nodes):
            data = 0
            src = 1
            dst = 2

            self.__qc = QuantumCircuit(3, 3)
            init = Initialize(self.__nodes[sender].psi)
            init.label = "init"
            self.__qc.append(init, [data])
            self.__gen_bell_pair(1, 2)

            self.__qc.cx(data, src)
            self.__qc.h(data)

            self.__qc.measure(data, data)
            self.__qc.measure(src, src)

            self.__qc.cx(src, dst)
            self.__qc.cz(data, dst)

            state = self.__sim().get_statevector()
            alpha_index = 0
            alpha = 0

            # index between statevector indices with dst measured as 0 and the indices with it measured as 1
            state_border = int((2 ** self.__qc.num_qubits) / 2)
            for i in range(state_border):
                if abs(state[i]) > abs(alpha):
                    alpha = state[i]
                    alpha_index = i

            beta = max(state[state_border : ], key = abs)
            # set receiver node's psi to the outcome
            self.__nodes[receiver].psi = [alpha, beta]

            sender_psi_0 = (alpha_index % 2 == 0)
            # set sender node's psi to the measurement outcome of their previous psi
            self.__nodes[sender].psi = [int(sender_psi_0), int(not sender_psi_0)]
        else:
            print("Index %d or %d out of bounds of length %u" % (sender, receiver, len(self.__nodes)))
```

```python
init_psi = [1j / sqrt(3), -sqrt(2 / 3)]
print("initial: %s" % init_psi)

a = Node(init_psi)
b = Node()
c = Node()
d = Node()

internet = QuantumInternet([a, b, c, d])
for i in range(internet.size() - 1):
    internet.teleport(i, i + 1)

print("a: %s" % a)
print("b: %s" % b)
print("c: %s" % c)
print("d: %s" % d)

success = True
for i in range(len(d.psi)):
    if not isclose(d.psi[i], init_psi[i]):
        success = False
        break
print("Qubit state successfully teleported across internet: %r" % success)
```

    initial: [0.5773502691896258j, -0.816496580927726]
    a: [1, 0]
    b: [0, 1]
    c: [0, 1]
    d: [0.5773502691896257j, (-0.8164965809277259+2.999759773043692e-16j)]
    Qubit state successfully teleported across internet: True

    Chinese scientists used a satellite to beam entangled pairs of photons to 3 ground stations in China that are more than 1200 km apart. Other approaches to quantum communication include ground-based ones such as sending pairs of entangled photons through fiber-obtic cables and through open air. However, these methods are limited to a few hundred km because the photons collide with atoms along the way and their state, which is very delicate, gets disrupted. Another approach involves chaining complex devices called a Quantum Repeaters, and could possibly receive, store, and resend quantum information across a long distance. However, this method is incredibly complex and is very theoretical because of its difficulty, as it involves receiving two photons from different places, storing the quantum information, and interfering them with each other prior to sending signals on a network. This must by confirming that you have stored the photons without measuring them. The current solution that he Chinese used is beaming quantum information through space and distributing them across 10s/100s of km from the ground. The satellite has numerous crystals and lasers that generate, split, and send entangled photon pairs through separate beams to the ground. The satellite produced around 6 million entangled pairs per second, but only one out of these 6 million reached the detectors on the ground because the beams weakened while travelling through the atmosphere. However, this is still a trillion times more efficient than the transmission rate of the best telecommunication fibers.

    More recently, last december scientists from Fermilab, a labaratoiy  the US Departement of Energy office, successfully teleported photon qubits across 44 km with a success rate of above 90%. They teleported the qubits with a fiber-optic netweork and extremely high quality single-photon detectors. They are planning on contructing a Chicago-area metropolitan quantum network in the future.

Sources:
- https://www.scientificamerican.com/article/china-shatters-ldquo-spooky-action-at-a-distance-rdquo-record-preps-for-quantum-internet/
- https://news.fnal.gov/2020/12/fermilab-and-partners-achieve-sustained-high-fidelity-quantum-teleportation/


# <center> ЛАБОРАТОРНАЯ  РАБОТА 9 </center>
# <center> МИНИМИЗАЦИЯ ФУНКЦИЙ </center>

Теоретический материал к данной теме содержится в [1, главы 9, 10].

```python
from sympy.solvers import solve
from sympy import exp, sin, cos, sqrt, log, ln, pi
from sympy import Rational as syR
from sympy import Symbol, diff
from scipy.misc import derivative

import numpy as np

import matplotlib.pyplot as plt
%matplotlib inline
```

## Задача 1.
Методом Ньютона найти минимум и максимум унимодальной на отрезке $[a,b]$  функции $ f(x) $ с точностью $\varepsilon =10^{-6}$ . Предусмотреть подсчет числа итераций, потребовавшихся для достижения заданной точности.

Ваши функции:
$f(x) = {Task1[TASK_VARIANT][f_latex]}$

Пределы:
$[a;b] = [Task1[TASK_VARIANT][a];Task1[TASK_VARIANT][b]] $

```python
epsilon1 = 10**(-6)
# Функции
f1 = {Task1[TASK_VARIANT][f1_latex]}
# Пределы:
a1,b1 = {Task1[TASK_VARIANT][a]}, {Task1[TASK_VARIANT][b]}
```

Постройте график функции. По графику определите, где находятся максимум и минимум функции.

```python
#график
```

Реализуйте функцию для метода Ньютона с подсчетом количества итераций.

```python
def NewtoonMethod(f,x,eps):
```

Найдите минимум и максиму Вашей функции.

```python
#ответы
```

## Задача 2.
Указанным в индивидуальном варианте методом найти минимумы и максимумы функции  $f(x)$ на отрезке $[x_1,x_2]$  с точностью  $\varepsilon =10^{-6}$. Предусмотреть подсчет числа итераций, потребовавшихся для достижения заданной точности.

Ваши функции:
$f2(t) = {Task1[TASK_VARIANT][f2_latex]}$

Пределы:
$[x1;x2] = [Task1[TASK_VARIANT][x1];Task1[TASK_VARIANT][x2]] $

```python

# Функции
f2 = {Task1[TASK_VARIANT][f2_latex]}
# Пределы:
x1,x2 = {Task1[TASK_VARIANT][x1]}, {Task1[TASK_VARIANT][x2]}
```

Постройте график Вашей функции. По графику найдите максимум и минимум.

```python
#график
```

Реализуйте функции, вычисляющая экстремумы функции, в соответсвии с Вашим  вариантом.

```python
#место для кода
```

```python
#место для функции
```

Найдите максимум и минимум Вашей функции.

```python
#код
```

```python
#максимум
```

```python
#минимум
```

#### ЛИТЕРАТУРА:
1. Амосов А.А., Дубинский Ю.А., Копченова Н.В. Вычислительные методы для инженеров. М.: Высшая школа, 1994.

```python

```


# Day 10
## Adapter Array

Given input file has a list of output joltages for adapters. Adapters can take an input 1, 2, or 3 joltages lower than their output and still produce the same output joltage.
Device Joltage is 3 higher than the maximum joltage of the adapter list.
Form an arrary to convert effectively zero jolts to device joltage.

```python
const fs = require("fs");

const joltages = fs
  .readFileSync("./input.txt", (_, a) => a)
  .toString()
  .split("\r\n")
  .map((a) => parseInt(a));
```

compute device joltage and add it to the chain

```python
const deviceJoltage = Math.max(...joltages) + 3;
joltages.push(deviceJoltage);
```

    101

## Part 1
Find Joltage distribution (i.e. jumps in Joltage between adapters (1,2,3)). Return the Distribution of 1 __Multiplied__ by distribution of 3

### Approach & Solution
- Easiest approach is to sort the joltages in ascending order
- Iterate through the sorted list and log the difference between current and previous joltages.
- The logging can be done in an array with indices being the the joltageDifference

```python
const sortedJoltages = joltages.sort((a,b)=> a-b);
let prevJoltage = 0;  // ease of use for a for-of loop... don't judge me

const joltageDistribution = [0,0,0,0]; // index = joltageDifference

for(let joltage of sortedJoltages){
    const difference = joltage - prevJoltage;
    joltageDistribution[difference] += 1;
    prevJoltage = joltage;
};

joltageDistribution[1] * joltageDistribution[3];
```

    2170

## Part 2
Find the total number of distinct ways adapters can be arranged to get the DeviceJoltage from effectively zero

### Approach

This problem is equivalant to the [Climbing Stairs Problem](https://leetcode.com/problems/climbing-stairs/). What are the number of ways you can reach a certain step, if you can climb one or two steps at a time.
This problem can be solved using reccursion with Time complexity of $O(2^n)$ or using dynamic programming techniques with a Time Complexity of $O(n)$, The latter of the two is used here. For a reccursion based approach reference the C# solution.

There are a couple of caveats that need to be considered when comparing this problem with the Climbing Stairs Problem(CSP).
    1. This problem has a jump of 1, 2, or 3 "steps"(Joltages) as opposed to 2 steps in the CSP.
    2. In this problem certain steps(Joltages) cannot be reached due to in availability of adapters for the required Joltage. This means the ways to reach that certain steps(Joltages) will have $0$ ways to reach them. No such special cases are in the original CSR

_______________________________

For the CSR if $F(n)$ is the number of ways the reach the $n^{th}$ step, it can be shown that $F(n) = F(n-1) + F(n-2)$. This equation might look familiar, since it is the equation for the $n^{th}$ [Fibinocci number](https://en.wikipedia.org/wiki/Fibonacci_number).
The edge cases for CSR are $F(1)$ and $F(2)$ which are $1$, $2$ respectively.

Full representation of CSR is:

$\begin{equation} F(n) = F(n-1) + F(n-2) \\\ F(1) = 1 \\\ F(2) = 2 \end{equation}$

-----------------------
For our problem we have jumps of 1, 2 or 3. It can be shown that if this joltage can be reached, the number of ways to do that is $F(n) = F(n-1) + F(n-2) + F(n-3)$
When that joltage cannot be reached (not in input list) $F(n) = 0$. The edge cases are for $F(1)$, $F(2)$, $F(3)$ which when can be reached have the values $1$, $1 + F(1)$, $1 + F(2) +F(1)$ respectively.

Full representation of our problem is:

$\begin{equation}
    F(n) =
    \begin{cases}
      F(n-1) + F(n-2) + F(n-3), & \text{if}\ reachable \\
      0, & \text{otherwise}
    \end{cases}
  \end{equation}$

$\begin{equation}
    F(1) =
    \begin{cases}
      1, & \text{if}\ reachable \\
      0, & \text{otherwise}
    \end{cases}
  \end{equation}$

  $\begin{equation}
    F(2) =
    \begin{cases}
      1 + F(1), & \text{if}\ reachable \\
      0, & \text{otherwise}
    \end{cases}
  \end{equation}$

  $\begin{equation}
    F(3) =
    \begin{cases}
      1 + F(2) + F(1), & \text{if}\ reachable \\
      0, & \text{otherwise}
    \end{cases}
  \end{equation}$

### Solution
For the solution, we will use an array to represent the number of ways to get to the index. for example `arr[15]` would give the number of ways to reach the joltage 15.
We also need fast lookups to see if a joltage is reachable (available in the input list).

```python
const joltageSet = new Set(joltages);
const stepsToJoltage =[]; // f(n) array
```

```python
// stepsToJoltage[0] = 0; ignoring zero index for sanity
stepsToJoltage[1] = joltageSet.has(1) ? 1 : 0;
stepsToJoltage[2] = joltageSet.has(2) ? stepsToJoltage[1] + 1 : 0;
stepsToJoltage[3] = joltageSet.has(3) ? stepsToJoltage[1] + stepsToJoltage[2] + 1 : 0;

for(let j = 4; j<=deviceJoltage; j++){
    stepsToJoltage[j] = joltageSet.has(j) ? stepsToJoltage[j-1] + stepsToJoltage[j-2] + stepsToJoltage[j-3] : 0;
}

stepsToJoltage[deviceJoltage];
```

    24803586664192


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

```python
def random_binning(N, d,return_string=False):
    """
    Function to distribute a sum total of N into d bins
    Returns a list
    """
    import numpy as np

    count=N
    sum_count=0
    lst=[]
    for i in range(d-1):
        x=np.random.randint(0,count+1)
        if return_string:
            lst.append(str(x))
        else:
            lst.append(x)
        count=count-x
        sum_count+=x

    if return_string:
        lst.append(str(N-sum_count))
    else:
        lst.append(N-sum_count)
    return lst
```

### Function to generate single term from a list of exponents. E.g. if we pass 1,2,0,3 it will generate: $x_1.x_2^2.x_4^3$

```python
def gen_single_term(lst):
    """
    Function to generate single term from a list of exponents.
    """
    term=''
    for i in range(1,len(lst)+1):
        if lst[i-1]==0:
            pass
        elif lst[i-1]==1:
            term+='x'+str(i)+'.'
        else:
            term+='x'+str(i)+'^'+str(lst[i-1])+'.'

    return term[:-1]
```

```python
gen_single_term([1,1,2,0,1])
```

    'x1.x2.x3^2.x5'

```python
def count_symbol(lst):
    syms = set(lst)
    syms_dict={}
    term_lst=[]
    for s in syms:
        syms_dict[s]=lst.count(s)

    for k,v in syms_dict.items():
        if v!=1:
            term_lst.append(str(v)+'.'+k)
        else:
            term_lst.append(k)

    return term_lst
```

```python
def gen_multinomial(n_features=5,max_power=5,max_terms=10,fixed_terms=None,coefficients=True,
                   prob_negative_sign=0.3):
    """
    Generates multinomial expression.
    n_features: Number of independent variables
    max_power: Maximum exponent each terms can be raised to. A random power will be chosen up to this maximum.
    max_terms: Maximum number of terms. A random number of terms will be chosen up to this maximum.
    fixed_terms: Attempt will be made to generate only this many terms. Sometimes does not work.
    coefficients (boolean): Adds (or does not) random integer coefficients in front of the terms.
    prob_negative_sign: Probability of putting a negative term.
                        Each term's sign wil be chosen randomly based on this probability
    """
    import numpy as np
    eqn=''
    eqn_terms=[]

    if fixed_terms!=None:
        n_terms=fixed_terms
    else:
        n_terms=np.random.randint(2,max_terms+1)

    for i in range(n_terms):
        power=np.random.randint(1,max_power+1)
        #power=max_power
        power_lst=random_binning(power,n_features)
        term=gen_single_term(power_lst)
        if coefficients:
            coeff=np.random.randint(1,11)
            if coeff!=1:
                coeff=str(coeff)
                term=coeff+'.'+term
        eqn_terms.append(term)

    eqn_terms=count_symbol(eqn_terms)

    for e in eqn_terms:
        eqn+=e
        sign=np.random.choice(['+','-'],p=[prob_negative_sign,1-prob_negative_sign])
        eqn= eqn+' '+sign+' '

    return eqn[:-3]
```

```python
gen_multinomial(coefficients=True)
```

    '6.x1^2.x2.x4 - 4.x1^2.x4^3 - 7.x1^2.x2^2 - 3.x1 - 7.x1^3.x3.x5 - 5.x1^5 - 6.x5'

```python
def make_equation_system(n_samples=10,n_features=5,max_power=5,max_terms=10,
                               fixed_terms=None,coefficients=False,prob_negative_sign=0.3):
    """
    Generates multiple samples of multinomials for constructing a system of equations
    """
    regressions=[]
    for i in range(n_samples):
        regressions.append(gen_multinomial(n_features=n_features,max_power=max_power,
                                           max_terms=max_terms,fixed_terms=fixed_terms,
                                           coefficients=coefficients,prob_negative_sign=prob_negative_sign))

    return regressions
```

```python
make_equation_system(n_samples=10,n_features=4,max_power=3,max_terms=8,fixed_terms=4,
                     coefficients=True,prob_negative_sign=0.5)
```

    ['10.x2 - x1 - 7.x1 - 6.x1',
     '2.x2.x4^2 - 2.x1 + 8.x1.x2.x4 - 10.x4',
     '8.x1^2 - 4.x1.x2 + 6.x1^3 + 4.x1',
     '2.x1^3 + 3.x1.x2^2 - 9.x1^2.x4 - 9.x1',
     '7.x1.x3 - x1^3 + x1^2.x3 + 3.x2^2.x3',
     '10.x2^2.x4 - 8.x2 + 5.x1.x3 + x2^2.x4',
     '2.x1^3 - x1.x4 - 5.x1^2 + 2.x3',
     '10.x1 + 4.x1.x2 - 9.x1^2.x4 + 3.x1^2.x4',
     'x2 - x1^3 + x2.x3^2 - 10.x1^2',
     '3.x1.x2.x4 - 3.x1^3 + 3.x1^2 - 3.x1']

### Evaluate a polynomial string

```python
from sympy import *
```

```python
def symbolize(s):
    """
    Converts a a string (equation) to a SymPy symbol object
    """
    from sympy import sympify
    s1=s.replace('.','*')
    s2=s1.replace('^','**')
    s3=sympify(s2)

    return(s3)
```

```python
def eval_multinomial(s,vals=None,symbolic_eval=False):
    """
    Evaluates polynomial at vals.
    vals can be simple list, dictionary, or tuple of values.
    vals can also contain symbols instead of real values provided those symbols have been declared before using SymPy
    """
    from sympy import Symbol,sympify,symbols

    sym_s=symbolize(s)
    sym_set=sym_s.atoms(Symbol)
    sym_lst=[]
    for s in sym_set:
        sym_lst.append(str(s))
    sym_lst.sort()
    if symbolic_eval==False and len(sym_set)!=len(vals):
        print("Length of the input values did not match number of variables and symbolic evaluation is not selected")
        return None
    else:
        if type(vals)==list:
            sub=list(zip(sym_lst,vals))
        elif type(vals)==dict:
            l=list(vals.keys())
            l.sort()
            lst=[]
            for i in l:
                lst.append(vals[i])
            sub=list(zip(sym_lst,lst))
        elif type(vals)==tuple:
            sub=list(zip(sym_lst,list(vals)))
        result=sym_s.subs(sub)

    return result
```

```python
s1=gen_multinomial(fixed_terms=5,coefficients=True)
```

```python
s1
```

    '2.x4 - 6.x2^2.x3 - 10.x1 - 6.x3^2.x4^2 - 8.x1^3.x2.x5'

```python
eval_multinomial(s1,{'x1':2,'x3':1,'x4':0,'x2':3,'x5':1},symbolic_eval=True)
```

    -266

```python
vals=(2,3,1)
eval_multinomial(s1,vals)
```

    Length of the input values did not match number of variables and symbolic evaluation is not selected

```python
from sympy import symbols
x,y=symbols('x y')
vals=[2,x,2,2,y]
eval_multinomial(s1,vals,symbolic_eval=True)
```

    -12*x**2 - 64*x*y - 112

### Pretty printing and LaTeX format output

```python
def pretty_multinomial(n_features=5,max_power=5,max_terms=10,fixed_terms=None,coefficients=False):
    from sympy import init_printing
    init_printing()
    s=gen_multinomial(n_features=n_features,max_power=max_power,max_terms=max_terms,
                      fixed_terms=fixed_terms,coefficients=coefficients)
    sym_s=symbolize(s)
    return(sym_s)
```

```python
pretty_multinomial(coefficients=True,fixed_terms=4)
```

```python
def latex_multinomial(n_features=5,max_power=5,max_terms=10,fixed_terms=None,coefficients=False):
    from sympy import latex
    s=gen_multinomial(n_features=n_features,max_power=max_power,max_terms=max_terms,
                      fixed_terms=fixed_terms,coefficients=coefficients)
    sym_s=symbolize(s)
    return(latex(sym_s))
```

```python
latex_multinomial(coefficients=True,fixed_terms=4)
```

    '5 x_{1}^{4} - 10 x_{1}^{2} x_{2} x_{5} + x_{1}^{2} x_{3} - 10 x_{2}^{3}'

```python
lst=[]
for i in range(6):
    lst.append(pretty_multinomial(coefficients=True))
lst
```

```python
pretty_multinomial(n_features=2,max_power=4,coefficients=True)
```

```python

```


# Probabilistic tf-idf

Probabilistic tf-idf is a probabilistic model for matching large lists of sparse binary vectors - primarily, I had tokenized company names in mind when creating it. It extends the commonly used tf-idf method of matching tokenized names in that

 * In the absence of training data (i.e. no known matches), it produces scores similar to tf-idf.
 * Knowledge derived from known matches can be incorporated to learn both better entity descriptions as well as token-level relevance parameters.
 * Even without training data, it offers unsupervised training that allows to adapt to the matching task at hand to some extent.

The matching algorithm is computationally efficient by leveraging sparsity in the same way as tf-idf does.

# How it works

## Model

The model consists of _entities_ that are probability distributions over binary vectors of a fixed size - the _observations_. I call the elements of the vectors _tokens_ because the main application I have in mind is matching tokenized short strings, e.g. names. The distributions are independent Bernoulli distributions for the tokens. Each observation is assigned to an entity, but the number of entities is not necessarily fixed. In other words, the model is a non-parametric mixture with a product of independent Bernoullis as observation model.

With [notation as below](#Notation), the model is

$$
\begin{align}
\alpha_t &= \pi_t s_t \\
\beta_t &= (1 - \pi_t) s_t \\
r_{zt} &\sim \text{Beta}(\alpha_t, \beta_t) \\
p(\mathbf{x} \mid z) &= \prod_t r_{zt}^{x_t} \, (1 - r_{zt})^{1 - x_t}
\end{align}
$$

which can be complemented by an additional prior for $\alpha_t, \beta_t$, as discussed in the section on model training. In order to complete the mixture specification, one needs a partition prior as well. This is not in the scope of this document.

In the entity resolution settings I have in mind, entities usually have very few observations assigned to them (often just one), so that a lot of the information resides in the priors. Sharing parameters across entities allows to share information on token usage across entities.

## Intuition

This section contains simplified expressions for matching scores in the entity resolution setting, where most entities have only one observation and token vectors are very sparse - e.g. containing a handful of tokens out of tens or hundreds of thousands per entity. This implies $\pi_t \ll 1$.

When matching entities, we consider the log-probability of observing a vector $\mathbf{x}$ for entity $z$ with count vector $\mathbf{k}_z$ and number of observations $n_z$. We set $n_z = 1$, so that $k_{zt} \in \{0, 1\}$. Since we are dealing with single entities and tokens only, we suppress the entity and token indices in the following and write $x, k, n$ etc.

### Interpretation of $s_t$

The probability to observe a token (i.e. $x = 1$) given that it has been observed for the entity (i.e. $k = 1$) is

$$
\begin{align}
p(x = 1 \mid k = 1, n = 1)
    &= \frac{s \pi + 1}{s + 1} \\
    &\approx \frac{1}{s + 1}
\end{align}
$$

where the second line holds if $\pi s \ll 1$, which is typically the case. Hence $\log s^{-1}$ are the logits for observing a token in a new observation given that it has been observed before, and is a measure of the reliability of a token.

### Matching scores

Since both $k$ and $x$ can take on two values each, there are only four cases that can occur. We continue to assume $\pi s \ll 1$ and divide probabilities by prior probabilities to simplify expressions. Since the prior does not depend on the entity, this does not affect score differences.

For a token-entity match, $x = k = 1$, we obtain a score

$$
\begin{align}
\log\frac{p(x = 1 \mid k = 1, n=1)}{p_0(x = 1)} &= \log\pi^{-1} + \log\frac{\pi s + 1}{s + 1} \\
&\approx \log \pi^{-1} - \log\left(s + 1\right)
\end{align}
$$

whereas for non-matching we get

$$
\log \frac{p(x = 0 \mid k = 1, n=1)}{p_0(x=0)} = \log s - \log\left(s + 1\right)
$$

The same result holds for $x=1, k=0$.

Finally matching absence of a token yields

$$
\begin{align}
\log\frac{p(x = 0 \mid k = 0, n=1)}{p_0(x = 0)} &= \log\left(1 - \pi\right)^{-1} + \log\frac{\left(1 - \pi\right) s + 1}{s + 1} \\
&\approx -\log \left(s + 1\right)
\end{align}
$$

Combining all parts leads to the score

$$
\log \frac{p(\mathbf{x} \mid z)}{p_0(\mathbf{x})} \approx
    \underbrace{\sum_{t \in \mathbf{x}\cap\mathbf{k}_z} \log\pi_t^{-1}}_{\text{matches}}
    - \underbrace{\sum_{t \in \mathbf{x}\cup\mathbf{k}_z \setminus \mathbf{x}\cap\mathbf{k}_z} \log s_t^{-1}}_{\text{mismatches}} - \underbrace{\sum_t\log\left(s_t + 1\right)}_{\text{const}}
$$

## Sparse Collapsed Formulation

An immediate problem with the formulation above is that the $r_{zt}$ are dense, so that even for tasks of moderate size memory issues arise. This can be fixed by marginalizing $r_{zt}$ using conjugacy. The resulting token distribution is still Bernoulli, but is expressed in terms of the sparse sufficient statistics

 * $n_z$ number of observations assigned to entity $z$ and
 * $\mathbf{k}_z$, the token count vector for all observations assigned to entity $z$

By standard calculations, the result is

$$
\begin{align}
p_{zt} &\equiv p(x_t = 1 \mid z) \\
    &= \frac{\alpha_t + k_{zt}}{\alpha_t + \beta_t + n_z}
\end{align}
$$

This sparse expression allows to sparsify the entire computation of the observation probabilies by shuffling terms. It is easier to do this for the log-probability

$$
\log p(\mathbf{x} \mid z) = \sum_t \left[x_t \log p_{zt} + (1 - x_t) \log(1 - p_{zt})\right]
$$

The basic idea is to shuffle terms such that sums containing specific (token and observation/entity dependent) data are restricted as much as possible, while unrestricted sums only contain less specific data. This allows to both reuse computations as well as save memory.

Let's denote the token probability with $k_{zt}$ set to zero (regardless of its actual value) by $p^0_{zt}$. Note that $p^0_{zt}$ depends on the entity through $n_z$ only. To simplify notation for sums, I use $t \in \mathbf{k}_z$ as shorthand for $k_{zt} > 0$ (and similar for other set operations).

Since $p_{zt} = p^0_{zt}$ if $t \notin \mathbf{k}_z$, we have

$$
\sum_{t} \log p_{zt} = \sum_{t\in \mathbf{k}_z} \log\frac{p_{zt}}{p^0_{zt}} + \sum_{t} \log p^0_{zt}
$$

Applying this to the observation probability, we get

$$
\begin{align}
\log p(\mathbf{x} \mid z) &= \sum_{t \in \mathbf{x}} \log p_{zt} +  \sum_{t \notin \mathbf{x}} \log (1 - p_{zt}) \\
&= \sum_{t \in \mathbf{x}\cap\mathbf{k}_z} \log \frac{p_{zt}}{p^0_{zt}} +
   \sum_{t\in \mathbf{x}} \log p^0_{zk} +
   \sum_{t \in \mathbf{k}_z \setminus \mathbf{x}} \log\frac{1 - p_{zt}}{1 - p^0_{zt}} +
   \sum_{t\notin \mathbf{x}} \log \left(1 - p^0_{zt}\right) \\
&= \sum_{t \in \mathbf{x}\cap \mathbf{k}_z} \left[\log \frac{p_{zt}}{p^0_{zt}} - \log\frac{1 - p_{zt}}{1 - p^0_{zt}}\right] + \sum_{t \in \mathbf{k}_z} \log\frac{1 - p_{zt}}{1 - p^0_{zt}} + \sum_{t \in \mathbf{x}} \log\frac{p^0_{zt}}{1 - p^0_{zt}} + \sum_t \log \left(1 - p^0_{zt}\right)
\end{align}
$$

While this looks more complicated, it makes computation practical if we choose not to compute probabilities where the token overlap is zero.

1. The first sum over $\mathbf{x} \cap \mathbf{k}_z$ is a sparse matrix multiplication of two matrices with the same sparsity patterns as the document-term matrices for observations/entities have. Since we set probabilities for no token overlap to zero, the remaining terms have to be evaluated only if $\mathbf{x}\cap\mathbf{k}_z$ is not empty, i.e. for the non-zero entries of the result of the sparse matrix multiplication.
1. The second and last term can be pre-computed since they do not depend on the observation,
1. The last two terms depend on $z$ only through the number of known name variants $n_z$ because this is the only dependency that $p^0_{zt}$ has on $z$. Usually there will be only a handful of different values of $n_z$, way fewer than values of $z$.
1. $p^0_{zt}$ can be looked up from a dense matrix of size (number of tokens, number of distinct $n_z$). If there are very many observations per entity (say thousands) and a large vocabulary (say hundreds of thousands) caching may no longer be feasible in memory, but this should rarely be a problem in practice.

## Training

__TODO__

# Appendix

## Notation

 * $t$ is used as index for tokens.
 * $z$ is used as index for entities.
 * $\mathbf{x}$ denotes an observation of tokens.
 * $n_z$ is the number of observations assigned to entity $z$.
 * $k_{zt}$ is the number of times token $t$ appears in all observations assigned to entity $z$. I also use $\mathbf{k}_z$ to denote the count vector for entity $z$.
 * $t \in \mathbf{x}$ means $x_t = 1$.
 * $r_{zt}$ is the Bernoulli rate for entity $z$ to emit token $t$
 * $\alpha_t, \beta_t$ are prior parameters for $r_{zt}$.
 * $\pi_t, s_t$ are an alternative mean/strength parametrization for $\alpha_t, \beta_t$
 * $p_{zt}$ is the probability for entity $z$ to emit token $t$ after marginalizing $r_{zt}$
 * $p^0_{zt}$ has the same meaning, but for the case of forcing $k_{zt} = 0$ regardless of its actual value

## Corrections for excluded observations (currently not implemented)

Setting probabilities for observations with no token overlap to zero leads to observation probabilities that do not sum to one. The probability for such observations is

$$
p(|\mathbf{x}\cap\mathbf{k}_z| = 0 \mid z) = \prod_{t \in \mathbf{k}_z} (1 - p_{zt})
$$

Since for the unrestricted case $\sum_{\mathbf{x}} p(\mathbf{x} \mid z) = 1$, we can compute normalized probabilities by setting

$$
\log p(\mathbf{x} \mid z) \rightarrow \log p(\mathbf{x} \mid z) - \log\left(1 - \prod_{t\in\mathbf{k}_z} (1 - p_{zt})\right)
$$

This correction is typically very small because the $t \in \mathbf{k}_z$ have high probability to occur, except when there is only one token, where a typical value is $-\log 0.8 \approx 0.2$

Similarly, we can correct the prior probabilities by disallowing observations with no tokens:

$$
\log p_0(\mathbf{x}) \rightarrow \log p_0(\mathbf{x}) - \log\left(1 - \prod_t (1 - \pi_t)\right)
$$

These corrections do not interfere with marginalizing $r_{zt}$ for predictions, but strictly speaking they do affect parameter training - even though the effect should be minor.


```python
import numpy as np
import sympy as sym
import numba
import pydae.build as db

```

```python

```

```python
S_b = 90e3
U_b = 400.0
Z_b = U_b**2/S_b
I_b = S_b/(np.sqrt(3)*U_b)
Omega_b = 2*np.pi*50
R_s = 0.023/Z_b
R_r = 0.024/Z_b
Ll_s = 0.086/Z_b
Ll_r = 0.196/Z_b
L_m = 3.7/Z_b

params = {'S_b':S_b,'U_b':U_b,'I_b':I_b,
          'R_s':R_s,'R_r':R_r,'L_ls':Ll_s,'L_lr':Ll_r,'L_m':L_m,   # synnchronous machine d-axis parameters
          'H_m':3.5,'Omega_b':2*np.pi*50,'D':0.1,
          'v_0':1,'theta_0':0.0,
          'X_l':0.05, 'omega_s':1.0,'v_rd':0.0,'v_rq':0.0,'v_sd':0.0,'v_sq':-1.0}

u_ini_dict = {'tau_m':0.1, 'Q_c':0.0}  # for the initialization problem
u_run_dict = {'tau_m':0.1,'Q_c':0.0}  # for the running problem (here initialization and running problem are the same)

x_list = ['omega_e','psi_sd','psi_sq','psi_rd','psi_rq']    # [inductor current, PI integrator]
y_ini_list = ['i_sd','i_sq','i_rd','i_rq'] # for the initialization problem
y_run_list = ['i_sd','i_sq','i_rd','i_rq'] # for the running problem (here initialization and running problem are the same)

sys_vars = {'params':params,
            'u_list':u_run_dict,
            'x_list':x_list,
            'y_list':y_run_list}

exec(db.sym_gen_str())  # exec to generate the required symbolic varables and constants
```

```python
#v_sd =  -v_h*sin(theta_h)
#v_sq =  v_h*cos(theta_h)

tau_e = psi_sd*i_sq - psi_sq*i_sd

domega_e = 1/(2*H_m)*(tau_m - tau_e - D*omega_e)
dpsi_sd = Omega_b*(-v_sd -R_s*i_sd - omega_s*psi_sq)
dpsi_sq = Omega_b*(-v_sq -R_s*i_sq + omega_s*psi_sd)
dpsi_rd = Omega_b*(-v_rd -R_r*i_rd - (omega_s-omega_e)*psi_rq)
dpsi_rq = Omega_b*(-v_rq -R_r*i_rq + (omega_s-omega_e)*psi_rd)

g_1 = -psi_sd + (L_m + L_ls)*i_sd + L_m*i_rd
g_2 = -psi_sq + (L_m + L_ls)*i_sq + L_m*i_rq
g_3 = -psi_rd + (L_m + L_lr)*i_rd + L_m*i_sd
g_4 = -psi_rq + (L_m + L_lr)*i_rq + L_m*i_sq

#g_5 = P_h - (v_h*v_0*sin(theta_h - theta_0))/X_l
#g_6 = Q_c + Q_h + (v_h*v_0*cos(theta_h - theta_0))/X_l - v_h**2/X_l
#g_7 = -P_h - (v_sd*i_sd + v_sq*i_sq)
#g_8 = -Q_h - (v_sq*i_sd - v_sd*i_sq)

h_1 = I_b*(i_sd*i_sd + i_sq*i_sq)**0.5
h_p = (v_sd*i_sd + v_sq*i_sq)
h_q = (v_sd*i_sq - v_sq*i_sd)

sys = {'name':'imib_fisix_5ord',
       'params':params,
       'f':[domega_e,dpsi_sd,dpsi_sq,dpsi_rd,dpsi_rq],
       'g':[g_1,g_2,g_3,g_4],#,g_5,g_6,g_7,g_8],
       'g_ini':[g_1,g_2,g_3,g_4],#,g_5,g_6,g_7,g_8],
       'x':x_list,
       'y_ini':y_ini_list,
       'y':y_run_list,
       'u_run_dict':u_run_dict,
       'u_ini_dict':u_ini_dict,
       'h':[h_1,h_p,h_q]}

sys = db.system(sys)
db.sys2num(sys)
```

```python
sys['f']
```

$\displaystyle \left[\begin{matrix}\frac{i_{sd} \psi_{sq} - i_{sq} \psi_{sd} + \tau_{m}}{2 H_{m}}\\\Omega_{b} \left(- R_{s} i_{sd} - \omega_{s} \psi_{sq}\right)\\\Omega_{b} \left(- R_{s} i_{sq} + \omega_{s} \psi_{sd} + 1\right)\\\Omega_{b} \left(- R_{r} i_{rd} - \psi_{rq} \left(- \omega_{e} + \omega_{s}\right) - v_{rd}\right)\\\Omega_{b} \left(- R_{r} i_{rq} + \psi_{rd} \left(- \omega_{e} + \omega_{s}\right) - v_{rq}\right)\end{matrix}\right]$

```python

```


# ÁLGEBRA LINEAR COMPUTACIONAL

## SEGUNDA AVALIAÇÃO - 2020.2 - PROFESSOR ERITO MARQUES

---
### GRUPO:
**Ícaro Santos Barcelos Pereira - _2017780187_**\
**Leandro Bataglia Pereira - _2018780199_**\
**Pedro Henrique Farolfi Camelo - _2018780326_**

---

Repositório dos algoritmos solicitados na segunda prova da disciplina de Álgebra Linear Computacional.
##### Instruções:
As respostas devem ser enviadas no formato jupyter notebook.\
Os trabalhos devem ser feitos em grupo. Não é permitida troca de informações entre os grupos.\
Fixada a data de entrega, não serão aceitos trabalhos entregues fora dessa data.

```python
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore",category=UserWarning)
warnings.filterwarnings("ignore",category=RuntimeWarning)
```

```python
import numpy as np
import pandas as pd
import math
from normas import *
from criterios import *
from cholesky import *
from iterativos import *
from benford import *
from fatoracaoLU import fatoracaoLU
from substituicao import resolve_substituicao
from sympy import Matrix, init_printing
init_printing()
#BIBLIOTECA QUE NÃO ESTOU USANDO MAS PODE SER ÚTIL PARA EXIBIR AS MATRIZES/VETORES = display(Matrix(*variavel que representa o dado*)
```

```python
# Coverti isso aqui em um procedimento, a parada é que vc pode mandar isso
# ocorrer no início de qualquer célula e pode alterar o excel em tempo de execução :)

# CONVERSÃO DOS DADOS EM ARRAYS DA NUMPY COM TYPE 'float64'
def ler_matriz_A_vetor_b(file_excel):
    df = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name="A e b")
    df_matriz = df.iloc[:,0:-2]
    df_vetor = df.iloc[:,-1]
    matriz = np.array(df_matriz, dtype='f8') # Converte a matriz A em um array da numpy
    vetor = np.ravel(np.array(df_vetor, dtype='f8')) # Converte o vetor b em um array da numpy
    return matriz, vetor
```

### 1) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel) e avalie:

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes")
```

**a)** se A é tridiagonal;

```python
def verifica_tridiagonal(matriz_A, debug=False):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        if debug:
            print("A matriz não é quadrada -> não é tridiagonal")
        return False

    for i in range(linhas):
        for j in range(colunas):
            if np.abs(i-j) <= 1 and matriz_A[i,j] == 0:
                return False
            if np.abs(i-j) >= 2 and matriz_A[i,j] != 0:
                return False
    return True
```

```python
verifica_tridiagonal(matriz, debug=True)
```

**b)** se A é triangular superior;

```python
def verifica_tri_superior(matriz_A, debug=False):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        if debug:
            print("A matriz não é quadrada -> não é triangular superior")
        return False

    for i in range(linhas):
        for j in range(i):
            if matriz_A[i,j] != 0:
                return False
    return True
```

```python
verifica_tri_superior(matriz, debug=True)
```

**c)** se A é triangular inferior;

```python
def verifica_tri_inferior(matriz_A):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        print("A matriz não é quadrada -> não é triangular inferior")
        return False

    for j in range(colunas):
        for i in range(j):
            if matriz_A[i,j] != 0:
                return False
    return True
```

```python
verifica_tri_inferior(matriz)
```

**d)** se Traço de A é maior que 4.

```python
def traco(matriz_A, debug=False):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        if debug:
            print("A matriz não é quadrada -> traço não está definido.")
        return False

    n, traco = min((linhas, colunas)), 0
    for i in range(n):
        traco += matriz_A[i,i]
    if traco > 4:
        print("O traço da matriz é " + str(traco) + ", portanto é maior que 4")
        return True
    print("O traço da matriz é " + str(traco) + ", portanto não é maior que 4")
    return False
```

```python
A = np.array([[1,2,3],[1,2,3]])
traco(A, debug=True)
```

```python
traco(matriz, debug=True)
```

---
### 2) Implemente um algoritmo que leia uma matriz A (triangular) e um vetor b (de uma planilha excel) e resolva o sistema Ax = b por substituição para frente ou para trás conforme o caso.

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes")
```

```python
def resolve_substituicao(matriz_A, vetor_b):
    if checa_tamanho_matriz_vetor(matriz_A, vetor_b) == False:
        return False

    if(verifica_tri_inferior(matriz_A)):
        return resolve_subs_frente(matriz_A, vetor_b)

    elif(verifica_tri_superior(matriz_A)):
        return resolve_subs_tras(matriz_A,vetor_b)

    else:
        print("A matriz não é triangular")
        return False

def checa_tamanho_matriz_vetor(matriz_A, vetor_b):
    lin_A, col_A = np.shape(matriz_A)
    col_b = np.shape(vetor_b)

    try:
        assert lin_A == col_b[0]
    except:
        print("A quantidade de linhas em A e de colunas em b são distintas")
        return False
    return True

def resolve_subs_frente(matriz_A, vetor_b): # Resolve um sistema com matriz triangular inferior
    n = len(matriz_A)
    vetor_resultado = np.array(np.copy(vetor_b), dtype='f8')

    for i in range(n):
        for j in range(i):
            vetor_resultado[i] = vetor_resultado[i] - matriz_A[i,j] * vetor_resultado[j]
        vetor_resultado[i] = vetor_resultado[i]/matriz_A[i,i]
    return vetor_resultado

def resolve_subs_tras(matriz_A,vetor_b): # Resolve um sistema com matriz triangular superior
    n = len(matriz_A)
    vetor_resultado = np.array(np.copy(vetor_b), dtype='f8')

    for i in range(n-1,-1,-1):
        for j in range(n-1,i,-1):
            vetor_resultado[i] = vetor_resultado[i] - matriz_A[i,j] * vetor_resultado[j]
        vetor_resultado[i] = vetor_resultado[i]/matriz_A[i,i]
    return vetor_resultado
```

```python
print(resolve_substituicao(matriz, vetor))
```

---
### 3) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel) e:

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes")
```

**a)** Avalie se é possível obter o fator de Cholesky;

```python
def verifica_pos_def(matriz_A):
    if verifica_inversa(matriz_A) and verifica_simetria(matriz_A):
        n = len(matriz_A)
        for i in range(n):
            soma = 0
            for j in range(n):
                soma += matriz_A[i,j]
            if(soma == 0 or matriz_A[i,i] <= 0):
                print("A matriz não é positiva definida.")
                return False
        print("A matriz é positiva definida.")
        return True
    print("A matriz não é positiva definida.")
    return False
```

```python
verifica_pos_def(matriz)
```

**b)** Resolva o sistema Ax = b usando a fatoração de Cholesky.

```python
def decomposicao_cholesky(matriz_A, vetor_b):
    if verifica_pos_def(matriz_A):
        return None
    n = len(matriz_A) # n é a ordem da matriz a
    matriz_R = np.copy(np.triu(matriz_A)) # matriz_r é uma cópia triangular superior da matriz A

    for i in range(n):
        for k in range(i): # Não é executado quando i = 0
            matriz_R[i,i] = matriz_R[i,i] - math.pow(matriz_R[k,i],2) # Define os termos da diagonal principal
        if matriz_R[i,i] <= 0: # Se algum elemento da diagonal principal for menor ou igual a zero, por definição, a matriz não é positiva definida
            return None
        matriz_R[i,i] = math.sqrt(matriz_R[i,i])
        for j in range(i+1,n): # Não é executado quando i = n
            for k in range(i): # Não é executado quando i = 0
                matriz_R[i,j] = matriz_R[i,j] - (matriz_R[k,i] * matriz_R[k,j])
            matriz_R[i,j] = matriz_R[i,j]/matriz_R[i,i]

    return resolve_cholesky(matriz_R, vetor_b)

def resolve_cholesky(matriz_R, vetor_b):
    matriz_Rt = np.copy(np.transpose(matriz_R)) # matriz_rt é a matriz R transposta

    vetor_y = subs.resolve_substituicao(matriz_Rt, vetor_b) # Faz o cálculo de Rt * y = b para obter o vetor y
    vetor_x = subs.resolve_substituicao(matriz_R, vetor_y) # Faz o cálculo de R * x = y para obter o vetor x (resultado)

    return vetor_x # Retorna o vetor x (resultado)
```

```python
decomposicao_cholesky(matriz, vetor)
```

---
### 4) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel) e:

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes")
```

**a)** Avalie se algum critério é satisfeito para aplicação dos métodos de Gauss-Seidel, Jacobi e SOR;

```python
def criterio_linhas(matriz_A): # Verificar o critério das linhas para o método de Jacobi
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    soma = 0

    for i in range(n): # Percorre toda a matriz
        for j in range(m):
            if i != j: # Desconsidera os termos da diagonal principal
                soma += np.abs(matriz_A[i,j])

        if soma >= np.abs(matriz_A[i,i]): # Verifica se a soma de todos os termos (sem contar o termo da diagonal principal) de cada linha é menor ou maior que o termo da diagonal principal (Critério das linhas)
            return False

        soma = 0
    return True
```

```python
def criterio_sassenfeld(matriz_A):
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que auxilia nos somatórios dos termos tanto de Aij*Beta, quanto de Aij
    vetor_betao = np.zeros(m) # Vetor de Betas iniciado com termos iguais a zero

    for i in range(n):
        for j in range(i): # j vai até i-1 (desconsidera a primeira iteração)
            aux += np.abs(matriz_A[i,j]) * vetor_betao[j] # A partir da segunda iteração, inclui a multiplicação de Aij pelos Betas anteriores no somatório (já que na primeira iteração não há Betas anteriores, obviamente)
        for j in range(i,n):
            aux += np.abs(matriz_A[i,j]) # Somatório dos módulos dos termos de Aij
        vetor_betao[i] = aux/np.abs(matriz_A[i,i]) #Divide o valor da soma pelo termo da diagonal principal
        aux = 0 # Zera a variável auxiliar pra próxima iteração

    print("Maior betão:", np.max(vetor_betao))
    if np.max(vetor_betao) < 1: # Verifica se a condição do critério de Sassenfeld (max(B) < 1) é satisfeita
        return True
    return False
```

```python
criterio_linhas(matriz)
```

```python
criterio_sassenfeld(matriz)
```

**b)** Resolva o sistema Ax = b usando os métodos iterativos de Gauss-Seidel, Jacobi e SOR.

## Jacobi

```python
def jacobi(matriz_A, vetor_b, vetor_x=0, N=10000, p=1e-15): # Função que executa o algoritmo de Jacobi
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que vai auxiliar a armazenar as subtrações dos valores Aij*xj

    if isinstance(vetor_x, int): # Se não é fornecido um vetor de aproximação inicial para x, ele é iniciado aqui com todos os elementos iguais a zero
        vetor_x = np.zeros(m)

    vetor_aux = np.copy(vetor_x) # Vetor auxiliar igual ao vetor x que representa a iteração k+1

    if criterio_linhas(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério das linhas foi satisfeito: O sistema converge.")
    else:
        print("ATENÇÃO: O critério das linhas não foi satisfeito.")

    for k in range(N): # Executa todas as iterações especificadas na chamada da função (N) se a condição de parada não for satisfeita
        for i in range(n):
            for j in range(n):
                if i != j: # Pula os elementos da diagonal principal pois eles não são utilizados
                    aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj

            vetor_aux[i] = (vetor_b[i] - aux)/matriz_A[i,i] # Executa a iteração do método com (bi-Aij*xj)/Aii
            aux = 0 # Zera a contagem do somatório dos valores novamente

        termos_encontrados = 0
        for i in range(n): #Verifica elemento por elemento de vetor_x e do vetor_aux
            if vetor_x[i] != 0:
                if np.abs(vetor_aux[i] - vetor_x[i])/np.abs(vetor_x[i]) <= p: # Condição de parada caso a variação relativa de uma iteração para outra seja menor que a tolerância p
                    termos_encontrados += 1
            else:
                if np.abs(vetor_aux[i] - vetor_x[i]) <= p*np.abs(vetor_x[i]):
                    termos_encontrados += 1

        if(termos_encontrados == n):
            print("(Jacobi) Resultado encontrado com o critério de parada, na iteração:", k)
            return vetor_x

        vetor_x = np.copy(vetor_aux) # Atualiza o vetor_x (k) com os valores do vetor_aux (k+1)

    print("(Jacobi) Resultado encontrado com o número máximo de iterações.")
    return vetor_x # Retorna o vetor solução
```

```python
print(jacobi(matriz, vetor, N=100))
```

## Gauss-Seidel

```python
def seidel(matriz_A, vetor_b, vetor_x=0, N=1000, p=1e-15): # Função que executa o algoritmo de Gauss-Seidel
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que vai auxiliar a armazenar as subtrações dos valores Aij*xj

    if isinstance(vetor_x, int): # Se não é fornecido um vetor de aproximação inicial para x, ele é iniciado aqui com todos os elementos iguais a zero
        vetor_x = np.zeros(m)

    vetor_aux = np.copy(vetor_x) # Vetor auxiliar igual ao vetor x que representa a iteração k+1

    if criterio_linhas(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério das linhas foi satisfeito: O sistema converge.")
    else:
        print("ATENÇÃO: O critério das linhas não foi satisfeito.")

    if criterio_sassenfeld(matriz_A): # Verifica se o critério de Sassenfeld é satisfeito
        print("O critério de Sassenfeld foi satisfeito.")
    else:
        print("ATENÇÃO: O critério de Sassenfeld não foi satisfeito.")

    for k in range(N): # Executa todas as iterações especificadas na chamada da função
        for i in range(n):
            for j in range(m):
                if i != j: # Pula os elementos da diagonal principal pois eles não são utilizados
                    if i > j:
                        aux += matriz_A[i,j] * vetor_aux[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores que já foram atualizados (de 0 a i) que ficam no vetor auxiliar
                    else:
                        aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores ainda não foram atualizados (de i+1 a n) que ficam no vetor x

            vetor_aux[i] = (vetor_b[i] - aux)/matriz_A[i,i] # Executa a iteração do método com (bi-Aij*xj)/Aii
            aux = 0 # Zera a contagem do somatório dos valores novamente

        termos_encontrados = 0
        for i in range(n): #Verifica elemento por elemento de vetor_x e do vetor_aux
            if vetor_x[i] != 0:
                if np.abs(vetor_aux[i] - vetor_x[i])/np.abs(vetor_x[i]) <= p: # Condição de parada caso a variação relativa de uma iteração para outra seja menor que a tolerância p
                    termos_encontrados += 1
            else:
                if np.abs(vetor_aux[i] - vetor_x[i]) <= p*np.abs(vetor_x[i]):
                    termos_encontrados += 1

        if(termos_encontrados == n):
                print("(Seidel) Resultado encontrado com o critério de parada, na iteração:", k)
                return vetor_x

        vetor_x = np.copy(vetor_aux) # Atualiza o vetor_x (k) com os valores do vetor_aux (k+1)

    print("(Seidel) Resultado encontrado com o número máximo de iterações.")
    return vetor_x # Retorna o vetor solução
```

```python
print(seidel(matriz, vetor))
```

## SOR

```python
def calcular_omega_otimo(matriz_A):
    if verifica_pos_def(matriz_A) == False or verifica_tridiagonal(matriz_A, debug=True) == False:
        print("Não há como encontrar w ótimo.")
        return None

    n = len(matriz_A)
    D = np.copy(matriz_A)
    for i in range(n):
        for j in range(n):
            if i != j:
                D[i,j] = 0.

    D = np.linalg.inv(D)
    L = np.tril(matriz_A)
    U = np.triu(matriz_A)

    for i in range(n):
        for j in range(n):
            if i == j:
                L[i,j] = 0.
                U[i,j] = 0.

    LU = L + U
    T = np.matmul(D, LU)
    P = np.linalg.eig(T)
    p = np.amax(P[0])

    print("omega: {}".format(2/(1 + math.sqrt(1 - pow(p, 2)))))
    return 2/(1 + math.sqrt(1 - pow(p, 2)))

```

```python
def sor(matriz_A, vetor_b, vetor_x=0, N=10000, p=1e-15, w=1.7): # Quando w = 1 é igual ao Seidel
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que vai auxiliar a armazenar as subtrações dos valores Aij*xj

    omega = calcular_omega_otimo(matriz_A)

    if omega != None:
        w = omega

    if isinstance(vetor_x, int): # Se não é fornecido um vetor de aproximação inicial para x, ele é iniciado aqui com todos os elementos iguais a zero
        vetor_x = np.zeros(m)

    vetor_aux = np.copy(vetor_x) # Vetor auxiliar igual ao vetor x que representa a iteração k+1

    if criterio_linhas(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério das linhas foi satisfeito: O sistema converge.")
    else:
        print("ATENÇÃO: O critério das linhas não foi satisfeito.")

    if criterio_sassenfeld(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério de Sassenfeld foi satisfeito.")
    else:
        print("ATENÇÃO: O critério de Sassenfeld não foi satisfeito.")

    for k in range(N): # Executa todas as iterações especificadas na chamada da função
        for i in range(n):
            for j in range(m):
                if i != j: # Pula os elementos da diagonal principal pois eles não são utilizados
                    if i > j:
                        aux += matriz_A[i,j] * vetor_aux[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores que já foram atualizados (de 0 a i) que ficam no vetor auxiliar
                    else:
                        aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores ainda não foram atualizados (de i+1 a n) que ficam no vetor x

            vetor_aux[i] = (1-w)*vetor_x[i] + (w*(vetor_b[i] - aux))/matriz_A[i,i] # Executa a iteração do método com (1-w)*xi(bi-Aij*xj)/Aii
            aux = 0 # Zera a contagem do somatório dos valores novamente

        termos_encontrados = 0
        for i in range(n): #Verifica elemento por elemento de vetor_x e do vetor_aux
            if vetor_x[i] != 0:
                if np.abs(vetor_aux[i] - vetor_x[i])/np.abs(vetor_x[i]) <= p: # Condição de parada caso a variação relativa de uma iteração para outra seja menor que a tolerância p
                    termos_encontrados += 1
            else:
                if np.abs(vetor_aux[i] - vetor_x[i]) <= p*np.abs(vetor_x[i]):
                    termos_encontrados += 1

        if(termos_encontrados == n):
                print("(SOR) Resultado encontrado com o critério de parada, na iteração:", k)
                return vetor_x

        vetor_x = np.copy(vetor_aux) # Atualiza o vetor_x (k) com os valores do vetor_aux (k+1)

    print("(SOR) Resultado encontrado com o número máximo de iterações:")
    return vetor_x # Retorna o vetor solução
```

```python
sor(matriz, vetor)
```

---
### 5) Implemente um algoritmo que leia uma matriz A (de uma planilha excel) e:

```python
def ler_matriz_A(file_matrix):
    df_matriz = pd.read_excel(r"dados/"+file_matrix+".xlsx", header=None, sheet_name="A") # Le o arquivo do excel que contém a matriz A sem cabeçalho
    matriz = np.array(df_matriz, dtype='f8') # Converte a matriz A em um array da numpy
    return matriz
```

```python
matriz = ler_matriz_A("prova_ALC_testes")
```

**a)** Calcule a transposta de A;

```python
def calcula_transposta(matriz_A):
    if not verifica_quadrada(matriz_A):
        print("A matriz não é quadrada.")
        return None

    n = len(matriz_A)
    matriz_At = np.zeros((n,n), dtype='f8')

    for i in range(n):
        for j in range(n):
            if i == j:
                matriz_At[i,j] = matriz_A[i,j]
            else:
                matriz_At[i,j] = matriz_A[j,i]

    return matriz_At
```

```python
calcula_transposta(matriz)
```

**b)** Calcule o determinante de A;

```python
def eliminacao_gauss(matriz_A):
    n = len(matriz_A)
    Am = np.array(matriz_A).astype("float64")

    for i in range(n):
        for j in range(i+1, n):
            if Am[i,i] == 0:
                Am[i,i] = 1e-18

            x = Am[j,i]/Am[i,i] # x é o multiplicador necessário para executar a eliminação
            for k in range(n):
               Am[j,k] -= Am[i,k] * x # aplica o multiplicador x para zerar os elementos abaixo da diagonal principal

    return calcula_determinante(Am, n) # retorna o vetor resultado

def calcula_determinante(matriz_A, n):
    det = 1
    for i in range(n):
        det *= matriz_A[i,i]
    return det
```

```python
eliminacao_gauss(matriz)
```

**c)** Calcule a matriz adjunta de A;

```python
def calcula_adjunta(matriz_A):
    if verifica_inversa(matriz_A):
        matriz_Adj = np.linalg.inv(matriz_A) * eliminacao_gauss(matriz_A)
        return matriz_Adj
```

```python
print(calcula_adjunta(matriz))
```

**d)** Calcule o traço de A;

```python
def traco(matriz_A):
    linhas, colunas = np.shape(matriz_A)
    n, traco = min((linhas, colunas)), 0

    for i in range(n):
        traco += matriz_A[i,i]
    return traco

```

```python
print("Traço de A:", traco(matriz))
```

**e)** Avalie se A é simétrica;

```python
def verifica_simetria(matriz_A):
    if verifica_quadrada(matriz_A):
        n = len(matriz_A)
        for i in range(n):
            for j in range(n):
                if (i != j) and (matriz_A[i,j] != matriz_A[j,i]):
                    print("A matriz não é simétrica")
                    return False
        print("A matriz é simétrica")
        return True
    print("A matriz não é quadrada")
    return False
```

```python
verifica_simetria(matriz)
```

**f)** Avalie se A é ortogonal;

```python
def verifica_ortogonal(matriz_A):
    if not verifica_quadrada:
        print("A matriz não é quadrada.")
        return False

    if not verifica_inversa:
        print("A matriz é singular.")
        return False

    n = len(matriz_A)
    matriz_At = calcula_transposta(matriz_A)
    matriz_Id = np.identity(n)

    matriz_Resultado = np.matmul(matriz_A, matriz_At)

    if (matriz_Resultado-matriz_Id).all() <= 1e-10:
        print("A matriz é ortogonal.")
        return True
    else:
        print("A matriz não é ortogonal.")
        return False
```

```python
verifica_ortogonal(matriz)
```

**g)** Calcule a norma de linha, norma de Frobenius e norma coluna de A.

```python
def soma_linha(matriz_A): # norma-infinita matricial
    n, max, x = len(matriz_A), 0, 0

    for i in range(n):
        for j in range(n):
            x += math.fabs(matriz_A[i,j])

        if x > max:
            max = x

    return max
```

```python
soma_linha(matriz)
```

```python
def frobenius(matriz_A): # norma-2 matricial
    n, x = len(matriz_A), 0

    for i in range(n):
        for j in range(n):
            x += math.pow(math.fabs(matriz_A[i,j]), 2)

    return x ** (1/2)
```

```python
frobenius(matriz)
```

```python
def soma_coluna(matriz_A): # norma-1 matricial
    n, max, x = len(matriz_A), 0, 0

    for j in range(n):
        for i in range(n):
            x += math.fabs(matriz_A[i,j])

        if x > max:
            max = x

    return max
```

```python
soma_coluna(matriz)
```

**h)** Avalie se A é positiva definida.

```python
def verifica_pos_def(matriz_A):
    if verifica_inversa(matriz_A) and verifica_simetria(matriz_A):
        n = len(matriz_A)
        for i in range(n):
            soma = 0
            for j in range(n):
                soma += matriz_A[i,j]
            if(soma == 0 or matriz_A[i,i] <= 0):
                print("A matriz não é positiva definida.")
                return False
        print("A matriz é positiva definida.")
        return True
    print("A matriz não é positiva definida.")
    return False
```

```python
verifica_pos_def(matriz)
```

---
### 6) Construa um algoritmo que receba dois vetores x e y e uma matriz A positiva definida (de uma planilha excel). Calcule:

```python
def ler_matriz_A_vetor_x_vetor_y(file_name, file_matrix, file_array_x, file_array_y):
    df_matriz = pd.read_excel(r"dados/"+file_name+".xlsx", header=None, sheet_name=file_matrix) # Le o arquivo do excel que contém a matriz A sem cabeçalho
    df_vetor_x = pd.read_excel(r"dados/"+file_name+".xlsx", header=None, sheet_name=file_array_x) # Le o arquivo do excel que contém o vetor x sem cabeçalho
    df_vetor_y = pd.read_excel(r"dados/"+file_name+".xlsx", header=None, sheet_name=file_array_y) # Le o arquivo do excel que contém o vetor y sem cabeçalho
    matriz = np.array(df_matriz, dtype='f8') # Converte a matriz A em um array da numpy
    vetor_x = np.ravel(np.array(df_vetor_x, dtype='f8')) # Converte o vetor b em um array da numpy
    vetor_y = np.ravel(np.array(df_vetor_y, dtype='f8')) # Converte o vetor b em um array da numpy
    return matriz, vetor_x, vetor_y

```

```python
matriz, vetor_x, vetor_y = ler_matriz_A_vetor_x_vetor_y("prova_ALC_testes(1)", "matriz_A(2)", "vetor_b(1)", "vetor_b(1)")
```

**a)** Norma 1, norma 2 e norma infinito de x e y.

```python
def manhattan(vetor): # norma-1 vetorial
    n, x = len(vetor), 0

    for i in range(n):
        x += np.fabs(vetor[i])
    return x
```

```python
print(manhattan(vetor_x))
print(manhattan(vetor_y))
```

```python
def euclidiana(vetor): # norma-2 vetorial
    n, x = len(vetor), 0

    for i in range(n):
        x += np.fabs(vetor[i]) ** 2

    return x ** (1/2)
```

```python
print(euclidiana(vetor_x))
print(euclidiana(vetor_y))
```

```python
def infinita(vetor): # norma-infinita vetorial
    n, max = len(vetor), vetor[0]

    for i in range(1, n):
        if np.fabs(vetor[i]) > max:
            max = np.fabs(vetor[i])

    return max
```

```python
print(infinita(vetor_x))
print(infinita(vetor_y))
```

**b)** Ângulo entre x e y;

```python
def produto_interno(vetor_x, vetor_y):
    n, p_interno = len(vetor_x), 0

    for i in range(n):
        p_interno += vetor_x[i] * vetor_y[i]

    return p_interno

def angulo_vetores(vetor_x, vetor_y):
    n = len(vetor_x)

    p_interno = produto_interno(vetor_x, vetor_y)
    norma_x = euclidiana(vetor_x)
    norma_y = euclidiana(vetor_y)
    p_normas = norma_x * norma_y

    div = p_interno/p_normas

    return np.arccos(div)
```

```python
angulo_vetores(vetor_x, vetor_y)
```

**c)** Norma de x e y induzida por A;

```python
def matricial_induzida(matriz_A, vetor_b, norma=manhattan):

    if norma == manhattan:
        mat = np.matmul(matriz_A, vetor_b)
        return manhattan(mat)/manhattan(vetor_b)

    elif norma == euclidiana:
        mat = np.matmul(matriz_A, vetor_b)
        return euclidiana(mat)/euclidiana(vetor_b)

    elif norma == infinita:
        mat = np.matmul(matriz_A, vetor_b)
        return infinita(mat)/infinita(vetor_b)
```

```python
matricial_induzida(matriz, vetor_x)
matricial_induzida(matriz, vetor_y)
```

**d)** Produto interno de x e y.

```python
def produto_interno(vetor_x, vetor_y):
    n, p_interno = len(vetor_x), 0

    for i in range(n):
        p_interno += vetor_x[i] * vetor_y[i]

    return p_interno
```

```python
produto_interno(vetor_x, vetor_y)
```

---
### 7) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel). Encontre a fatoração LU de A.

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes")
```

```python
fatoracaoLU(matriz,vetor)
```

---
### 8) Construa um algoritmo que receba uma matriz A positiva (de uma planilha excel). Calcule:

```python
matriz = ler_matriz_A("prova_ALC_testes")
```

**a)** O número condição;

**b)** Retorne uma mensagem ao usuário sobre sua interpretação.

```python
def condition_number(matriz):
    inv = np.linalg.inv(matriz)
    kond = frobenius(matriz) * frobenius(inv)
    print("Número condição: {}".format(kond))
    print("Para um sistema linear com essa matriz, o erro esperado causado por uma perturbação em b vai será de aproximadamente:")
    return kond
```

```python
condition_number(matriz)
```

---
### 9) Construa um algoritmo que receba três matrizes: U, sigma e V (de uma planilha excel) e:

```python
def ler_matrizes_USV(file_excel):
    df = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name="SVD")
    data = np.array(df)

    n, m = np.shape(data)
    nan_find = ~np.isnan(data).all(axis=0)
    nan_index = []

    for i in range(len(nan_find)):
        if nan_find[i] == False:
            nan_index.append(i)

    data_U = data[:,:nan_index[0]]
    data_S = data[:,nan_index[0]+1:nan_index[1]]
    data_Vt = data[:,nan_index[1]+1:m]

    matriz_U = data_U[~np.isnan(data_U).all(axis=1)]
    matriz_S = data_S[~np.isnan(data_S).all(axis=1)]
    matriz_Vt = data_Vt[~np.isnan(data_Vt).all(axis=1)]

    return matriz_U, matriz_S, matriz_Vt
```

```python
matriz_U, matriz_S, matriz_Vt = ler_matrizes_USV("prova_ALC_testes")
```

**a)** Avalie se o produto das três matrizes representa uma decomposição SVD de alguma matriz X;

**b)** Caso a resposta à pergunta anterior seja negativa, retorne ao usuário uma explicação justificando esse fato.

```python
# U e V ortogonais e S não-quadrada e diagonal
def verifica_diagonal(matriz):
    n, m = np.shape(matriz)

    for i in range(n):
        for j in range(m):
            if i != j and matriz[i,j] != 0:
                print("A matriz não é diagonal.")
                return False
    print("A matriz é diagonal.")
    return True

def verifica_SVD(matriz_U, matriz_S, matriz_Vt):
    print("Para as matrizes USVt serem a decomposição SVD de uma matriz X:\n-> U e Vt devem ser ortogonais.\n-> S deve ser diagonal não-quadrada.\n")
    print("Verificando se U é ortogonal...")
    if verifica_ortogonal(matriz_U):
        print("\nVerificando se Vt é ortogonal...")
        if verifica_ortogonal(matriz_Vt):
            print("\nVerificando se S é diagonal e não-quadrada...")
            if verifica_diagonal(matriz_S):
                nS, mS = np.shape(matriz_S)
                if nS != mS:
                    print("A matriz é não-quadrada.")
                    print("")
                    mU = len(matriz_U)
                    nVt = len(matriz_Vt)

                    if mU == nS and mS == nVt:
                        print("As matrizes são a decomposição SVD da matriz X abaixo:")
                        matriz_X = np.matmul(np.matmul(matriz_U, matriz_S), matriz_Vt)
                        print(matriz_X)

                        return True
                    else:
                        print("As matrizes não têm dimensões compatíveis para executar um produto de matrizes.")
                        return False
                else:
                    print("A matriz S é quadrada.")
                    return False
            else:
                print("A matriz S não é diagonal.")
                return False
        else:
            print("A matriz Vt não é ortogonal.")
            return False
    else:
        print("A matriz U não é ortogonal.")
        return False
```

```python
verifica_SVD(matriz_U, matriz_S, matriz_Vt)
```

---
### 10) Construa um algoritmo que leia uma planilha em Excel com 4 colunas e calcule a estatística de Benford (para 1 dígito) para cada uma das colunas.

```python
def ler_dados_benford(file_name): # Le o arquivo do excel que contém os dados para testar o algoritmo de ocorrências (Benford)
    df_benford = pd.read_excel(r"dados/"+file_name+".xlsx", header=None)
    dataset_benford = np.array(df_benford) # Converte a matriz de dados em um array numpy
    return dataset_benford

def benford(dados):
    n, m = np.shape(dados)
    primeiro_digito = np.zeros((n,m)) # Matriz auxiliar para armazenar o primeiro dígito de cada posição da matriz de dados

    # Looping para armazenar os primeiros dígitos dos elementos da matriz de dados na matriz primeiro_digito
    for j in range(m):
        for i in range(n):
            num = [x for x in str(dados[i,j])]
            for value in num:
                if value != '0' and value != '.' and value != '-':
                    primeiro_digito[i,j] = int(value)
                    break

    # Verifica as ocorrências de 1 a 9 na matriz primeiro_digito por colunas
    for i in range(1, 10):
        count = np.count_nonzero(primeiro_digito == i, axis=0)
        # Mostra a porcentagem de ocorrências pra cada ocorrência de 1 a 9 da matriz primeiro_digito
        for j in range(m):
            if j == 0:
                print('Porcentagem de ocorrência de primeiro dígito "'+str(i)+'" em cada coluna:', end=' ')
            print("{:.4f}".format((count[j]/n)*100)+'%', end=' | ')
        print('')
```

```python
benford(ler_dados_benford("dados_benford"))
```