<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 The inverse Matrix Theorem
The inverse matrix theorem is a statement about a number of equivalent conditions that we can impose on a matrix.
Given a matrix $A$ with shape $(n, n)$
1.  $Det(A) \neq 0$ implies $A^{-1}$ exists.
2.  We can find a Matrix B such that $A * B = B * A = np.eye(n) = I_n$
3.  $Det(A) = \lambda _1 * \lambda _2 * ... * \lambda _n$ where $\lambda _i$ is an eigenvalue of the matrix.
These require some explanation.
Determinants.
Linear Transformations may stretch the underlying space by some magnitude (eigenvalues) along certain vectors (eigenvectors).  These are the vectors and values which "characterize a matrix." -- Hence the word eigen.
Step1:  Hmm.... is this related to anything??
Step2:  Hmm... Interesting.
The Determinant of a matrix is just the product of it's eigenvalues.
Step3:  It looks like the eigenvectors are normalized to length 1.
Let's look at another matrix and look at its eigenvalues and eigenvectors.  This one should be a bit simpler.
Step4:  Hmm.... what is happening there, why does it have imaginary eigenvectors??
Step5:  This looks like a twist to me.  It's sending 
$$ [1, 0]^T \mapsto [0,1]^T$$
$$ [0, 1]^T \mapsto [-1,0]^T$$
If it's a twist... how can it have a direction it stretches something in a straight line in... we'd have to imagine really hard...
Oh.
That's why it has imaginary eigenvectors -- they don't really exist.  But they do symbolically and mathematically.
Well... according to the inverse matrix theorem, the fact that $Det(A) = 1 \neq 0$ implies that it has an inverse matrix.  Lets check that out.
Step6:  Hmm...
$$ [1,0]^T \mapsto [0,-1]^T \ [0,1]^T \mapsto [1,0]^T $$
Oh!
It's a rotation in the opposite direction!!  Neato gang!
What happens if we multiply the two together...?
Step7:  We just get the identity matrix back -- just like we wanted!!
Lets take another example.
Fix a new matrix $A = 2 * I_2$
Step8:  What should the inverse of $A$ be?
<ASSISTANT_TASK:>
Python Code:
import numpy as np
import numpy.linalg as la
A = np.array(range(1,5)).reshape(2,2)
determinant_A = la.det(A)
print(A)
print("Determinant is: {}".format(determinant_A)) # Notice the rounding error.
Explanation: The inverse Matrix Theorem
The inverse matrix theorem is a statement about a number of equivalent conditions that we can impose on a matrix.
Given a matrix $A$ with shape $(n, n)$
1.  $Det(A) \neq 0$ implies $A^{-1}$ exists.
2.  We can find a Matrix B such that $A * B = B * A = np.eye(n) = I_n$
3.  $Det(A) = \lambda _1 * \lambda _2 * ... * \lambda _n$ where $\lambda _i$ is an eigenvalue of the matrix.
These require some explanation.
Determinants.
Linear Transformations may stretch the underlying space by some magnitude (eigenvalues) along certain vectors (eigenvectors).  These are the vectors and values which "characterize a matrix." -- Hence the word eigen.
End of explanation
# Let's check it's eigenvalues.
print("The Matrix A has eigenvalues: {}".format([x for x in la.eigvals(A)]))
print("And their product is: {}".format(np.product(la.eigvals(A))))
determinant_A - np.product(la.eigvals(A)) < 10**-5
# Lets define an epsilon and check if they are the same.
eps = 10 ** -5
def check_floats_equal(float_1, float_2, eps=eps):
    return float_1 - float_2 < eps
check_floats_equal(np.product(la.eigvals(A)), la.det(A))
Explanation: Hmm.... is this related to anything??
End of explanation
vals, vecs = la.eig(A)
print(vecs)
# Lets define a distance function for vectors in a matrix.
def l2_distance_cols(matrix):
    norms = []
    for row in matrix.transpose():
        dist = 0
        for val in row:
            dist += val ** 2
        
        norms.append(dist)
    
    return np.array(norms)
        
l2_distance_cols(vecs)
Explanation: Hmm... Interesting.
The Determinant of a matrix is just the product of it's eigenvalues.
End of explanation
A = np.array([0, -1, 1, 0]).reshape(2, 2)
A
np.linalg.det(A)
vals, vecs = np.linalg.eig(A)
print("Values:\n{}".format(vals))
print("Vectors:\n{}".format(vecs))
Explanation: It looks like the eigenvectors are normalized to length 1.
Let's look at another matrix and look at its eigenvalues and eigenvectors.  This one should be a bit simpler.
End of explanation
# First a quick vector making helper function.
def list_to_vector(items):
    return np.array(items).reshape(len(items), 1)
np.matmul(A, list_to_vector([1,0]))
np.matmul(A, list_to_vector([0,1]))
Explanation: Hmm.... what is happening there, why does it have imaginary eigenvectors??
End of explanation
A_inv = np.linalg.inv(A)
print("Here is A^-1\n{}".format(A_inv))
np.matmul(A_inv, list_to_vector([1,0]))
np.matmul(A_inv, list_to_vector([0,1]))
Explanation: This looks like a twist to me.  It's sending 
$$ [1, 0]^T \mapsto [0,1]^T$$
$$ [0, 1]^T \mapsto [-1,0]^T$$
If it's a twist... how can it have a direction it stretches something in a straight line in... we'd have to imagine really hard...
Oh.
That's why it has imaginary eigenvectors -- they don't really exist.  But they do symbolically and mathematically.
Well... according to the inverse matrix theorem, the fact that $Det(A) = 1 \neq 0$ implies that it has an inverse matrix.  Lets check that out.
End of explanation
I_2 = np.matmul(A, A_inv)
print("{} \n* \n{} \n=\n{}".format(A, A_inv, I_2))
Explanation: Hmm...
$$ [1,0]^T \mapsto [0,-1]^T \ [0,1]^T \mapsto [1,0]^T $$
Oh!
It's a rotation in the opposite direction!!  Neato gang!
What happens if we multiply the two together...?
End of explanation
A = np.eye(2) * 2
print(A)
vals, vecs = np.linalg.eig(A)
print("Vals:\n{}".format(vals))
print("Vecs:\n{}".format(vecs))
Explanation: We just get the identity matrix back -- just like we wanted!!
Lets take another example.
Fix a new matrix $A = 2 * I_2$
End of explanation
A_inv = np.linalg.inv(A)
print("The inverse is:\n{}".format(A_inv))
Explanation: What should the inverse of $A$ be?
End of explanation
<END_TASK>