<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 <center>
    <img src="http
Step1:  <div id='intro' />
Introduction
Back to TOC
In this jupyter notebook we will analyze two different way for solving a linear system of equations. The main idea is to verify computationally that translating from a theoretical solution into a computational one is not necesarily a straightforward task and we must face some decisions.
The classical problem we will be solving is the following
Step2:  The other algorithm we will use is implemented in the function np.linalg.solve of NumPy.
In both cases, they receive the matrix $A$ and the right-hand-side $\mathbf{b}$.
Before we continue the analysis, we need to briefly explain how to build a random matrix $A$ and a random right-hand-side vector $\mathbf{b}$, this is achived in the next code
Step3:  The following code compares the solution obtained by each approach for the small problem.
Step4:  We should see that the output x1 and x2 are the same. More on this will be study in the course, what we will study now (and also later) is the time it takes to find $\mathbf{x}$ by both algorihtms.
Step5:  Do you notice a difference in the computation time?
To be able to undertand in more detail the tendency we need to test it for several values of n.
Step6:  The following code solve 10 linear systems of equations from size $10\times 10$ upto $2000\times 2000$, and for each one we compute the average time for several ejecutions.
<ASSISTANT_TASK:>
Python Code:
import numpy as np
import matplotlib.pyplot as plt
Explanation: <center>
    <img src="http://sct.inf.utfsm.cl/wp-content/uploads/2020/04/logo_di.png" style="width:60%">
    <h1> INF-285 - Computación Científica </h1>
    <h2> Solving a linear system of equations </h2>
    <h2> <a href="#acknowledgements"> [S]cientific [C]omputing [T]eam </a> </h2>
    <h2> Version: 1.00 </h2>
</center>
<div id='toc' />
Table of Contents
Introduction
Acknowledgements
End of explanation
# This function solves a linear system of equation 
# in a NOT recomended way, we just include it here
# for comparison purposes. Please DON'T do this!
def solve_inverse(A,b):
    B = np.linalg.inv(A) # FORBIDDEN!!
    x = np.dot(B,b)
    return x
Explanation: <div id='intro' />
Introduction
Back to TOC
In this jupyter notebook we will analyze two different way for solving a linear system of equations. The main idea is to verify computationally that translating from a theoretical solution into a computational one is not necesarily a straightforward task and we must face some decisions.
The classical problem we will be solving is the following:
$$
\begin{equation}
    A\,\mathbf{x} = \mathbf{b}, \quad A \in \mathbb{R}^{n\times n}, \mathbf{x} \in \mathbb{R}^n, \text{ and } \mathbf{b} \in \mathbb{R}^n,
\end{equation}
$$
where we know $A$ and $\mathbf{b}$, and we are looking for $\mathbf{x}$.
The representation of the solution is the following:
$$
\begin{equation}
    \mathbf{x}=A^{-1}\,\mathbf{b}.
\end{equation}
$$
This way for finding $\mathbf{x}$ can be implemented in two steps:
1. Compute $A^{-1}$,
2. Multiply $A^{-1}$ by $\mathbf{b}$.
This approach has been implemented in the function solve_inverse.
End of explanation
# We use this to ensure the output is reproducible,
# what we do is to set a initial seed for the 
# pseudo-random number genrator.
np.random.seed(0)
# Here we define the dimension of the problem we will study
n = 10
# Here we generate 'A' and 'b'.
A = np.random.random((n,n)) # Warning: We hope the matrix is not singular!
b = np.random.random(n)
Explanation: The other algorithm we will use is implemented in the function np.linalg.solve of NumPy.
In both cases, they receive the matrix $A$ and the right-hand-side $\mathbf{b}$.
Before we continue the analysis, we need to briefly explain how to build a random matrix $A$ and a random right-hand-side vector $\mathbf{b}$, this is achived in the next code:
End of explanation
x1 = np.linalg.solve(A,b)
print('x1: ',x1)
x2 = solve_inverse(A,b)
print('x2: ',x2)
Explanation: The following code compares the solution obtained by each approach for the small problem.
End of explanation
%timeit np.linalg.solve(A,b)
%timeit solve_inverse(A,b)
Explanation: We should see that the output x1 and x2 are the same. More on this will be study in the course, what we will study now (and also later) is the time it takes to find $\mathbf{x}$ by both algorihtms.
End of explanation
# This line of code will help us to manipulate the output.
out = %timeit -o -q -n 10 -r 5 np.linalg.solve(A,b)
out.all_runs
Explanation: Do you notice a difference in the computation time?
To be able to undertand in more detail the tendency we need to test it for several values of n.
End of explanation
def generate_data_Axb():
    Ns = np.linspace(10,2000,10, dtype=int)
    time_solve = np.zeros(len(Ns))
    time_inv   = np.zeros(len(Ns))
    np.random.seed(0)
    for i, n in np.ndenumerate(Ns):
        print('working on n =',n)
        A = np.random.random((n,n))
        b = np.random.random(n)
        out = %timeit -o -q -n 5 -r 5 np.linalg.solve(A,b)
        time_solve[i] = out.average
        out = %timeit -o -q -n 5 -r 5 solve_inverse(A,b)
        time_inv[i]   = out.average
        print('time it took =',time_solve[i]+time_inv[i],'[sec]')
    return Ns, time_solve, time_inv
Ns, time_solve, time_inv = generate_data_Axb()
plt.figure(figsize=(5,5))
plt.semilogy(Ns, time_solve, '.', label="np.linalg.solve")
plt.semilogy(Ns, time_inv,   '.', label="solve_inverse")
plt.grid(True)
plt.ylabel('Time in [sec]')
plt.xlabel('Dimension of linear system')
plt.legend(loc='best')
plt.show()
plt.figure(figsize=(5,5))
plt.plot(Ns, time_inv/time_solve, 'k.', label="time_inv/time_solve")
plt.grid(True)
plt.ylabel('Time in [sec]')
plt.xlabel('Dimension of linear system')
plt.legend(loc='best')
plt.show()
plt.figure(figsize=(5,5))
plt.loglog(Ns, time_solve, '.', label="np.linalg.solve")
plt.loglog(Ns, time_inv,   '.', label="solve_inverse")
plt.grid(True)
plt.ylabel('Time in [sec]')
plt.xlabel('Dimension of linear system')
plt.legend(loc='best')
plt.show()
Explanation: The following code solve 10 linear systems of equations from size $10\times 10$ upto $2000\times 2000$, and for each one we compute the average time for several ejecutions.
End of explanation
<END_TASK>