Skip to content

Commit 64e91ff

Browse files
committed
transforms wip
1 parent a0518ce commit 64e91ff

File tree

1 file changed

+130
-111
lines changed

1 file changed

+130
-111
lines changed

src/dsp/fourier_circular_intuition.clj

Lines changed: 130 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
;; a circle at constant speed.
5050

5151
;; 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)))
5454

5555
;; Position on the circle: (cos θ, sin θ)
5656
(def x-positions (dfn/cos angles))
@@ -63,10 +63,13 @@
6363
:=x-title "Horizontal Position"
6464
:=y-title "Vertical Position"
6565
:=title "A Point Rotating on a Circle"
66-
:=width 400
66+
:=width 450
6767
:=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))
7073

7174
;; This circle is our **fundamental object**. Everything else—sine waves, cosine waves,
7275
;; complex exponentials—derives from this simple rotation.
@@ -80,13 +83,17 @@
8083
;; As the point rotates, each shadow moves back and forth along its axis. Let's trace these
8184
;; shadows over time.
8285

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)))
8489

8590
;; Create dataset with both projections
8691
(def projections-data
8792
(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}))
9097

9198
;; Visualize both shadows
9299
(-> projections-data
@@ -97,82 +104,118 @@
97104
:=title "Two Shadows of One Rotation"
98105
:=width 700
99106
:=height 300})
100-
(plotly/layer-line {:=color "steelblue"
107+
(plotly/layer-line {:=mark-color "steelblue"
101108
:=name "Horizontal shadow (cosine)"})
102109
(plotly/layer-line {:=y :vertical
103-
:=color "orange"
110+
:=mark-color "orange"
104111
:=name "Vertical shadow (sine)"}))
105112

106113
;; **Key insight**: The horizontal shadow traces **cosine**, the vertical shadow traces **sine**.
107114
;; But they're not two separate things—they're two views of the **same circular motion**.
108115
;;
109116
;; Cosine and sine are **projections**, not primitives. The circle is more fundamental.
110117

111-
;; ## The Problem: Shadows Are Incomplete
118+
;; ## The Problem: One Shadow Loses Direction
112119

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)
115124
;;
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.
120128
;;
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.
123132

124-
;; Let's visualize this ambiguity:
133+
;; Let's see this with a concrete example that matters for Fourier decomposition.
125134

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
130138

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)"}))
161150
(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
164184
:=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"
167187
:=width 700
168188
:=height 300})
169189
(plotly/layer-line))
170190

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).
173212
;;
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.
176219

177220
;; ## The Speed of Rotation: Frequency
178221

@@ -194,24 +237,15 @@
194237
:frequency freq}))
195238

196239
(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
215249
(plotly/base {:=x :time
216250
:=y :position
217251
:=color :frequency
@@ -235,27 +269,18 @@
235269
;; shadow is just the sum of individual shadows.
236270

237271
;; 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))
239273
(def component-1hz (dfn/cos (dfn/* 2.0 Math/PI 1.0 composite-time)))
240274
(def component-3hz (dfn/* 0.6 (dfn/cos (dfn/* 2.0 Math/PI 3.0 composite-time))))
241275
(def composite-signal (dfn/+ component-1hz component-3hz))
242276

243277
(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
259284
(plotly/base {:=x :time
260285
:=y :amplitude
261286
:=color :component
@@ -311,18 +336,19 @@
311336

312337
;; Let's visualize what this means in the complex plane:
313338

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))
316344
(def demo-theta (dfn/* omega demo-time))
317345

318346
(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}))
324350

325-
(-> (tc/dataset complex-plane-data)
351+
(-> complex-plane-data
326352
(plotly/base {:=x :real
327353
:=y :imag
328354
:=color :time
@@ -331,7 +357,6 @@
331357
:=title "Complex Plane: e^(iωt) Traces a Circle"
332358
:=width 450
333359
:=height 450})
334-
(plotly/layer-line)
335360
(plotly/layer-point {:=mark-size 6}))
336361

337362
;; **What you're seeing**: Each point represents a moment in time. The real coordinate is the
@@ -392,9 +417,3 @@
392417
;; and at what angle they started.
393418
;;
394419
;; 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

Comments
 (0)