Added Batman logo, transformation types and updated description

#2
Files changed (3) hide show
  1. app.py +88 -24
  2. description.md +9 -1
  3. 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
- st.text("Select elements of transformation\nmatrix (A)")
19
- minv = -5.0
20
- maxv = 5.0
21
- step = 0.1
22
- a_00 = st.slider(label = '$A_{0,0}$', min_value = minv, max_value=maxv, value=1.0, step=step)
23
- a_01 = st.slider(label = '$A_{0,1}$', min_value = minv, max_value=maxv, value=0.0, step=step)
24
- a_10 = st.slider(label = '$A_{1,0}$', min_value = minv, max_value=maxv, value=0.0, step=step)
25
- a_11 = st.slider(label = '$A_{1,1}$', min_value = minv, max_value=maxv, value=1.0, step=step)
26
- t = np.array([[a_00,a_01], [a_10, a_11]], dtype=np.float64)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 normal space (without transform)', value=False)
32
 
33
-
34
 
35
- x = np.linspace(-1,1,1000)
36
- y = getSquareYVectorised(x) if data == 'Square' else getCircle(x)
 
 
 
 
 
 
37
 
38
- x_dash_up, y_dash_up = transform(x,y,t)
39
- x_dash_down, y_dash_down = transform(x,-y,t)
 
 
 
 
 
40
 
41
  evl, evec = np.linalg.eig(t)
42
- det = np.linalg.det(t)
43
  fig, ax = plt.subplots()
44
 
45
  if showNormalSpace:
46
- ax.plot(x, y, 'r', alpha=0.5)
47
- ax.plot(x, -y, 'g', alpha=0.5)
 
 
 
 
 
 
 
 
 
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
- ax.plot(x_dash_up,y_dash_up,'r')
54
- ax.plot(x_dash_down,y_dash_down, 'g')
 
 
 
 
 
 
 
 
 
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,5,'|A|={:.2f}'.format(det))
 
 
 
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