Every thing is a sin()
February 11, 2023 - Draft
What do heat transfer, music, JPEG, and Wi-Fi have in common? In 1807 Jean-Baptiste Fourier presented his paper "On the propagation of heat in solid bodies" to the Paris Institute and it made a lot nerds happy.
Inanimate carbon rod
We'll start by looking at this rod with an infrared camera:
If we graph the temperature we get a beautifiul sine wave:
Let's measure the rod temperature while it cools off:
Fourier found that for a perfect sine wave like ours, we can get the temperature over time using this formula:
In the programming world we can translate this to the following code currently running on your graphic card:
float curve(float x, float t) {
float o = 2.0 * pi; // omega: adjust frequency
float k = 1.0; // conductivity: adjust how fast heat propagates
return cos(o * x) * exp(-k * o * o * t);
}
ω
: angle in radians * π
k
: how fast heat propagatesNow here is what happens when we make contact between a hot rod and a cold rod:
How can we compute the heat flow in this case? We have a formula for sine waves, but what we have here is a square wave.
Hip to be square
Let's pull some fairly standard electronic music production gear: an oscillator, an oscilloscope, and a spectrum analyzer with an harmonics indicator.
The numbers on the spectrum analyzer are called harmonics:
- the first harmonic, also called the fundamental, equals
frequency * 1
- the second, also called the octave, equals
frequency * 2
- the third equals
frequency * 3
- and so on..
If you play with the oscillator a bit you will notice two important things:
- a sine wave only has one harmonic: itself
- a square wave only has odd harmonics decreasing exponentially
And that's all a square wave is, a sine wave with odd harmonics decreasing exponentially. So we have everything needed to decompose a square wave into sine waves and compute heat diffusion using them. Let's update our code:
float temperature(float x, float t) {
float o = 2 * pi; // omega: adjust frequency
float k = 1; // conductivity: adjust how fast heat propagates
return cos(o * x) * exp(-k * o * o * t);
}
float curve(float x, float t) {
float h = 3.0; // harmonics
float c = 0.0; // sum of curves
// Loop over odd harmonics: 1, 3, 5, 7, ...
for (float i = 1.0; i <= h; i += 2.0) {
// add the temperature for frequency * i and amplitude / i
c += temperature(x * i, t) / i;
}
return c;
}
ω
: angle in radians * π
k
: how fast heat propagatesh
: how many harmonicsDo the astral plane
But how do you do a Fourier transform?
It's time to open our third eye and switch dimensions: instead of representing time as a plane we're going represent it in its true form, which as we all know is a circle repeating itself.
To help us visualize that, let's make the curve draw into a spinning disk:
Rotation speed
Tweaking the rotation speed gives us different shapes. Let's graph the average horizontal position based on the rotation speed:
Let's plug a square wave generate into this graph and see what happens:
Input frequency
What we have here isn't exactly a Fourier transform, but it's close enough. It does what Fourier transforms does: represent signals as a function of frequency instead of a function time.
Shape of things to come
Every thing can be represented as signal, and every signal can be represented as an infinite sum of sine waves. So
Final thoughts
That's all for today folks, hope you enjoyed the ride.
All animations in this post are computed in real-time using WebGL and SVG, feel free to look at the souce code and even run it locally using the readme
instructions!
Discuss on HN - Discuss on GitHub
Appendix
The center of mass visualization is based on work by Grant Sanderson from 3Blue1Brown, I recommend checking his videos out if you're interested in this topic.
If you're like me the formula might be hard to grasp:
Here is what I ended up with using JavaScript and simple math:
function kindaFFT({
// Input signal
g = (x) => Math.sin(x * Math.PI * 2),
// Rotation frequency
f = 1,
// How many samples should we collect
samples = 64,
} = {}) {
// Store the graph data
let x = []
let y = []
// Store weighted averages
let centerX = 0
let centerY = 0
// Store averages weight
let divider = 0
// Loop over all the angles
for (let i = 1; i <= samples; i++) {
// Loop for each winding rotation
for (let t = (i / samples) * f; t <= f; t++) {
// Get the curve value for this angle and winding
let value = g(t / f)
// We now have a vector: an angle and an amplitude,
// convert it vector into cartesian coordinates:
centerX += value * Math.cos(t * Math.PI * 2)
centerY += value * Math.sin(t * Math.PI * 2)
// Add the value to the average weights
divider += value
}
}
// Compute the average position
return {
x: centerX / divider,
y: centerY / divider,
}
}