@@ -17,21 +17,23 @@ kernelspec:
17171818This lecture describes circulant matrices and some of their properties.
191920-Circulant matrices have a special structure that connects them to useful concepts
21-including
20+Circulant matrices are useful because multiplying by them is closely connected to convolution, and their eigenvectors can be constructed using the Discrete Fourier Transform.
21+22+We use circulant matrices to connect several useful concepts, including
22232324* convolution
2425* Fourier transforms
2526* permutation matrices
262727-Because of these connections, circulant matrices are widely used in machine learning, for example, in image processing.
28+For background on eigenvalues and eigenvectors, see {doc}`linear_algebra`; for another use of Fourier transforms and convolution, see {doc}`hoist_failure`.
29+30+Circulant matrices are also widely used in machine learning, for example, in image processing.
283129323033We begin by importing some Python packages
31343235```{code-cell} ipython3
3336import numpy as np
34-from numba import jit
3537import matplotlib.pyplot as plt
3638```
3739@@ -60,14 +62,28 @@ c_{1} & c_{2} & c_{3} & c_{4} & c_{5} & \cdots & c_{0}
6062\end{array}\right]
6163$$ (eqn:circulant)
626465+This pattern can be formalized as follows.
66+67+```{prf:definition} Circulant matrix
68+:label: def-circulant-matrix
69+70+An $N \times N$ matrix $C$ is **circulant** if there are numbers $c_0, \ldots, c_{N-1}$ such that
71+72+$$
73+C_{ij} = c_{(j-i) \bmod N},
74+\qquad 0 \leq i,j \leq N-1.
75+$$
76+77+Equivalently, each row is obtained from the previous row by shifting entries one step to the right.
78+```
79+6380It is also possible to construct a circulant matrix by creating the transpose of the above matrix, in which case only the
6481first column needs to be specified.
65826683Let's write some Python code to generate a circulant matrix.
67846885```{code-cell} ipython3
69-@jit
70-def construct_cirlulant(row):
86+def construct_circulant(row):
71877288 N = row.size
7389@@ -83,14 +99,16 @@ def construct_cirlulant(row):
839984100```{code-cell} ipython3
85101# a simple case when N = 3
86-construct_cirlulant(np.array([1., 2., 3.]))
102+construct_circulant(np.array([1., 2., 3.]))
87103```
8810489105### Some Properties of Circulant Matrices
9010691107Here are some useful properties:
9210893-Suppose that $A$ and $B$ are both circulant matrices. Then it can be verified that
109+Suppose that $A$ and $B$ are both circulant matrices of the same order and constructed using the same cyclic shift convention.
110+111+Then it can be verified that
9411295113 * The transpose of a circulant matrix is a circulant matrix.
96114@@ -111,16 +129,18 @@ Now consider a circulant matrix with first row
111129 The **convolution** of vectors $c$ and $a$ is defined as the vector $b = c * a $ with components
112130113131$$
114- b_k = \sum_{i=0}^{n-1} c_{k-i} a_i
132+ b_k = \sum_{i=0}^{N-1} c_{k-i} a_i
115133$$ (eqn:conv)
116134135+Here and below, indices such as $k-i$ are interpreted modulo $N$.
136+117137We use $*$ to denote **convolution** via the calculation described in equation {eq}`eqn:conv`.
118138119139It can be verified that the vector $b$ satisfies
120140121-$$ b = C^T a $$
141+$$ b = C^\top a $$
122142123-where $C^T$ is the transpose of the circulant matrix defined in equation {eq}`eqn:circulant`.
143+where $C^\top$ is the transpose of the circulant matrix defined in equation {eq}`eqn:circulant`.
124144125145126146@@ -176,7 +196,7 @@ $$
176196and solving
177197178198$$
179-\textrm{det}(P - \lambda I) = (-1)^N \lambda^{N}-1=0
199+\textrm{det}(P - \lambda I) = (-1)^N(\lambda^N - 1)=0
180200$$
181201182202@@ -189,7 +209,7 @@ Thus, **singular values** of the permutation matrix $P$ defined in equation {eq
189209It can be verified that permutation matrices are orthogonal matrices:
190210191211$$
192-P P' = I
212+P P^\top = I
193213$$
194214195215@@ -200,8 +220,7 @@ $$
200220Let's write some Python code to illustrate these ideas.
201221202222```{code-cell} ipython3
203-@jit
204-def construct_P(N):
223+def construct_cyclic_shift_matrix(N):
205224206225 P = np.zeros((N, N))
207226@@ -213,7 +232,7 @@ def construct_P(N):
213232```
214233215234```{code-cell} ipython3
216-P4 = construct_P(4)
235+P4 = construct_cyclic_shift_matrix(4)
217236P4
218237```
219238@@ -224,7 +243,7 @@ P4
224243225244```{code-cell} ipython3
226245for i in range(4):
227- print(f'𝜆{i} = {𝜆[i]:.1f} \nvec{i} = {Q[i, :]}\n')
246+ print(f'𝜆{i} = {𝜆[i]:.1f} \nvec{i} = {Q[:, i]}\n')
228247```
229248230249In graphs below, we shall portray eigenvalues of a shift permutation matrix in the complex plane.
@@ -249,7 +268,7 @@ for i, N in enumerate([3, 4, 6, 8]):
249268 row_i = i // 2
250269 col_i = i % 2
251270252- P = construct_P(N)
271+ P = construct_cyclic_shift_matrix(N)
253272 𝜆, Q = np.linalg.eig(P)
254273255274 circ = plt.Circle((0, 0), radius=1, edgecolor='b', facecolor='None')
@@ -287,7 +306,7 @@ F_{8}=\left[\begin{array}{ccccc}
287306\end{array}\right]
288307$$
289308290-The matrix $F_8$ defines a [Discete Fourier Transform](https://en.wikipedia.org/wiki/Discrete_Fourier_transform).
309+The matrix $F_8$ defines a [Discrete Fourier Transform](https://en.wikipedia.org/wiki/Discrete_Fourier_transform).
291310292311To convert it into an orthogonal eigenvector matrix, we can simply normalize it by dividing every entry by $\sqrt{8}$.
293312@@ -332,7 +351,7 @@ Q8 @ np.conjugate(Q8)
332351Let's verify that $k$th column of $Q_{8}$ is an eigenvector of $P_{8}$ with an eigenvalue $w^{k}$.
333352334353```{code-cell} ipython3
335-P8 = construct_P(8)
354+P8 = construct_cyclic_shift_matrix(8)
336355```
337356338357```{code-cell} ipython3
@@ -353,15 +372,11 @@ Next, we execute calculations to verify that the circulant matrix $C$ defined i
353372354373355374$$
356-C = c_{0} I + c_{1} P + \cdots + c_{n-1} P^{n-1}
375+C = c_{0} I + c_{1} P + \cdots + c_{N-1} P^{N-1}
357376$$
358377359378and that every eigenvector of $P$ is also an eigenvector of $C$.
360379361-```{code-cell} ipython3
362-363-```
364-365380We illustrate this for $N=8$ case.
366381367382```{code-cell} ipython3
@@ -373,10 +388,10 @@ c
373388```
374389375390```{code-cell} ipython3
376-C8 = construct_cirlulant(c)
391+C8 = construct_circulant(c)
377392```
378393379-Compute $c_{0} I + c_{1} P + \cdots + c_{n-1} P^{n-1}$.
394+Compute $c_{0} I + c_{1} P + \cdots + c_{N-1} P^{N-1}$.
380395381396```{code-cell} ipython3
382397N = 8
@@ -403,7 +418,7 @@ Now let's compute the difference between two circulant matrices that we have co
403418np.abs(C - C8).max()
404419```
405420406-The $k$th column of $P_{8}$ associated with eigenvalue $w^{k-1}$ is an eigenvector of $C_{8}$ associated with an eigenvalue $\sum_{h=0}^{7} c_{j} w^{h k}$.
421+The $j$th column of $Q_{8}$ is an eigenvector of $C_{8}$ associated with eigenvalue $\sum_{k=0}^{7} c_k w^{j k}$.
407422408423```{code-cell} ipython3
409424𝜆_C8 = np.zeros(8, dtype=complex)
@@ -430,7 +445,7 @@ for j in range(8):
430445431446The **Discrete Fourier Transform** (DFT) allows us to represent a discrete time sequence as a weighted sum of complex sinusoids.
432447433-Consider a sequence of $N$ real number $\{x_j\}_{j=0}^{N-1}$.
448+Consider a sequence of $N$ real numbers $\{x_j\}_{j=0}^{N-1}$.
434449435450The **Discrete Fourier Transform** maps $\{x_j\}_{j=0}^{N-1}$ into a sequence of complex numbers $\{X_k\}_{k=0}^{N-1}$
436451@@ -498,7 +513,7 @@ def plot_magnitude(x=None, X=None):
498513 if (X is not None):
499514 data.append(X)
500515 names.append('X')
501- xs.append('j')
516+ xs.append('k')
502517503518 num = len(data)
504519 for i in range(num):
@@ -526,7 +541,7 @@ x_{n} = \sum_{k=0}^{N-1} \frac{1}{N} X_{k} e^{2\pi\left(\frac{kn}{N}\right)i}, \
526541$$
527542528543```{code-cell} ipython3
529-def inverse_transform(X):
544+def inverse_DFT(X):
530545531546 N = len(X)
532547 w = np.e ** (complex(0, 2*np.pi/N))
@@ -540,7 +555,7 @@ def inverse_transform(X):
540555```
541556542557```{code-cell} ipython3
543-inverse_transform(X)
558+inverse_DFT(X)
544559```
545560546561Another example is
@@ -551,7 +566,7 @@ $$
551566552567Since $N=20$, we cannot use an integer multiple of $\frac{1}{20}$ to represent a frequency $\frac{11}{40}$.
553568554-To handle this, we shall end up using all $N$ of the availble frequencies in the DFT.
569+To handle this, we shall end up using all $N$ of the available frequencies in the DFT.
555570556571Since $\frac{11}{40}$ is in between $\frac{10}{40}$ and $\frac{12}{40}$ (each of which is an integer multiple of $\frac{1}{20}$), the complex coefficients in the DFT have their largest magnitudes at $k=5,6,15,16$, not just at a single frequency.
557572@@ -614,7 +629,7 @@ X = DFT(x)
614629X
615630```
616631617-Now let's evaluate the outcome of postmultiplying the eigenvector matrix $F_{20}$ by the vector $x$, a product that we claim should equal the Fourier tranform of the sequence $\{x_n\}_{n=0}^{N-1}$.
632+Now let's evaluate the outcome of postmultiplying the eigenvector matrix $F_{20}$ by the vector $x$, a product that we claim should equal the Fourier transform of the sequence $\{x_n\}_{n=0}^{N-1}$.
618633619634```{code-cell} ipython3
620635F20, _ = construct_F(20)
@@ -624,13 +639,9 @@ F20, _ = construct_F(20)
624639F20 @ x
625640```
626641627-Similarly, the inverse DFT can be expressed as a inverse DFT matrix $F^{-1}_{20}$.
642+Similarly, the inverse DFT can be expressed as an inverse DFT matrix $F^{-1}_{20}$.
628643629644```{code-cell} ipython3
630645F20_inv = np.linalg.inv(F20)
631646F20_inv @ X
632647```
633-634-```{code-cell} ipython3
635-636-```