|
49 | 49 | ;; a circle at constant speed. |
50 | 50 |
|
51 | 51 | ;; Generate one complete rotation |
52 | | -(def n-points 100) |
53 | | -(def angles (dfn/* (dfn// (range n-points) n-points) (* 2 Math/PI))) |
| 52 | +(def n-points 400) |
| 53 | +(def angles (dfn/* (dfn// (range n-points) (double n-points)) (* 2 Math/PI))) |
54 | 54 |
|
55 | 55 | ;; Position on the circle: (cos θ, sin θ) |
56 | 56 | (def x-positions (dfn/cos angles)) |
|
63 | 63 | :=x-title "Horizontal Position" |
64 | 64 | :=y-title "Vertical Position" |
65 | 65 | :=title "A Point Rotating on a Circle" |
66 | | - :=width 400 |
| 66 | + :=width 450 |
67 | 67 | :=height 400}) |
68 | | - (plotly/layer-line) |
69 | | - (plotly/layer-point {:=mark-size 8})) |
| 68 | + plotly/layer-line |
| 69 | + (plotly/layer-point {:=mark-size 8 |
| 70 | + :=mark-opacity 0.5}) |
| 71 | + plotly/plot |
| 72 | + (assoc-in [:layout :showlegend] false)) |
70 | 73 |
|
71 | 74 | ;; This circle is our **fundamental object**. Everything else—sine waves, cosine waves, |
72 | 75 | ;; complex exponentials—derives from this simple rotation. |
|
80 | 83 | ;; As the point rotates, each shadow moves back and forth along its axis. Let's trace these |
81 | 84 | ;; shadows over time. |
82 | 85 |
|
83 | | -(def time-points (dfn// (range n-points) 10.0)) ; Time in arbitrary units |
| 86 | +(def time-points |
| 87 | + ;; Normalized time 0 to 1 |
| 88 | + (dfn// (range n-points) (double n-points))) |
84 | 89 |
|
85 | 90 | ;; Create dataset with both projections |
86 | 91 | (def projections-data |
87 | 92 | (tc/dataset {:time time-points |
88 | | - :horizontal x-positions ; Cosine - the horizontal shadow |
89 | | - :vertical y-positions})) ; Sine - the vertical shadow |
| 93 | + ;; Cosine - the horizontal shadow |
| 94 | + :horizontal x-positions |
| 95 | + ;; Sine - the vertical shadow |
| 96 | + :vertical y-positions})) |
90 | 97 |
|
91 | 98 | ;; Visualize both shadows |
92 | 99 | (-> projections-data |
|
97 | 104 | :=title "Two Shadows of One Rotation" |
98 | 105 | :=width 700 |
99 | 106 | :=height 300}) |
100 | | - (plotly/layer-line {:=color "steelblue" |
| 107 | + (plotly/layer-line {:=mark-color "steelblue" |
101 | 108 | :=name "Horizontal shadow (cosine)"}) |
102 | 109 | (plotly/layer-line {:=y :vertical |
103 | | - :=color "orange" |
| 110 | + :=mark-color "orange" |
104 | 111 | :=name "Vertical shadow (sine)"})) |
105 | 112 |
|
106 | 113 | ;; **Key insight**: The horizontal shadow traces **cosine**, the vertical shadow traces **sine**. |
107 | 114 | ;; But they're not two separate things—they're two views of the **same circular motion**. |
108 | 115 | ;; |
109 | 116 | ;; Cosine and sine are **projections**, not primitives. The circle is more fundamental. |
110 | 117 |
|
111 | | -;; ## The Problem: Shadows Are Incomplete |
| 118 | +;; ## The Problem: One Shadow Loses Direction |
112 | 119 |
|
113 | | -;; Here's where things get interesting. Suppose I tell you: "I see a horizontal shadow moving |
114 | | -;; back and forth, reaching ±1." Can you tell me what the point on the circle is doing? |
| 120 | +;; Here's the fundamental issue. Imagine two points rotating at the same speed but in |
| 121 | +;; **opposite directions**: |
| 122 | +;; - Point A: rotating **counterclockwise** (upward when moving right) |
| 123 | +;; - Point B: rotating **clockwise** (downward when moving right) |
115 | 124 | ;; |
116 | | -;; **No!** The horizontal shadow alone is ambiguous. When the shadow is at +1, the point could be: |
117 | | -;; - At the rightmost point of the circle (3 o'clock position) |
118 | | -;; - Or it could have vertical position +0.5 (northeast somewhere) |
119 | | -;; - Or vertical position -0.5 (southeast somewhere) |
| 125 | +;; Watch their horizontal shadows. At any moment when both shadows are at the same position, |
| 126 | +;; **you cannot tell which direction each point is rotating** by looking at horizontal position |
| 127 | +;; alone. |
120 | 128 | ;; |
121 | | -;; The shadow gives you **one coordinate**. To know where the point is on the circle, you need |
122 | | -;; **both** horizontal and vertical positions. |
| 129 | +;; The horizontal shadow (cosine) captures **frequency** (speed of rotation) but loses |
| 130 | +;; **direction** (sign of the vertical component). You need **both shadows** to distinguish |
| 131 | +;; clockwise from counterclockwise. |
123 | 132 |
|
124 | | -;; Let's visualize this ambiguity: |
| 133 | +;; Let's see this with a concrete example that matters for Fourier decomposition. |
125 | 134 |
|
126 | | -;; Three different rotations, same horizontal shadow |
127 | | -(def rotation-1 {:name "Starting at 3 o'clock" :phase 0.0}) |
128 | | -(def rotation-2 {:name "Starting at 2 o'clock" :phase (/ Math/PI 6)}) |
129 | | -(def rotation-3 {:name "Starting at 4 o'clock" :phase (- (/ Math/PI 6))}) |
| 135 | +;; Two different signals, same frequency, different "direction" |
| 136 | +(def demo-time (dfn// (range 400) 100.0)) |
| 137 | +(def freq 2.0) ; 2 Hz |
130 | 138 |
|
131 | | -(defn rotation-with-phase [phase] |
132 | | - (let [t (dfn// (range 50) 5.0) |
133 | | - theta (dfn/+ (dfn/* 2.0 Math/PI t) phase)] |
134 | | - {:time t |
135 | | - :horizontal (dfn/cos theta) |
136 | | - :vertical (dfn/sin theta)})) |
137 | | - |
138 | | -;; All three have the same frequency, different starting angles (phases) |
139 | | -(def phase-comparison-data |
140 | | - (concat |
141 | | - (map (fn [i] (assoc (rotation-with-phase 0.0) |
142 | | - :index i |
143 | | - :rotation (:name rotation-1))) |
144 | | - (range 50)) |
145 | | - (map (fn [i] (assoc (rotation-with-phase (/ Math/PI 6)) |
146 | | - :index i |
147 | | - :rotation (:name rotation-2))) |
148 | | - (range 50)) |
149 | | - (map (fn [i] (assoc (rotation-with-phase (- (/ Math/PI 6))) |
150 | | - :index i |
151 | | - :rotation (:name rotation-3))) |
152 | | - (range 50)))) |
153 | | - |
154 | | -;; Show horizontal projections - they look different! |
155 | | -(-> (tc/dataset (for [rot [rotation-1 rotation-2 rotation-3] |
156 | | - :let [{:keys [time horizontal]} (rotation-with-phase (:phase rot))] |
157 | | - i (range (count time))] |
158 | | - {:time (nth time i) |
159 | | - :horizontal (nth horizontal i) |
160 | | - :rotation (:name rot)})) |
| 139 | +;; Signal A: cos(2πft) - rotation starting at 3 o'clock, going counterclockwise |
| 140 | +(def signal-a (dfn/cos (dfn/* 2.0 Math/PI freq demo-time))) |
| 141 | + |
| 142 | +;; Signal B: cos(2πft + π) = -cos(2πft) - rotation starting at 9 o'clock, going counterclockwise |
| 143 | +;; This is like rotating in the opposite direction |
| 144 | +(def signal-b (dfn/* -1.0 (dfn/cos (dfn/* 2.0 Math/PI freq demo-time)))) |
| 145 | + |
| 146 | +;; Visualize both signals |
| 147 | +(-> (tc/concat |
| 148 | + (tc/dataset {:time demo-time :amplitude signal-a :signal "Signal A: cos(2πft)"}) |
| 149 | + (tc/dataset {:time demo-time :amplitude signal-b :signal "Signal B: -cos(2πft)"})) |
161 | 150 | (plotly/base {:=x :time |
162 | | - :=y :horizontal |
163 | | - :=color :rotation |
| 151 | + :=y :amplitude |
| 152 | + :=color :signal |
| 153 | + :=x-title "Time (seconds)" |
| 154 | + :=y-title "Amplitude" |
| 155 | + :=title "Two Different Signals at Same Frequency" |
| 156 | + :=width 700 |
| 157 | + :=height 300}) |
| 158 | + (plotly/layer-line)) |
| 159 | + |
| 160 | +;; **Key observation**: These are **different signals** - one is the negative of the other. |
| 161 | +;; But if we only look at their **magnitude spectrum** (which ignores phase), they appear |
| 162 | +;; identical. |
| 163 | +;; |
| 164 | +;; Let's prove this with a more dramatic example using actual Fourier decomposition. |
| 165 | + |
| 166 | +;; ## Demonstrating Information Loss: Why Phase Matters |
| 167 | + |
| 168 | +;; Here are two completely different signals at the same frequency: |
| 169 | + |
| 170 | +;; Signal 1: cos(2πft) - starts at maximum, phase = 0 |
| 171 | +(def t-demo (dfn// (range 400) 100.0)) |
| 172 | +(def signal-cos (dfn/cos (dfn/* 2.0 Math/PI 5.0 t-demo))) |
| 173 | + |
| 174 | +;; Signal 2: sin(2πft) = cos(2πft - π/2) - starts at zero rising, phase = -π/2 |
| 175 | +(def signal-sin (dfn/sin (dfn/* 2.0 Math/PI 5.0 t-demo))) |
| 176 | + |
| 177 | +;; Visualize both |
| 178 | +(-> (tc/concat |
| 179 | + (tc/dataset {:time t-demo :amplitude signal-cos :signal "cos(2π·5t) - starts at max"}) |
| 180 | + (tc/dataset {:time t-demo :amplitude signal-sin :signal "sin(2π·5t) - starts at zero"})) |
| 181 | + (plotly/base {:=x :time |
| 182 | + :=y :amplitude |
| 183 | + :=color :signal |
164 | 184 | :=x-title "Time" |
165 | | - :=y-title "Horizontal Shadow" |
166 | | - :=title "Same Speed, Different Starting Angles → Different Shadows" |
| 185 | + :=y-title "Amplitude" |
| 186 | + :=title "Different Signals, Same Frequency and Magnitude" |
167 | 187 | :=width 700 |
168 | 188 | :=height 300}) |
169 | 189 | (plotly/layer-line)) |
170 | 190 |
|
171 | | -;; **Observation**: Even though all three rotations have the **same speed** (same frequency), |
172 | | -;; their horizontal shadows look different because they started at different angles. |
| 191 | +;; **Critical fact**: These signals have: |
| 192 | +;; ✓ Same frequency (5 Hz) |
| 193 | +;; ✓ Same magnitude spectrum (both will show amplitude 1.0 at 5 Hz) |
| 194 | +;; ✗ Different phase spectrum (0° vs -90°) |
| 195 | +;; ✗ **Completely different values at every time point!** |
| 196 | + |
| 197 | +;; Check the difference: |
| 198 | +(def difference-at-start (dfn/- (first signal-cos) (first signal-sin))) |
| 199 | +;; cos(0) - sin(0) = 1.0 - 0.0 = 1.0 |
| 200 | + |
| 201 | +(kind/hiccup |
| 202 | + [:div |
| 203 | + [:p [:strong "At t=0:"]] |
| 204 | + [:ul |
| 205 | + [:li (str "cos signal: " (format "%.3f" (first signal-cos)))] |
| 206 | + [:li (str "sin signal: " (format "%.3f" (first signal-sin)))] |
| 207 | + [:li (str "difference: " (format "%.3f" difference-at-start))]]]) |
| 208 | + |
| 209 | +;; **The fatal problem**: If you run a Fourier transform and **throw away the phase** (keeping |
| 210 | +;; only magnitudes), you cannot tell these signals apart. You've lost the information about |
| 211 | +;; whether the rotation started at 3 o'clock (cosine) or 12 o'clock going right (sine). |
173 | 212 | ;; |
174 | | -;; If we only track the horizontal shadow (cosine), we lose information about the **phase** |
175 | | -;; (starting angle). To fully describe the rotation, we need both shadows. |
| 213 | +;; Without phase, the Fourier transform becomes **non-invertible** - you cannot reconstruct |
| 214 | +;; the original signal. This isn't a mathematical quirk - it's fundamental information loss. |
| 215 | +;; |
| 216 | +;; Complex numbers solve this by preserving **both** the horizontal shadow (real part) and |
| 217 | +;; vertical shadow (imaginary part), which together uniquely specify the rotation and its |
| 218 | +;; starting angle. |
176 | 219 |
|
177 | 220 | ;; ## The Speed of Rotation: Frequency |
178 | 221 |
|
|
194 | 237 | :frequency freq})) |
195 | 238 |
|
196 | 239 | (def freq-comparison |
197 | | - (concat |
198 | | - (let [{:keys [time position]} (rotation-at-frequency 1.0 2.0 50.0)] |
199 | | - (map-indexed (fn [i _] {:time (nth time i) |
200 | | - :position (nth position i) |
201 | | - :frequency "1 Hz (slow)"}) |
202 | | - time)) |
203 | | - (let [{:keys [time position]} (rotation-at-frequency 3.0 2.0 50.0)] |
204 | | - (map-indexed (fn [i _] {:time (nth time i) |
205 | | - :position (nth position i) |
206 | | - :frequency "3 Hz (medium)"}) |
207 | | - time)) |
208 | | - (let [{:keys [time position]} (rotation-at-frequency 5.0 2.0 50.0)] |
209 | | - (map-indexed (fn [i _] {:time (nth time i) |
210 | | - :position (nth position i) |
211 | | - :frequency "5 Hz (fast)"}) |
212 | | - time)))) |
213 | | - |
214 | | -(-> (tc/dataset freq-comparison) |
| 240 | + (let [{:keys [time position]} (rotation-at-frequency 1.0 2.0 50.0) |
| 241 | + {time-3 :time position-3 :position} (rotation-at-frequency 3.0 2.0 50.0) |
| 242 | + {time-5 :time position-5 :position} (rotation-at-frequency 5.0 2.0 50.0)] |
| 243 | + (tc/concat |
| 244 | + (tc/dataset {:time time :position position :frequency "1 Hz (slow)"}) |
| 245 | + (tc/dataset {:time time-3 :position position-3 :frequency "3 Hz (medium)"}) |
| 246 | + (tc/dataset {:time time-5 :position position-5 :frequency "5 Hz (fast)"})))) |
| 247 | + |
| 248 | +(-> freq-comparison |
215 | 249 | (plotly/base {:=x :time |
216 | 250 | :=y :position |
217 | 251 | :=color :frequency |
|
235 | 269 | ;; shadow is just the sum of individual shadows. |
236 | 270 |
|
237 | 271 | ;; Create a composite signal: 1 Hz + 3 Hz |
238 | | -(def composite-time (dfn// (range 200) 50.0)) |
| 272 | +(def composite-time (dfn// (range 400) 100.0)) |
239 | 273 | (def component-1hz (dfn/cos (dfn/* 2.0 Math/PI 1.0 composite-time))) |
240 | 274 | (def component-3hz (dfn/* 0.6 (dfn/cos (dfn/* 2.0 Math/PI 3.0 composite-time)))) |
241 | 275 | (def composite-signal (dfn/+ component-1hz component-3hz)) |
242 | 276 |
|
243 | 277 | (def superposition-data |
244 | | - (concat |
245 | | - (map-indexed (fn [i _] {:time (nth composite-time i) |
246 | | - :amplitude (nth component-1hz i) |
247 | | - :component "1 Hz component"}) |
248 | | - composite-time) |
249 | | - (map-indexed (fn [i _] {:time (nth composite-time i) |
250 | | - :amplitude (nth component-3hz i) |
251 | | - :component "3 Hz component"}) |
252 | | - composite-time) |
253 | | - (map-indexed (fn [i _] {:time (nth composite-time i) |
254 | | - :amplitude (nth composite-signal i) |
255 | | - :component "Combined signal"}) |
256 | | - composite-time))) |
257 | | - |
258 | | -(-> (tc/dataset superposition-data) |
| 278 | + (tc/concat |
| 279 | + (tc/dataset {:time composite-time :amplitude component-1hz :component "1 Hz component"}) |
| 280 | + (tc/dataset {:time composite-time :amplitude component-3hz :component "3 Hz component"}) |
| 281 | + (tc/dataset {:time composite-time :amplitude composite-signal :component "Combined signal"}))) |
| 282 | + |
| 283 | +(-> superposition-data |
259 | 284 | (plotly/base {:=x :time |
260 | 285 | :=y :amplitude |
261 | 286 | :=color :component |
|
311 | 336 |
|
312 | 337 | ;; Let's visualize what this means in the complex plane: |
313 | 338 |
|
314 | | -(def omega 2.0) ; Rotation speed (frequency) |
315 | | -(def demo-time (dfn// (range 100) 20.0)) |
| 339 | + |
| 340 | +(def omega |
| 341 | + ;; Rotation speed (frequency) |
| 342 | + 2.0) |
| 343 | +(def demo-time (dfn// (range 400) 80.0)) |
316 | 344 | (def demo-theta (dfn/* omega demo-time)) |
317 | 345 |
|
318 | 346 | (def complex-plane-data |
319 | | - (map-indexed (fn [i _] |
320 | | - {:real (Math/cos (nth demo-theta i)) |
321 | | - :imag (Math/sin (nth demo-theta i)) |
322 | | - :time (nth demo-time i)}) |
323 | | - demo-time)) |
| 347 | + (tc/dataset {:real (dfn/cos demo-theta) |
| 348 | + :imag (dfn/sin demo-theta) |
| 349 | + :time demo-time})) |
324 | 350 |
|
325 | | -(-> (tc/dataset complex-plane-data) |
| 351 | +(-> complex-plane-data |
326 | 352 | (plotly/base {:=x :real |
327 | 353 | :=y :imag |
328 | 354 | :=color :time |
|
331 | 357 | :=title "Complex Plane: e^(iωt) Traces a Circle" |
332 | 358 | :=width 450 |
333 | 359 | :=height 450}) |
334 | | - (plotly/layer-line) |
335 | 360 | (plotly/layer-point {:=mark-size 6})) |
336 | 361 |
|
337 | 362 | ;; **What you're seeing**: Each point represents a moment in time. The real coordinate is the |
|
392 | 417 | ;; and at what angle they started. |
393 | 418 | ;; |
394 | 419 | ;; Real numbers give you shadows. Complex numbers give you the circle itself. |
395 | | - |
396 | | -;; --- |
397 | | -;; |
398 | | -;; **Next in the series**: |
399 | | -;; - [Signal Transforms: A Comprehensive Guide](#) - Practical tools for FFT, DCT, wavelets in Clojure |
400 | | -;; - Fourier Theory Across Domains (upcoming) - Continuous, discrete, periodic, and finite cases unified |
|
0 commit comments