Spaces:
Runtime error
Runtime error
Added Batman logo, transformation types and updated description
#2
by
bhardwajsatyam
- opened
- app.py +88 -24
- description.md +9 -1
- utils.py +104 -3
app.py
CHANGED
@@ -2,7 +2,11 @@ from matplotlib import pyplot as plt
|
|
2 |
import numpy as np
|
3 |
import streamlit as st
|
4 |
import pandas as pd
|
5 |
-
from utils import getSquareYVectorised, getCircle, transform, plotGridLines
|
|
|
|
|
|
|
|
|
6 |
|
7 |
np.set_printoptions(precision=3)
|
8 |
xlim = (-10,10)
|
@@ -13,50 +17,110 @@ st.write(
|
|
13 |
"This app shows the effect of a 2x2 linear transformation on simple shapes to understand the role of eigenvectors and eigenvalues in quantifying the nature of a transformation.")
|
14 |
|
15 |
with st.sidebar:
|
16 |
-
data = st.selectbox('Select type of dataset', ['Square', 'Circle'])
|
|
|
|
|
|
|
17 |
st.write("---")
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
st.write("---")
|
28 |
st.write("The transformation matrix A is:")
|
29 |
st.table(pd.DataFrame(t))
|
30 |
st.write("---")
|
31 |
-
showNormalSpace = st.checkbox(label= 'Show
|
32 |
|
33 |
-
|
34 |
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
evl, evec = np.linalg.eig(t)
|
42 |
-
det = np.linalg.det(t)
|
43 |
fig, ax = plt.subplots()
|
44 |
|
45 |
if showNormalSpace:
|
46 |
-
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
if not np.iscomplex(evec).any():
|
49 |
ax.quiver(0,0,evec[0,0],evec[1,0],scale=1,scale_units ='xy',angles='xy', facecolor='black', alpha=0.5)
|
50 |
ax.quiver(0,0,evec[0,1],evec[1,1],scale=1,scale_units ='xy',angles='xy', facecolor='black', alpha=0.5)
|
51 |
plotGridLines(xlim,ylim,np.array([[1,0], [0,1]]),'#9D9D9D','Normal Space',0.4)
|
52 |
|
53 |
-
|
54 |
-
ax.plot(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
if not (np.iscomplex(evl).any() or np.iscomplex(evec).any()):
|
56 |
ax.quiver(0,0,evec[0,0]*evl[0],evec[1,0]*evl[0],scale=1,scale_units ='xy',angles='xy', facecolor='cyan', label='$eigen\ vector_{\lambda_0}$')
|
57 |
ax.quiver(0,0,evec[0,1]*evl[1],evec[1,1]*evl[1],scale=1,scale_units ='xy',angles='xy', facecolor='blue', label='$eigen\ vector_{\lambda_1}$')
|
58 |
plotGridLines(xlim,ylim,t,'#403B3B','Transformed space',0.6)
|
59 |
-
ax.text(11,
|
|
|
|
|
|
|
60 |
|
61 |
ax.set_xlim(*xlim)
|
62 |
ax.set_ylim(*ylim)
|
@@ -70,7 +134,7 @@ st.pyplot(fig)
|
|
70 |
|
71 |
df = pd.DataFrame({'Eigenvalues': evl, 'Eigenvectors': [str(evec[:,0]), str(evec[:,1])],\
|
72 |
'Transformed Eigenvectors': [str(evec[:,0]*evl[0]), str(evec[:,1]*evl[1])]})
|
73 |
-
st.table(df)
|
74 |
|
75 |
if np.iscomplex(evl).any() or np.iscomplex(evec).any():
|
76 |
st.write("Due to complex eigenvectors and eigenvalues, the transformed eigenvectors are not\
|
|
|
2 |
import numpy as np
|
3 |
import streamlit as st
|
4 |
import pandas as pd
|
5 |
+
from utils import getSquareYVectorised, getCircle, getBatman, transform, plotGridLines, discriminant
|
6 |
+
|
7 |
+
minv = -5.0
|
8 |
+
maxv = 5.0
|
9 |
+
step = 0.1
|
10 |
|
11 |
np.set_printoptions(precision=3)
|
12 |
xlim = (-10,10)
|
|
|
17 |
"This app shows the effect of a 2x2 linear transformation on simple shapes to understand the role of eigenvectors and eigenvalues in quantifying the nature of a transformation.")
|
18 |
|
19 |
with st.sidebar:
|
20 |
+
data = st.selectbox('Select type of dataset', ['Square', 'Circle', 'Batman'])
|
21 |
+
if data == 'Batman':
|
22 |
+
black = st.checkbox(label='Black')
|
23 |
+
transform_type = st.selectbox('Select type of transformation', ['Custom', 'Stretch', 'Shear', 'Rotate'])
|
24 |
st.write("---")
|
25 |
+
if transform_type == 'Custom':
|
26 |
+
st.markdown("Select elements of transformation matrix $A$")
|
27 |
+
a_00 = st.slider(label = '$a_{00}$', min_value = minv, max_value=maxv, value=1.0, step=step)
|
28 |
+
a_01 = st.slider(label = '$a_{01}$', min_value = minv, max_value=maxv, value=0.0, step=step)
|
29 |
+
a_10 = st.slider(label = '$a_{10}$', min_value = minv, max_value=maxv, value=0.0, step=step)
|
30 |
+
a_11 = st.slider(label = '$a_{11}$', min_value = minv, max_value=maxv, value=1.0, step=step)
|
31 |
+
t = np.array([[a_00, a_01], [a_10, a_11]], dtype=np.float64)
|
32 |
+
elif transform_type == 'Stretch':
|
33 |
+
both = st.checkbox('Set equal')
|
34 |
+
if not both:
|
35 |
+
stretch_x = st.slider(label = 'Stretch in x-direction', min_value = minv, max_value=maxv, value=1.0, step=step)
|
36 |
+
stretch_y = st.slider(label = 'Stretch in y-direction', min_value = minv, max_value=maxv, value=1.0, step=step)
|
37 |
+
t = np.array([[stretch_x, 0], [0, stretch_y]], dtype=np.float64)
|
38 |
+
else:
|
39 |
+
stretch = st.slider(label = 'Scale', min_value = minv, max_value=maxv, value=1.0, step=step)
|
40 |
+
t = np.array([[stretch, 0], [0, stretch]], dtype=np.float64)
|
41 |
+
elif transform_type == 'Shear':
|
42 |
+
left, right = st.columns(2)
|
43 |
+
with left:
|
44 |
+
both = st.checkbox('Set equal')
|
45 |
+
if not both:
|
46 |
+
shear_x = st.slider(label = 'Shear in x-direction', min_value=minv, max_value=maxv, value=0.0, step=step)
|
47 |
+
shear_y = st.slider(label = 'Shear in y-direction', min_value=minv, max_value=maxv, value=0.0, step=step)
|
48 |
+
t = np.array([[1, shear_x], [shear_y, 1]], dtype=np.float64)
|
49 |
+
else:
|
50 |
+
with right:
|
51 |
+
sign = st.checkbox('Opposite sign')
|
52 |
+
shear = st.slider(label = 'Shear in both directions', min_value=minv, max_value=maxv, value=0.0, step=step)
|
53 |
+
t = np.array([[1, -shear], [shear, 1]], dtype=np.float64) if sign else np.array([[1, shear], [shear, 1]], dtype=np.float64)
|
54 |
+
else:
|
55 |
+
st.markdown("Rotate by $\\theta$ in anti-clockwise\ndirection")
|
56 |
+
min_theta = -180.0
|
57 |
+
max_theta = 180.0
|
58 |
+
theta = st.slider(label = '$\\theta$', min_value=min_theta, max_value=max_theta, value=0.0, step=step, format="%f°")
|
59 |
+
rtheta = np.pi * theta/180.0
|
60 |
+
t = np.array([[np.cos(rtheta), -np.sin(rtheta)], [np.sin(rtheta), np.cos(rtheta)]], dtype=np.float64)
|
61 |
st.write("---")
|
62 |
st.write("The transformation matrix A is:")
|
63 |
st.table(pd.DataFrame(t))
|
64 |
st.write("---")
|
65 |
+
showNormalSpace = st.checkbox(label= 'Show original space (without transform)', value=False)
|
66 |
|
|
|
67 |
|
68 |
+
if data == 'Square':
|
69 |
+
x = np.linspace(-1,1,1000)
|
70 |
+
y = getSquareYVectorised(x)
|
71 |
+
elif data == 'Circle':
|
72 |
+
x = np.linspace(-1,1,1000)
|
73 |
+
y = getCircle(x)
|
74 |
+
else:
|
75 |
+
X, Y = getBatman(s=2)
|
76 |
|
77 |
+
if data != 'Batman':
|
78 |
+
x_dash_up, y_dash_up = transform(x,y,t)
|
79 |
+
x_dash_down, y_dash_down = transform(x,-y,t)
|
80 |
+
else:
|
81 |
+
tmp = [transform(x, y, t) for x, y in zip(X, Y)]
|
82 |
+
X_dash = [t[0] for t in tmp]
|
83 |
+
Y_dash = [t[1] for t in tmp]
|
84 |
|
85 |
evl, evec = np.linalg.eig(t)
|
|
|
86 |
fig, ax = plt.subplots()
|
87 |
|
88 |
if showNormalSpace:
|
89 |
+
if data != 'Batman':
|
90 |
+
ax.plot(x, y, 'r', alpha=0.5)
|
91 |
+
ax.plot(x, -y, 'g', alpha=0.5)
|
92 |
+
else:
|
93 |
+
for i, (x, y) in enumerate(zip(X, Y)):
|
94 |
+
if black:
|
95 |
+
ax.plot(x, y, 'k-', alpha=0.5, linewidth=1)
|
96 |
+
elif i < 3:
|
97 |
+
ax.plot(x, y, 'g-', alpha=0.5, linewidth=1)
|
98 |
+
else:
|
99 |
+
ax.plot(x, y, 'r-', alpha=0.5, linewidth=1)
|
100 |
if not np.iscomplex(evec).any():
|
101 |
ax.quiver(0,0,evec[0,0],evec[1,0],scale=1,scale_units ='xy',angles='xy', facecolor='black', alpha=0.5)
|
102 |
ax.quiver(0,0,evec[0,1],evec[1,1],scale=1,scale_units ='xy',angles='xy', facecolor='black', alpha=0.5)
|
103 |
plotGridLines(xlim,ylim,np.array([[1,0], [0,1]]),'#9D9D9D','Normal Space',0.4)
|
104 |
|
105 |
+
if data != 'Batman':
|
106 |
+
ax.plot(x_dash_up,y_dash_up,'r')
|
107 |
+
ax.plot(x_dash_down,y_dash_down, 'g')
|
108 |
+
else:
|
109 |
+
for i, (x, y) in enumerate(zip(X_dash, Y_dash)):
|
110 |
+
if black:
|
111 |
+
ax.plot(x, y, 'k-', linewidth=1)
|
112 |
+
elif i < 3:
|
113 |
+
ax.plot(x, y, 'g', linewidth=1)
|
114 |
+
else:
|
115 |
+
ax.plot(x, y, 'r', linewidth=1)
|
116 |
if not (np.iscomplex(evl).any() or np.iscomplex(evec).any()):
|
117 |
ax.quiver(0,0,evec[0,0]*evl[0],evec[1,0]*evl[0],scale=1,scale_units ='xy',angles='xy', facecolor='cyan', label='$eigen\ vector_{\lambda_0}$')
|
118 |
ax.quiver(0,0,evec[0,1]*evl[1],evec[1,1]*evl[1],scale=1,scale_units ='xy',angles='xy', facecolor='blue', label='$eigen\ vector_{\lambda_1}$')
|
119 |
plotGridLines(xlim,ylim,t,'#403B3B','Transformed space',0.6)
|
120 |
+
ax.text(11,3,'|A|={:.2f}'.format(np.linalg.det(t)), fontdict={'fontsize':11})
|
121 |
+
ax.text(11,2,'D = {:.2f}'.format(discriminant(t)), fontdict={'fontsize':11})
|
122 |
+
if discriminant(t) < 0:
|
123 |
+
ax.text(13,1,'Negative!'.format(discriminant(t)), fontdict={'fontsize':8})
|
124 |
|
125 |
ax.set_xlim(*xlim)
|
126 |
ax.set_ylim(*ylim)
|
|
|
134 |
|
135 |
df = pd.DataFrame({'Eigenvalues': evl, 'Eigenvectors': [str(evec[:,0]), str(evec[:,1])],\
|
136 |
'Transformed Eigenvectors': [str(evec[:,0]*evl[0]), str(evec[:,1]*evl[1])]})
|
137 |
+
st.table(df.style.format({'Eigenvalues':'{:.2f}'}))
|
138 |
|
139 |
if np.iscomplex(evl).any() or np.iscomplex(evec).any():
|
140 |
st.write("Due to complex eigenvectors and eigenvalues, the transformed eigenvectors are not\
|
description.md
CHANGED
@@ -39,4 +39,12 @@ When the matrix $A$ is singular, then the transformed space collapses to a line.
|
|
39 |
|
40 |
Solving for $\lambda$, we get $$ \frac{1}{2} \left(a_{00}+a_{11}\pm \sqrt{a_{00}^2-2 a_{11} a_{00}+a_{11}^2+4 a_{01} a_{10}}\right) $$
|
41 |
|
42 |
-
The quantity under the square root sign is called the Discriminant, denoted by $D$. When $D < 0$, the eigenvalues and consequently the eigenvectors are complex. On the other hand, when $A$ is symmetric $a_{01} = a_{10}$, then the discriminant is always positive and the eigendecomposition is real.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
|
40 |
Solving for $\lambda$, we get $$ \frac{1}{2} \left(a_{00}+a_{11}\pm \sqrt{a_{00}^2-2 a_{11} a_{00}+a_{11}^2+4 a_{01} a_{10}}\right) $$
|
41 |
|
42 |
+
The quantity under the square root sign is called the Discriminant, denoted by $D$. When $D < 0$, the eigenvalues and consequently the eigenvectors are complex. On the other hand, when $A$ is symmetric $a_{01} = a_{10}$, then the discriminant is always positive and the eigendecomposition is real.
|
43 |
+
|
44 |
+
Some common transformations include
|
45 |
+
|
46 |
+
| Name | Matrix | Explanation |
|
47 |
+
|:----:|:--------------------:|:-----:|
|
48 |
+
|Stretch |$\begin{bmatrix} s_{x} & 0 \\ 0 & s_{y} \end{bmatrix}$| Streches by $s_x$ in $x$-direction and by $s_y$ in the $y$-direction. When $s_x = s_y = s$, this is equivalent to scaling by $s$ |
|
49 |
+
|Shear| $\begin{bmatrix} 1 & s_{x} \\ s_{y} & 1 \end{bmatrix}$| Shears simultaneously by $s_x$ in $x$-direction and by $s_y$ in the $y$-direction. When $s_x = -s_y$, this is equivalent to rotate and scale. |
|
50 |
+
|Rotate| $\begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}$| Rotation by $\theta$ in the anti-clockwise direction. Since all vectors rotate under this transformation, the eigenvalues and eigenvectors are complex. |
|
utils.py
CHANGED
@@ -10,10 +10,10 @@ def getSquareY(x):
|
|
10 |
getSquareYVectorised = np.vectorize(getSquareY)
|
11 |
|
12 |
def getCircle(x):
|
13 |
-
return np.sqrt(1-np.square(x))
|
14 |
|
15 |
def transform(x,y,t):
|
16 |
-
points = np.array([x,y])
|
17 |
result = t @ points
|
18 |
return result[0,:], result[1,:]
|
19 |
|
@@ -30,4 +30,105 @@ def plotGridLines(xlim,ylim,t,color,label,linewidth):
|
|
30 |
y = [i,i]
|
31 |
x = [xlim[0]-20,xlim[1]+20]
|
32 |
x,y = transform(x,y,t)
|
33 |
-
plt.plot(x,y, color=color,linestyle='dashed',linewidth=linewidth)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
getSquareYVectorised = np.vectorize(getSquareY)
|
11 |
|
12 |
def getCircle(x):
|
13 |
+
return np.sqrt(1 - np.square(x))
|
14 |
|
15 |
def transform(x,y,t):
|
16 |
+
points = np.array([x, y])
|
17 |
result = t @ points
|
18 |
return result[0,:], result[1,:]
|
19 |
|
|
|
30 |
y = [i,i]
|
31 |
x = [xlim[0]-20,xlim[1]+20]
|
32 |
x,y = transform(x,y,t)
|
33 |
+
plt.plot(x,y, color=color,linestyle='dashed',linewidth=linewidth)
|
34 |
+
|
35 |
+
def discriminant(t):
|
36 |
+
return t[0,0]**2 - 2*t[1,1]*t[0,0] + t[1,1]**2 + 4*t[0,1]*t[1,0]
|
37 |
+
|
38 |
+
def getBatman(s=2):
|
39 |
+
X = []
|
40 |
+
Y = []
|
41 |
+
|
42 |
+
# lower
|
43 |
+
x = np.linspace(-4, 4, 1600)
|
44 |
+
y = np.zeros((0))
|
45 |
+
for px in x:
|
46 |
+
y = np.append(y,abs(px/2)- 0.09137*px**2 + np.sqrt(1-(abs(abs(px)-2)-1)**2) -3)
|
47 |
+
X.append(x/s)
|
48 |
+
Y.append(y/s)
|
49 |
+
|
50 |
+
# lower left
|
51 |
+
x = np.linspace(-7., -4, 300)
|
52 |
+
y = np.zeros((0))
|
53 |
+
for px in x:
|
54 |
+
y = np.append(y, -3*np.sqrt(-(px/7)**2+1))
|
55 |
+
X.append(x/s)
|
56 |
+
Y.append(y/s)
|
57 |
+
|
58 |
+
# lower right
|
59 |
+
x = np.linspace(4, 7, 300)
|
60 |
+
y = np.zeros((0))
|
61 |
+
for px in x:
|
62 |
+
y = np.append(y, -3*np.sqrt(-(px/7)**2+1))
|
63 |
+
X.append(x/s)
|
64 |
+
Y.append(y/s)
|
65 |
+
|
66 |
+
# top left
|
67 |
+
x = np.linspace(-7, -2.95, 300)
|
68 |
+
y = np.zeros((0))
|
69 |
+
for px in x:
|
70 |
+
y = np.append(y, 3*np.sqrt(-(px/7)**2+1))
|
71 |
+
X.append(x/s)
|
72 |
+
Y.append(y/s)
|
73 |
+
|
74 |
+
# top right
|
75 |
+
x = np.linspace(2.95, 7, 300)
|
76 |
+
y = np.zeros((0))
|
77 |
+
for px in x:
|
78 |
+
y = np.append(y, 3*np.sqrt(-(px/7)**2+1))
|
79 |
+
X.append(x/s)
|
80 |
+
Y.append(y/s)
|
81 |
+
|
82 |
+
# left ear left
|
83 |
+
x = np.linspace(-1, -.77, 2)
|
84 |
+
y = np.zeros((0))
|
85 |
+
for px in x:
|
86 |
+
y = np.append(y, 9-8*abs(px))
|
87 |
+
X.append(x/s)
|
88 |
+
Y.append(y/s)
|
89 |
+
|
90 |
+
# right ear right
|
91 |
+
x = np.linspace(.77, 1, 2)
|
92 |
+
y = np.zeros((0))
|
93 |
+
for px in x:
|
94 |
+
y = np.append(y, 9-8*abs(px))
|
95 |
+
X.append(x/s)
|
96 |
+
Y.append(y/s)
|
97 |
+
|
98 |
+
# mid
|
99 |
+
x = np.linspace(-.43, .43, 100)
|
100 |
+
y = np.zeros((0))
|
101 |
+
for px in x:
|
102 |
+
y = np.append(y,2)
|
103 |
+
X.append(x/s)
|
104 |
+
Y.append(y/s)
|
105 |
+
|
106 |
+
x = np.linspace(-2.91, -1, 100)
|
107 |
+
y = np.zeros((0))
|
108 |
+
for px in x:
|
109 |
+
y = np.append(y, 1.5 - .5*abs(px) - 1.89736*(np.sqrt(3-px**2+2*abs(px))-2) )
|
110 |
+
X.append(x/s)
|
111 |
+
Y.append(y/s)
|
112 |
+
|
113 |
+
x = np.linspace(1, 2.91, 100)
|
114 |
+
y = np.zeros((0))
|
115 |
+
for px in x:
|
116 |
+
y = np.append(y, 1.5 - .5*abs(px) - 1.89736*(np.sqrt(3-px**2+2*abs(px))-2) )
|
117 |
+
X.append(x/s)
|
118 |
+
Y.append(y/s)
|
119 |
+
|
120 |
+
x = np.linspace(-.7,-.43, 10)
|
121 |
+
y = np.zeros((0))
|
122 |
+
for px in x:
|
123 |
+
y = np.append(y, 3*abs(px)+.75)
|
124 |
+
X.append(x/s)
|
125 |
+
Y.append(y/s)
|
126 |
+
|
127 |
+
x = np.linspace(.43, .7, 10)
|
128 |
+
y = np.zeros((0))
|
129 |
+
for px in x:
|
130 |
+
y = np.append(y, 3*abs(px)+.75)
|
131 |
+
X.append(x/s)
|
132 |
+
Y.append(y/s)
|
133 |
+
|
134 |
+
return X, Y
|