Without loss of generality (WLOG) \(\omega > 0\).2 For simplicity let \(\omega = 1\). Also let \(\sigma=0\), we will handle general cases at the end of this page.
Example
For a nontrivial example, we choose \[
A_0 = \begin{pmatrix}
1 & -3 & 2 \\
1 & -1 & 1 \\
0 & 0 & -1
\end{pmatrix}
\]
The eigenvalues of \(A_0\) are: \[
\lambda_{1,2} = \pm i\sqrt{2}, \quad \lambda_3 = -1
\]
The corresponding eigenvectors are: \[
q = \begin{pmatrix}
2+i\frac{\sqrt{2}}{2} \\
1-i\frac{\sqrt{2}}{2} \\
0
\end{pmatrix}
\] There are simpler ones, we will see them later, this one is chosen for nice pictures.
Because we want \(\omega = 1\) we will take \(A = \frac{\sqrt{2}}{2} A_0\). The eigenvector \(q\) will be preserved this way and \(\lambda = i\).
Now we split \(q\) into a real and imaginary part:
\[ q = q_1 + iq_2 = Req + iImq\] Clearly \(q_1 \neq 0\) since LHS of the following equation would be real while the RHS would not. Similarly for \(q_2\).
\[Aq=A(q_1 + i q_2) = i(q_1 + i q_2) = -q_2 + iq_1 = Im\bar{q} + Re\bar{q}\]
This equation also implies \[ Aq_1 = -q_2,\; Aq_2=q_1\]
We can now see how A transforms the subspace generated by the real and imaginary part of vector q. Let us denote \(W = \mathop{\mathrm{span}}\{q_1, q_2\}\). These are surely linearly independent, so the dimension of \(W\) is 2.
import matplotlib.pyplot as plt# Create the figure and axisfig, ax = plt.subplots(figsize=(6, 6))# Set the axis limitsax.set_xlim(-1.5, 1.5)ax.set_ylim(-1.5, 1.5)# Add grid and axis labelsax.axhline(0, color='black',linewidth=0.5)ax.axvline(0, color='black',linewidth=0.5)ax.set_xticks(np.arange(-2, 3, 1))ax.set_yticks(np.arange(-2, 3, 1))ax.grid(False)# Plot the vectorsax.quiver(0, 0, *q1, angles='xy', scale_units='xy', scale=1, color='blue', label=r'$q_1 = Req = Re\bar{q}$')ax.quiver(0, 0, *q2, angles='xy', scale_units='xy', scale=1, color='green', label=r'$q_2 = Im q$')ax.quiver(0, 0, *(-q2), angles='xy', scale_units='xy', scale=1, color='red', label=r'$-Im q = Im \bar{q}$')# Add axis labelsax.text(*q1, r'$q_1 = Req = Re\bar{q}$', fontsize=12, color='blue', verticalalignment='bottom', horizontalalignment='left')ax.text(*q2, r'$q_2 = Im q$', fontsize=12, color='green', verticalalignment='top', horizontalalignment='right')ax.text(*(-q2), r'$-Im q = Im \bar{q}$', fontsize=12, color='red', verticalalignment='bottom', horizontalalignment='right')# Set labels for the axes (W)ax.text(2, 0, 'W', fontsize=12, verticalalignment='bottom', horizontalalignment='left')# Hide the grid and axesax.set_axis_off()# Show the plotplt.show()
Note that any vector from this subspace can be a real part of an eigenvector: For \(w_1 \in W\), define \(w_2 = -Aw_1\). One can verify that \(w = w_1 + iw_2\) satisfies \(Aw = iw\) making it an eigenvector with eigenvalue \(i\). We will focus on this subspace.
Note (or a nice result)
Moreover we can choose \(w_1 and w_2\) to be perpendicular (with respect to any inner product \(\langle\cdot,\cdot\rangle\)).
Proof:
If \(\langle q_1, q_2 \rangle = 0\), we are done. If not, WLOG \(\langle q_1, q_2 \rangle \gt 0\). But then
# Create the figure and axisfig, ax = plt.subplots(figsize=(3, 3))# Set the axis limitsax.set_xlim(-1.5, 1.5)ax.set_ylim(-1.5, 1.5)# Add grid and axis labelsax.axhline(0, color='black',linewidth=0.5)ax.axvline(0, color='black',linewidth=0.5)ax.set_xticks(np.arange(-2, 3, 1))ax.set_yticks(np.arange(-2, 3, 1))ax.grid(False)# Plot the vectorsax.quiver(0, 0, *q1, angles='xy', scale_units='xy', scale=1, color='blue', label=r'$q_1$')ax.quiver(0, 0, *q2, angles='xy', scale_units='xy', scale=1, color='green', label=r'$q_2$')# Draw a line from q1 to q2ax.plot([q1[0], q2[0]], [q1[1], q2[1]], color='purple', linestyle='--', linewidth=1, label='Line from $q_1$ to $q_2$')ax.quiver(0, 0, *(-q1), angles='xy', scale_units='xy', scale=1, color='blue', label=r'$q_2$')# Calculate the dot productsdot_q1_q2 = np.dot(q1, q2)dot_q1_q1 = np.dot(q1, q1)dot_q2_q2 = np.dot(q2, q2)# Coefficients of the quadratic equationa = dot_q2_q2-dot_q1_q1b = dot_q1_q1 - dot_q2_q2 +2*dot_q1_q2c =-dot_q1_q2# Solve the quadratic equationt1, t2 = np.roots([a,b,c])# Select the t between 0 and 1t = t1 if0<= t1 <=1else t2w1 = (1-t)*q1 + t*q2 w2 =-np.dot(A,np.array([*w1, 0]))[[0,1]]ax.quiver(0, 0, *w1, angles='xy', scale_units='xy', scale=1, color='red', label=r'$w_1$')ax.quiver(0, 0, *w2, angles='xy', scale_units='xy', scale=1, color='red', label=r'$w_1$')ax.text(*w1, r'$w_1$', fontsize=12, color='red', verticalalignment='top', horizontalalignment='left')ax.text(*w2, r'$w_2$', fontsize=12, color='red', verticalalignment='top', horizontalalignment='left')ax.plot([q2[0], -q1[0]], [q2[1], -q1[1]], color='orange', linestyle='--', linewidth=1, label='Line from $q_1$ to $-q_1$')# ax.quiver(0, 0, *(-q2), angles='xy', scale_units='xy', scale=1, color='red', label=r'$-Im q = Im \bar{q}$')# Add axis labelsax.text(*q1, r'$q_1$', fontsize=12, color='blue', verticalalignment='bottom', horizontalalignment='left')ax.text(*q2, r'$q_2$', fontsize=12, color='green', verticalalignment='top', horizontalalignment='right')# Set labels for the axes (W)ax.text(2, 0, 'W', fontsize=12, verticalalignment='bottom', horizontalalignment='left')# Hide the grid and axesax.set_axis_off()# Show the plotplt.show()
It would definitely be nicer to find a transformation following an elipse instead of straight line.
One application of this is in (linear) differential equations. Proven by picture we see that this ellipse can be found as a solution to \[
\dot{x} = Ax
\]
Code
# Define the differential equation x' = Axdef diff_eq(t, x):return np.dot(A, x)# Initial conditionx0 = np.array([*q1, 0])# Time span for the solutiont_span = (0, 10)t_eval = np.linspace(t_span[0], t_span[1], 400)# Solve the differential equationsol = solve_ivp(diff_eq, t_span, x0, t_eval=t_eval)# Create the figure and axisfig, ax = plt.subplots(figsize=(6, 6))# Set the axis limitsax.set_xlim(-2.2, 2.2)ax.set_ylim(-2.2, 2.2)# Add grid and axis labelsax.axhline(0, color='black',linewidth=0.5)ax.axvline(0, color='black',linewidth=0.5)ax.set_xticks(np.arange(-2, 3, 1))ax.set_yticks(np.arange(-2, 3, 1))ax.grid(False)# Plot the vectorsax.quiver(0, 0, *q1, angles='xy', scale_units='xy', scale=1, color='blue', label=r'$q_1$')ax.quiver(0, 0, *q2, angles='xy', scale_units='xy', scale=1, color='green', label=r'$q_2$')# Draw a line from q1 to q2ax.plot([q1[0], q2[0]], [q1[1], q2[1]], color='black', linestyle='--', linewidth=1, label='Line from $q_1$ to $q_2$')ax.plot([q2[0], -q1[0]], [q2[1], -q1[1]], color='black', linestyle='--', linewidth=1, label='Line from $q_1$ to $-q_1$')ax.quiver(0, 0, *(-q1), angles='xy', scale_units='xy', scale=1, color='blue', label=r'$q_2$')ax.quiver(0, 0, *w1, angles='xy', scale_units='xy', scale=1, color='red', label=r'$w_1$')ax.quiver(0, 0, *w2, angles='xy', scale_units='xy', scale=1, color='red', label=r'$w_1$')# Plot the solution of the differential equationax.plot(sol.y[0], sol.y[1], color='purple', label='Trajectory of $x(t)$')# Add labels for the trajectory# ax.text(sol.y[0][-1], sol.y[1][-1], r'$x(t)$', fontsize=12, color='orange', verticalalignment='bottom', horizontalalignment='left')# ax.quiver(0, 0, *(-q2), angles='xy', scale_units='xy', scale=1, color='red', label=r'$-Im q = Im \bar{q}$')# Add axis labelsax.text(*q1, r'$q_1$', fontsize=12, color='blue', verticalalignment='bottom', horizontalalignment='left')ax.text(*q2, r'$q_2$', fontsize=12, color='green', verticalalignment='top', horizontalalignment='right')# Set labels for the axes (W)ax.text(2, 0, 'W', fontsize=12, verticalalignment='bottom', horizontalalignment='left')# Hide the grid and axesax.set_axis_off()# Show the plotplt.show()
Now we will build a one-to-one correspondence between this two dimensional subspace \(W\) and \(\mathbb{C}\). Of course there are multiple options how to choose a linear isomorphism between \(W\) and \(\mathbb{C}\), but we will see that each one corresponds to a choice of \(w_1\).
Lets consider the dual basis of \([q_1,q_2]\) in \(W^*\) (space of linear forms on \(W\)) and denote it \([p^1, p^2]\).34
We can compose them into complex dual vector \(p=\frac{1}{2}(p^1-ip^2)\) so \(p, \bar{p}\) form a basis of complexified vector space \((W^*)^{\mathbb{C}} = (W^{\mathbb{C}})^*\) dual to \(q,\bar{q}\).
For this example we are lucky, that \(W\) is just standard \(\mathbb{R}^2\) and we get the dual basis of \(W\) as dual basis of \[
q_1 = \begin{pmatrix}
2 \\
1
\end{pmatrix},
q_2 = \begin{pmatrix}
\frac{\sqrt{2}}{2} \\
-\frac{\sqrt{2}}{2}
\end{pmatrix}
\]\[
Q=\begin{pmatrix}
q_1 & q_2
\end{pmatrix} =
\begin{pmatrix}
2 & \frac{\sqrt{2}}{2} \\
1 & -\frac{\sqrt{2}}{2}
\end{pmatrix}
\]
Note: If the subspace \(W\) were different one can just add some other vectors to form a basis, compute an inverse and take the first two rows or come with some more clever ideas.
Lets now check if \(p\) has an inverse. By an inverse I mean inverse of the linear map5
\[
p:W \to \mathbb{C}
\]
Where we view \(\mathbb{C}\) as vector space over \(\mathbb{R}\) (with some additional action \(\cdot\) of \(\mathbb{C} \setminus \{0\}\) which will prove useful later).
One can see that \(pq_1=\frac{1}{2}\), \(pq_2=\frac{1}{2}i\), which are linearly independent and since \(W\) and \(\mathbb{C}\) have both dimension 2, so \(p\) must be invertable. Particularly for
Lets now build a geometric intuition on these linear forms. \(p\) is complex, so it is better to work with its components \(p_1\) and \(p_2\). I like to vizualize them like functions of two variables in \(\mathbb{R}^3\).
Code
import plotly.graph_objects as godef create_3d_plot(q1,q2,swap=False): q_ = q2 if swap else q1 q = q1 if swap else q2 p1 = np.array([q_[1], -q_[0]]) coef =1/ np.dot(q, p1) p1 = p1 * coef# Define hyperplane as a mesh grid x = np.array([q_ + p1, q_ - p1, -q_ + p1, -q_ - p1]) z = np.dot(x, p1) z = np.reshape(z, (2, 2)) x = np.reshape(x, (2, 2, 2))# Prepare 3D mesh coordinates for Plotly surface x_vals = x[:, :, 0] y_vals = x[:, :, 1] z_vals = z# Create a plotly figure fig = go.Figure()# Plot the hyperplane surface fig.add_trace(go.Surface( x=x_vals, y=y_vals, z=z_vals, colorscale=[[0, 'lightblue'], [1, 'lightblue']], opacity=0.5, showscale=False ))# Plot boundary edges of the surfacefor i inrange(x.shape[0]): fig.add_trace(go.Scatter3d( x=x[i, :, 0], y=x[i, :, 1], z=z[i, :], mode='lines', line=dict(color='lightblue', width=2) )) fig.add_trace(go.Scatter3d( x=x[:, i, 0], y=x[:, i, 1], z=z[:, i], mode='lines', line=dict(color='lightblue', width=2) ))def add_arrow(x_start, y_start, z_start, u, v, w, color, label=None): x_end = x_start + u y_end = y_start + v z_end = z_start + w# Draw the line (arrow shaft) fig.add_trace(go.Scatter3d( x=[x_start, x_end], y=[y_start, y_end], z=[z_start, z_end], mode='lines', line=dict(color=color, width=4) ))# Add an arrowhead (small point at the end)# fig.add_trace(go.Scatter3d(# x=[x_end], y=[y_end], z=[z_end],# mode='markers', marker=dict(symbol="arrow-bar-up", size=10, color=color)# ))# Add label if providedif label: fig.add_trace(go.Scatter3d( x=[x_end], y=[y_end], z=[z_end], mode='text', text=[label], textposition='top center', textfont=dict(color=color) ))# Add arrows to the plot add_arrow(0, 0, 0, q1[0], q1[1], 0, 'blue', label=r'q1') add_arrow(0, 0, 0, q2[0], q2[1], 0, 'green', label=r'q2') add_arrow(0, 0, 0, p1[0], p1[1], 0, 'red', label='grad p1'if swap else'grad p2') fig.add_trace(go.Scatter3d( x=[q[0], q[0]], y=[q[1], q[1]], z=[0, 1], mode='lines+markers', line=dict(color='black', width=4, dash='longdash'), marker=dict(size=5, color='black') ))# Add axis lines fig.add_trace(go.Scatter3d(x=[-1.5, 1.5], y=[0, 0], z=[0, 0], mode='lines', line=dict(color='black', width=2))) # X-axis fig.add_trace(go.Scatter3d(x=[0, 0], y=[-1.5, 1.5], z=[0, 0], mode='lines', line=dict(color='black', width=2))) # Y-axis fig.add_trace(go.Scatter3d(x=[0, 0], y=[0, 0], z=[0, 1], mode='lines', line=dict(color='blue', width=1))) # Z-axis fig.add_trace(go.Scatter3d( x=[0], y=[0], z=[1], mode='text', text=['1'if swap else'i'], textposition='top center', textfont=dict(color='blue') ))# Set axis propertiesreturn figq1_better = np.array([1, .8]) /1.2q2_better = np.array([-2, 1])/2.2fig1 = create_3d_plot(q1_better,q2_better, True)fig2 = create_3d_plot(q1_better,q2_better, False)# Create a subplot figurefrom plotly.subplots import make_subplotsfig = make_subplots(rows=1, cols=2, specs=[[{'type': 'surface'}, {'type': 'surface'}]])# Add the first plot to the first subplotfor trace in fig1.data: fig.add_trace(trace, row=1, col=1)# Add the second plot to the second subplotfor trace in fig2.data: fig.add_trace(trace, row=1, col=2)# Update layout for the subplot figurezoom =1/1.7scene =dict( xaxis=dict(range=[-2, 2], visible=False), yaxis=dict(range=[-2, 2], visible=False), zaxis=dict(range=[-1, 1.2], visible=False), camera=dict( eye=dict(x=.4*zoom, y=-2*zoom, z=1.2*zoom), # Camera positioned at (1.5, 1.5, 1.5) up=dict(x=0, y=0, z=1), # Camera's "up" vector is along the Z-axis center=dict(x=0, y=0, z=0), # Camera is looking at the origin# Zoom in by a factor of 2 ),)fig.update_layout( scene=scene, scene2=scene, showlegend=False)fig.show()
Or simply imagine mapping \(q_1\) to \(1\) and \(q_2\) to \(i\)
Code
# Create the figure and axisfig, ax = plt.subplots(figsize=(4, 4))# Set the axis limitsax.set_xlim(-1, 1)ax.set_ylim(-1, 1)# Add grid and axis labelsax.axhline(0, color='black',linewidth=0.5)ax.axvline(0, color='black',linewidth=0.5)ax.set_xticks(np.arange(-2, 3, 1))ax.set_yticks(np.arange(-2, 3, 1))ax.grid(False)# Define canonical vectorse1 = np.array([1, 0])e2 = np.array([0, 1])# Plot the vectorsax.quiver(0, 0, *e1, angles='xy', scale_units='xy', scale=1, color='blue', label=r'$pq_1$')ax.quiver(0, 0, *e2, angles='xy', scale_units='xy', scale=1, color='green', label=r'$pq_2$')# Add axis labelsax.text(*e1, r'$pq_1$', fontsize=12, color='blue', verticalalignment='bottom', horizontalalignment='left')ax.text(*e2, r'$pq_2$', fontsize=12, color='green', verticalalignment='top', horizontalalignment='right')# Hide the grid and axesax.set_axis_off()# Show the plotplt.show()
Lets see how p(.) comutes with the linear map A.
Let \(A^*\) be the dual operator to \(A\). By definition
# Create the figure and axisfig, ax = plt.subplots(figsize=(4, 4))# Set the axis limitsax.set_xlim(-1, 1)ax.set_ylim(-1, 1)# Add grid and axis labelsax.axhline(0, color='black',linewidth=0.5)ax.axvline(0, color='black',linewidth=0.5)ax.set_xticks(np.arange(-2, 3, 1))ax.set_yticks(np.arange(-2, 3, 1))ax.grid(False)# Define canonical vectorse1 = np.array([1, 0])e2 = np.array([0, 1])# Plot the vectorsax.quiver(0, 0, *e2, angles='xy', scale_units='xy', scale=1, color='blue', label=r'$pAq_1 = ipq_1$')ax.quiver(0, 0, *(-e1), angles='xy', scale_units='xy', scale=1, color='green', label=r'$pAq_2 = ipq_2$')# Add axis labelsax.text(*e2, r'$pAq_1 = ipq_1$', fontsize=12, color='blue', verticalalignment='bottom', horizontalalignment='left')ax.text(*(-e1), r'$pAq_2 = ipq_2$', fontsize=12, color='green', verticalalignment='top', horizontalalignment='right')# Hide the grid and axesax.set_axis_off()# Show the plotplt.show()
General case
First lets consider all \(\omega > 0\) (keep \(\sigma = 0\) ). In that case there is a simple connection with the previous discussion we can decompose the operator into two:
\[
A = \omega \frac{1}{\omega} A = \omega \bar{A}
\] Where the operator
\[
\bar{A} = \frac{1}{\omega} A
\]
satisfies the simpler (\(\omega=0\)) case and the multipliation by \(\omega\) is just a scaling operator. This trivially translates to scaling in the complex space. For \(\omega < 0\) this means scaling and flipping or slimply a rotation in the opposite direction ( \(Aq_1 = q_2,\; Aq_2=-q_1\) ).
In contrast permitting \(\sigma \neq 0\) adds hudge complexity - it can be viewed as rotation and then adding the original vector. The decomposition could look something like this:
\[
A = \omega \bar{A} + \sigma E
\]
\[
Aq=\lambda q, \quad \lambda = \sigma + i\omega
\]
\[
\bar{A}q=iq
\]
Footnotes
You can also imagine a general operator A on complex vector space \(V\) where \(AReV \subseteq ReV\)↩︎
If \(\omega\) <0 the conjugated vector \(\bar{q}\) will have eigenvalue \(\bar{\lambda} = \sigma - i\omega\)↩︎
The upper index is related to the Einstains notaion (contravariet components). I think it is good to use the notation from fields that provide important backgroud, so those who know it can figure out the meaning faster and those who don’t are motivated to learn it.↩︎
Throughout the following text you can just imagine a vector from \(\mathbb{R}^n\) as column, the dual vector as row and understand the expression \(p^1q_1\) as multiplication of \(1\times n\) and \(n \times 1\) matrices. If we denote the matrix formed from the base \(Q = (q_1,q_2,\dots)\). The i-th dual base vector is then obtained as a solution to the system \(p^iQ=e_i^T\). In other words the matrix of dual basis (composed of rows) is \(Q^{-1}\)↩︎
This is some kind of dimensional reduction where we extract the extraordinary behavior on specific subspace.↩︎