suppose you're developing a theory of how the world works.
if you're a software developer, you might be familiar with a concept of a game loop. you have some state and update function. you call the update function in a loop, and so the state changes over time.
in a sense, a game loop describes the world of your game. most games are pretty complex but here's a very simple one — a counter:
let initialState = 0
function update(state) {
return state + 1;
}
function loop() {
let state = initialState;
while (true) {
state = update(state);
}
}this "world" just has a single number that grows with each tick: first it's 0, then it's 1, then it's 2, and so on till the end of time.
now, what is physics?
the basic idea of physics is to describe the behavior of things in our world in a way that lets us make predictions (and verify whether they were correct). so, putting on the software developer hat, you could think of physics as "reverse-engineering" the game loop of the universe itself — finding its "state" and "update function". we just need to find all the "things" and describe how those "things" evolve:
let initialState = [
/* ... particles? positions? forces? ... */
]
function update(state) {
const nextState = /* ??? physical laws ??? */
return nextState;
}
// game loop stays the same
function loop() {
let state = initialState;
while (true) {
state = update(state);
}
}if we fill these two gaps, we can predict everything that happens.
or so we might hope!
when i started writing this, i only meant to write about quantum mechanics as a game loop. however, i wanted to show a few game loops from classical physics first — and learning those has been so interesting that it snowballed into a post of its own. if you struggled with physics but can read pseudocode, i hope this post will convey the joy i had learning about these things.
let's add continuous time
of course, this presupposes that it's possible to describe our universe this way at all. in the real world, time seems to be continuous — it seems to "pass". we somehow need update to describe what happens after a time — not at some "next frame".
to make our game loop continuous, let us add a dt parameter:
function update(state, dt /* a bit of time */) {
const nextState = /* ??? physical laws ??? */
return nextState;
}now our physical laws can refer to a concept of time rather than "nth frame". the "counter" from before would now be a "clock":
function update(state, dt) {
return state + dt; // instead of state + 1 before
}but here is a trick question about the dt parameter.
suppose you wanted to calculate "the state of the world in five seconds". which of these do you think would produce better results?
a) calculating
update(state, 5)onceb) calculating
update(state, 0.01), then passing the result asstateinto the nextupdatecall, and so on, for500timesc) calculating
update(state, 0.00001), then passing the result asstateinto the nextupdatecall, and so on, for500000timesd) these would all produce the same result
(scroll down for answer)
the answer is — for the "clock" example above the answer is (d), but generally for most physics the answer is (c). in fact, to get a perfect answer, we'll want to apply the update an infinite number of times — each with an infinitely small dt. (math can help with this.)
we can think of the nature's game loop as being something like this:
function loop() {
let state = initialState;
for (let dt of time() /* for every bit of time */) {
state = update(state, dt);
}
}imagine dt is actually infinitely small here, and there are infinitely many of them. nature doesn't mind it, so we won't mind it either.
(computers mind it, but luckily this code isn't for computers.)
this is actually rather profound. if you just tried to guess how the world works, you might have looked for laws that say "what happens in three seconds", "what happens in five seconds", and so on. but it's often not possible directly! in practice, physical laws seem to want to be described as "what happens at every instant".
in either case, this actually makes our job easier. we only need to figure out what happens "now" at each moment, not what happens "over time". in other words, our update function doesn't even need to be correct for arbitrary dt — five seconds, three seconds, etc. we only need it to get ever correct-er as dt gets ever smaller.
to sum up, our job is to find the update function for the universe:
let initialState = [
/* ... particles? positions? forces? ... */
]
function update(state, dt) {
const nextState = /* ??? physical laws ??? */
return nextState;
}if we could describe the state and its update function over time, we'd have "solved" the universe and be able to predict anything.
(and who knows, maybe it is written in this pseudocode javascript)
newton's world: a game loop for objects
take newtonian gravity — which has been a successful model for centuries and seemed to work decently for both planets and apples.
when we describe movement of objects (in newtonian gravity), the "state" would be things like masses, positions and momentums:
let initialState = [
{ mass: 42, position: [3, 1, 2], momentum: [-3.2, 4.1, 1.8] },
// .. all the other objects we care about ...
]the "update function" would then update those every instant of time:
function update(state, dt) {
return state.map(obj => {
// how much do other things affect this object?
const forces = calculateGravity(obj, state)
// newton's second law:
const nextMomentum = obj.momentum.map((p, i) => p + forces[i] * dt)
const nextPosition = obj.position.map((x, i) => x + (nextMomentum[i] / obj.mass) * dt)
return { mass: obj.mass, momentum: nextMomentum, position: nextPosition }
})
}here, calculateGravity is a pure function that accumulates how each object is affected by every other object at a given instant:
const G = 6.674e-11
function calculateGravity(obj, state) {
const force = [0, 0, 0]
for (const other of state) {
if (other === obj) continue
const d = [
other.position[0] - obj.position[0],
other.position[1] - obj.position[1],
other.position[2] - obj.position[2]
]
const r = Math.hypot(...d) // distance
const f = (G * obj.mass * other.mass) / (r * r * r)
force[0] += f * d[0]
force[1] += f * d[1]
force[2] += f * d[2]
}
return force
}if you wanted to make it feel more real, you could then introduce additional concepts on top of this framework to implement collisions, bouncing, friction, and so on. many game engines do just that!
in newtonian gravity, the state includes positions, masses and momentums (or, equivalently, velocities) of each relevant object. the update function figures out how objects "act" on each other at each instant in time, and adjusts their momentums and positions.
this model describes the world of "big things" approximately well, and lets us make rough predictions about how it evolves over time.
for some cases, we can easily take an infinite sum of this "instant by instant" calculation to answer "what will happen in ten seconds", but in other cases we can't do it as neatly — so the best we can do are special cases or approximations. still, this is a win because we know how the world "evolves" over time; the rest is computing.
but then, some things really don't fit this model.
like, at all.
light is one of those things.
a rippling wobble
if you try to throw a stone, it rises and falls tracing a parabola. this actually follows from the newtonian model quite nicely: the stone's momentum changes on each instant, eventually pointing down.
most importantly, a stone always lands at a specific place. it may not be the place you were aiming for but at least it is quite specific.
light doesn't work this way.
get yourself a dark room and drill a tiny circular hole in one of its walls. then shine some light from that hole into the room. now go into the room and close the door behind you. what will you see?
if light behaved like a stone — a thing you "throw" that "lands" somewhere — you might expect that all light would concentrate on the opposite wall of the room — in a circle the size of your hole.
but that's not what happens! with a small enough circular hole and clean enough light, you'll see something called an Airy disk:
if you're not convinced that this is different from "an equal circle on the other side of the room", here's a clearer picture with a laser:
this definitely doesn't look like "a single circle in front of me".
this means that light doesn't seem to behave like stones (which only ever "fly forward") — light seems to radiate and spread out. but radiate how and by how much and in what manner and so on?
to answer this, let's simplify the situation to an idealized case.
suppose it was possible to "narrow down" our hole down to a single point. in that case, there would be no zebra pattern yet. however, light still wouldn't "fly forward" like a stone. in the idealized case of a single point, the light would spread equally in every direction.
in a sense, this makes light behave almost opposite from a stone!
when we talk about things like stones or planets or apples in the newtonian paradigm, we see them as points moving forwards in space. you'd expect them to "keep flying" in a single direction.
but when we talk about light, it seems to behave a lot more like a vibration — it's as if there's "wobbling" in the space, and that "wobbling" itself propagates through it. since every tiny point of the space "touches" all of its neighbor points at once, and no neighbor is special, the "wobbling" will naturally spread in every direction.
okay, but why do we get this zebra pattern then?
to get the zebra pattern, we need to add the hole dimensions back, which we lost with the idealized single point. a hole contains infinitely many points, each point contributing its own "wobble".
now let's see what happens when these "wobbles" overlap.
imagine two people shaking a tree to obtain some fruit. if their shakes are synchronized, they're shaking the tree twice as hard. but if their shakes come at precisely the opposite times, each shake will cancel the other's shake out, and the tree won't move at all. if they shake at different speeds, the tree may start moving in peculiar patterns. we might say their shakes "interfere" with each other.
the same thing happens here. when you drill an actual hole in an actual room, no matter how small it ends up, it will actually be composed of infinitely many "single points". the equal wobbles spreading from each of those points will overlap with each other. some will mutually amplify, while others will cancel each other out.
and that's how you get this "in and out" zebra pattern — which, if you think about it, is a similar pattern to ripples in a pond:
the space seems to "wobble"
each single point spreads "wobbling" equally in all directions
those travelling "wobbles" overlap, creating peaks and valleys
those moving peaks and valleys look like travelling "ripples"
so the "wobbling-over-time" seems to "ripple-over-space"
that's what physicists call waves.
note that if you've read about this before, you might have seen a different pattern like this (known as "single slit diffraction"):
this experiment uses a rectangular hole that's thinner on one side, which is why it loses the circular simplicity of the pond. i think the circular hole looks much more illustrative so i used it instead.
let's now bring it back to the update function.
a game loop for waves
the newtonian update function evolves objects with positions.
but to describe light, we'd need different state than "objects with positions, masses, and momentums". we'd need to describe a thing that can "wobble" — perhaps a pond, or maybe a field — something like Map<Point, PointState>. our update function would then need to describe how a "wobble" at a point propagates through that field at each instant of time — from a point to its neighbors, and so on.
turns out, there's a very concise and elegant way to do this.
first, let's imagine our field filled up with some data at every point:
type Point = [number, number, number]
type PointState = { value: number, velocity: number }
let initialState: Map<Point, PointState> = Map.fill(
// set up our infinite map with these at every point
{ value: 0, velocity: 0 }
)we only need to store two things for each point — a value (how "far" it has "wobbled" at this point) and a velocity (how much the value is changing this instant, and whether it's going up or down).
don't confuse value with coordinates of the points. the value is solely a measure of the "wobbling" itself (our field). in a pond, the value means "how tall is the water right here". if you imagine a field as a grid of numbers in space, the values are those numbers.
now, how do we actually write the update function?
the big idea is that whenever there's a "rip" in our field at some point (e.g. one point dipping below or above the surrounding water level), we want it to pull back — the pond wants to "mend itself".
we can quantify this by counting how "far" it is from its neighbors:
function tension(field, [x, y, z]) {
const neighbors = [
field.get([x + 1, y, z]), // right
field.get([x - 1, y, z]), // left
field.get([x, y + 1, z]), // above
field.get([x, y - 1, z]), // below
field.get([x, y, z + 1]), // in front of
field.get([x, y, z - 1]) // behind
].map(ps => ps.value)
const neighborSum = neighbors.reduce((a, b) => a + b, 0)
const ownValue = field.get([x, y, z]).value
return neighborSum - neighbors.length * ownValue
}if the tension is 0, our point is happy to stay where it is. if it's positive, the value is too low and "wants" to go up to match the neighbor values. if the tension is negative, it means our value is too high, and "wants" to go down. it wants to be like its neighbors!
now let's write the update function to make it do that.
first attempt
our first attempt might look like this:
function update(state, dt) {
return state.map((pointState, point) => {
const { value } = pointState
const nextValue = value + tension(state, point) * dt
return { value: nextValue }
})
}this says: if some point's value disagrees with its neighbors, we pull it closer to them. do this for all points forever, end of story.
curiously, this actually works, but it gives us something other than light. the behavior we've just discovered is how temperature works: every disturbance is eventually settled by every point averaging out.
this is cool (sometimes literally) but it's not how light works!
somehow, light doesn't average out.
it keeps on moving.
second attempt
this is actually why we're storing per-point velocity in addition to value. instead of adjusting value (current value at a point), we'll be adjusting the velocity, and put that in control of the value:
function update(state, dt) {
return state.map((pointState, point) => {
const { value, velocity } = pointState
const nextVelocity = velocity + tension(state, point) * dt
const nextValue = value + nextVelocity * dt
return { value: nextValue, velocity: nextVelocity }
})
}this is enough to create a rippling wobble — aka a wave!
but why?
we'll first answer why it wobbles, then why it ripples.
why it wobbles
why doesn't it just die out, like temperature?
suppose some point has a low value — a dip among its neighbors. this will give that point tension: it will "want" to align with them. think of a person trying to fit in with the rest of the society.
at each instant, we put tension into velocity. look what happens: until we've reached the goal, the velocity keeps accumulating — first, by a lot (high tension), then by less (lower tension), and finally it stops increasing when value reaches the neighbor average.
okay, we've reached the goal, so we can relax now?
no! we've only stopped the accumulation — it's like we've been pressing gas less hard and we just took the foot off the pedal. but we're still moving — in fact, we're moving faster than ever before!
the velocity is still positive, so we keep increasing the value.
oops!
we blow past the target we were aiming for (the average of our neighbors) so now we'll become the high point among the neighbors, the tension turns negative, and the neighbors start pulling us down. we've deviated again, so we're feeling the pressure to conform.
like a car losing speed and then accelerating backwards, it will take time before our velocity points downwards and we finally reverse course. what's next? we reach the goal, but do we stop there?
no! again, we're going to overshoot for the same reason. we've been accumulating negative velocity, and now our value dips below the neighbor average again. and the peer pressure now points up...
so this is where the "wobbling" comes from! we keep pressing "gas" until we reach the target so naturally we always overshoot it. then we keep pressing "gas" in the opposite direction and the same thing happens. it's like we are destined to forever miss the target.
the value at a point is wobbling back and forth.
why it ripples
so why doesn't the "wobbling" go on forever in a single point? why does it seem to quiet down in one place while rippling on elsewhere?
well, it's because we're always trying to move towards the neighbors' averages — but they have already changed because they've also tried to move closer to us! so actually, on the second pass they got closer, so although we still do overshoot, their pull wasn't as strong. however, since our neighbors have moved in our direction, our neighbors' neighbors will now start feeling the pull towards the new averages around them, so they'll move too, etc.
it's like if someone is trying very hard to be like everyone else but they try too hard and overdo it. however, now that they're starting to backpedal, the people around them have already started to adjust to them, so the people around those people start moving, and so on.
this is why, if you "poke" any point and change its value, it will try to "align" with neighbors, disturbing them in the process, and so the disturbance will keep getting spread out in all directions forever.
that's what physicists call a wave!
let's add continuous space
one problem with our formulation above is that we assume each point has a fixed set of six neighbors from each side of an infinite grid:
const neighbors = [
field.get([x + 1, y, z]), // right
field.get([x - 1, y, z]), // left
field.get([x, y + 1, z]), // above
field.get([x, y - 1, z]), // below
field.get([x, y, z + 1]), // in front of
field.get([x, y, z - 1]) // behind
].map(ps => ps.value)alas, that's not very useful for our universe because it seems like you can find a new point between any two points. in other words, our space seems to be continuous, so we need to somehow fix this.
earlier, we had the same problem with time, and we fixed it by replacing the + 1 with + dt "instant" and using dt everywhere.
we can actually apply the same fix here. let's swap + 1 for + ds:
function tension(field, [x, y, z], ds /* a bit of space */) {
// ...
const neighbors = [
field.get([x + ds, y, z]), // right
field.get([x - ds, y, z]), // left
field.get([x, y + ds, z]), // above
field.get([x, y - ds, z]), // below
field.get([x, y, z + ds]), // in front of
field.get([x, y, z - ds]) // behind
].map(ps => ps.value)
// ...instead of navigating space by going "up one cell" or "left one cell", we'll navigate it as "up a step" and "down a step", and we'll write "a step" as ds — an abstract "arbitrarily small distance" number.
in other words, what dt is to time, ds is to space.
of course, this ds has to come from somewhere. in math it comes from differentiation — literally "zooming in forever" around a point.
we could imagine that ds supplied by the structure of the field:
function update(state, dt) {
return state.map((pointState, point, ds /* a bit of space */) => {
// ...
const nextVelocity = velocity + tension(state, point, ds) * dt
// ...
})
}like with dt, we should keep in mind that our calculations don't need to be correct for any particular ds. rather, it's enough that our update formula gets ever correct-er as the ds gets ever-smaller.
fixing up the units
to complete the shift to ds, we need to fix up the units.
conceptually, tension means "how fast value should accelerate", so its unit should be value/time^2. but we used to return a distance.
this worked accidentally but now that we're using an arbitrary step size, we need to fix up the units by adding * (C * C) / (ds * ds):
const C = 1
function tension(field, [x, y, z], ds) {
// ...
const result = neighborSum - neighbors.length * ownValue
return result * (C * C) / (ds * ds)
}we're doing two things:
the
/ (ds * ds)rescales it so that the choice ofds"zoom level" doesn't make the tension seem larger or smaller.the
* (C * C)introducesCas a speed of our waves (set to one), connecting time with space so we can fix up the units.
now the return type of tension is value*(distance/time)(distance/time)/(distance*distance), or simply value/time^2, which is exactly what we would want from an acceleration of the value.
with this set of changes, our update function describes a wave in a continuous space — for example, how a sound wave spreads.
now the big question: does it describe light?
first stumble: polarization
let's recap our previous hypothesis for the nature of light:
type Point = [number, number, number]
type PointState = { value: number, velocity: number }
let initialState: Map<Point, PointState> = Map.fill(
{ value: 0, velocity: 0 }
)
function update(state, dt) {
return state.map((pointState, point, ds) => {
const { value, velocity } = pointState
const nextVelocity = velocity + tension(state, point, ds) * dt
const nextValue = value + nextVelocity * dt
return { value: nextValue, velocity: nextVelocity }
})
}
function tension(field, [x, y, z], ds) {
// ...
}this works for sound, and for a while, physicists thought that it also can describe light. however, experiments were showing that light waves carry more information than "a number moving up and down".
check out the next ten seconds on this video:
there are two pieces of fancy glass ("filters") placed after each other. when you turn either sideways, they both block out the light.
in particular, notice that when he rotates the filter underneath, the intersection somehow shines through the dimmed filter above it:
this is weird on multiple levels:
we know light has a color (or frequency — how fast the wave's peaks arrive) and brightness (or amplitude — how large each wobble is), but neither of those things "cares" about rotation. so why does rotating a filter do anything to the waves at all?
if the filter on top was filtering by color or by brightness, we'd expect it to "not to know" about the filter beneath it. but somehow it lets through only rays that passed through the filter below, blocking out everything else. how could it possibly "know" which waves have passed through the filter below? there's just not enough information in our model of the wave.
the fact that rotation plays a role is a giveaway hint — not only do waves have frequency and amplitude, but they also come at us carrying an angle. this angle is not about "where the wave is coming from" — it's more like internal information about the wave itself. a wave coming from a point could be a "2 o-clock wave". another wave coming from the same point could be a "9 o-clock wave". that angle is really about "how is it wobbling while moving towards us". it's like if you classified dogs by the direction they wag their tails.
then this explains filters: maybe the outer filter only passes "2 o-clock waves" through while turned. normally, everything goes dark because the screen sends waves at the wrong angle for it. but the filter beneath projects those waves onto another angle, so the light coming out of it wobbles at just the right angle for the outer filter.
this effect is called polarization.
a field of arrows
to account for angles, we need to change the state data structure:
type Point = [number, number, number]
type Arrow = [number, number, number]
type PointState = { value: Arrow, velocity: Arrow }
in particular, we don't have a field of numbers anymore. since waves are carrying a direction of wobble (e.g. "2 o-clock wave"), our field needs to be capable enough to store information at each point that lets such wobbles propagate. instead of an infinite space of raw numbers (as we had with sound), we'll have an infinite space of 3D arrows. the next ten seconds here give an intuition:
our wobbles now aren't just pushing numbers up and down, but they squeeze and rotate arrows on their path. however, just like before, those displacements spread from neighbors to neighbors outwards.
this is confusing so let's take stock of how arrows work:
imagine a sound wave aimed directly at your face. it wobbles by contracting air but the wobbles happen alongside the travel direction. it doesn't wobble "left to right" or "up and down" as it moves towards you — it wobbles "apart from / towards you". since this exactly matches the direction of travel, we didn't need to reflect that in the formulas — it's superfluous.
on the other hand, a light wave aimed directly at your face does not wobble along the travel direction at all. on the contrary, it never wobbles "apart from / towards you", and instead it wobbles "left to right" or "up and down" or any other axis.
the "rotation" carried by light waves is an axis (e.g. "2 o-clock wave") but the waves themselves may come from any direction. this is why, to avoid losing information, the field itself needs to be a field of 3D arrows — a 3D arrow per each point. a field tracks the state of all waves together so it needs to be able to sum up how differently rotated waves from different sides are rotating and stretching each point (i.e. 3D arrow) in the field.
second stumble: tension
now what should the update function do with the arrows?
recall that previously our update function was using tension, pushing each point to "get closer" in value to its neighbors' average:
function update(state, dt) {
return state.map((pointState, point, ds) => {
const { value, velocity } = pointState
const nextVelocity = velocity + tension(state, point, ds) * dt
const nextValue = value + nextVelocity * dt
return { value: nextValue, velocity: nextVelocity }
})
}we could try to apply tension to arrows (have each arrow rotate and stretch towards its neighbors), but this misses a needed constraint.
suppose a light wave is moving towards your face. if the update function of the light field was using tension, nothing would stop the light wave from wiggling "towards / from you" just like sound does.
however, we know experimentally that this doesn't happen. light coming at you can wiggle up-down or left-right (i.e. "sideways") but it just doesn't wiggle back-and-forth — not even a little bit.
so the formula has to do something different.
wires and magnets
now let's take a slight detour from the nature of light.
this wonderful video about Maxwell's laws has two illustrations.
in the 1820s, it was discovered that an electric current (i.e. a wire with electricity flowing) pulls a compass needle perpendicular to the flow, i.e. a change in electricity creates a magnetic field around it:
a decade later, it was discovered that a change in the magnetic field (e.g. by moving a magnet) will induce current in the wire around it:
so there's these two fields: magnetic field and electric field, and there's this weird symmetry between them where one's speed of change relates to how much the other one is curling around it.
in other words, there is a geometric transformation happening: as one of the fields is changing, the other one springs up around it, sort of "curling" around it, proportionally to how fast the change is.
now forget about electricity and magnetism. suppose you just have a field of arrows and want to find "whirlpools". this function does just that — for each point, it can answer whether it's "curly" around it:
function curl(field, [x, y, z], ds) {
// which way the arrow points as you step along each axis
const dx = (field.get([x + ds, y, z]) - field.get([x - ds, y, z])) / (2 * ds)
const dy = (field.get([x, y + ds, z]) - field.get([x, y - ds, z])) / (2 * ds)
const dz = (field.get([x, y, z + ds]) - field.get([x, y, z - ds])) / (2 * ds)
// do the surrounding arrows turn as you loop around this point?
return [
dy.z - dz.y, // looping around the x-axis
dz.x - dx.z, // looping around the y-axis
dx.y - dy.x, // looping around the z-axis
]
}intuitively, you can think of curling as a measure of "how much does it look like a whirlpool around this point". here's a 2D intuition:
here, the yellow arrows are the original field whose curl we're measuring, while the blue arrows are the curl at each point.
note how blue arrows work as "whirlpool detectors" of the yellow arrows' flow. blue arrows stick up where the yellow gets curly (in one direction), blue arrows stick down when the yellow gets curly in the other direction, and they're zero-ish where yellow is not curly.
it's like stalactites and stalagmites that grow only inside tornadoes.
the above picture has a 2D input vector field; in 3D, it is even more difficult to imagine. this 3D visualization may help somewhat:
think of the arrows as water flow, and then think of the ball as an unmoving sphere that can only rotate in the water. the question is: is the water at this point making the ball spin? if yes, imagine the axis it spins around. the arrow of the curl around that point will stick out of that axis exactly like the electric field sticks out here:
that's the curl at that point. every time the field gets "curly" around that point, the curl "sticks out" perpendicularly.
in other words, curl converts "whirlpools" into "thumbs up".
now we see the relationship between the two fields:
electric curl changes the magnetic field
magnetic curl changes the electric field
as one field changes, the other field curls around it. the curlier one field is, the faster the other field changes. this works in both directions, at every point, all the time — like in a game loop.
maxwell's world: a game loop for light
let's describe electric and magnetic fields as an update function:
type Point = [number, number, number]
type Arrow = [number, number, number]
type Field = Map<Point, Arrow>
let initialState = {
electric: Field.fill([0, 0, 0]),
magnetic: Field.fill([0, 0, 0]),
}
const C = 1
function update({ electric, magnetic }, dt) {
const nextElectric = electric.map((e, point, ds) =>
e + curl(magnetic, point, ds) * (C * C * dt)
)
const nextMagnetic = magnetic.map((b, point, ds) =>
b - curl(nextElectric, point, ds) * dt
)
return {
electric: nextElectric,
magnetic: nextMagnetic
}
}
function curl(field, [x, y, z], ds) {
// ... same as before ...
}this is it! (suppose +, - and * are defined for vectors.)
notice that there is no velocity here.
temperature had no velocity too, but that caused the field to immediately average out and killed waves. here, we don't need velocity because the two fields continually set each other off: each field's curl tells the other field how to change at every moment. in a sense, the two fields take turns being each other's velocities.
also, both of the issues we flagged earlier are fixed now. the electric and magnetic fields both deal with arrows, which gives us polarization. and, together with other maxwell equations, this guarantees the waves will only wobble "sideways" (unlike sound).
it turned out that these two fields, locked in a perpetual mutual transformation, explain both electric and magnetic phenomena, and the light itself. the C * C coefficient in the formula is the speed of light squared. in earlier formulas, it was composed of two known coefficients related specifically to magnets and charges. it was a "big moment" for physics that those coefficients, already known and measured experimentally in labs, ended up related to speed of light.
there is a wonderful demo that lets you see how the field "actually" looks like when stuff is changing — the arrows represent the electric field, and the colors (red/green) represent the magnetic field. here's a recording in case it doesn't work in your browser:
finally, you might have noticed that we haven't introduced any way to actually get something "into" the field unless you hardcode it.
we could try to add a list of charges to the state and write an update function for them, but classical physics genuinely has some holes there (for example, if you follow it strictly, every atom must immediately collapse — oh no) that don't have nice classical fixes.
so this sounds like a good place to stop — for now.
epilogue
we've traced a long road and ended up with a few update loops.
the first is newtonian gravity which keeps a list of "objects" and moves them at each instant according to the law of gravity:
let initialState = [
{ mass: 42, position: [3, 1, 2], momentum: [-3.2, 4.1, 1.8] },
// .. all the other objects we care about ...
]
function update(state, dt) {
return state.map(obj => {
// how much do other things affect this object?
const forces = calculateGravity(obj, state)
// newton's second law:
const nextMomentum = obj.momentum.map((p, i) => p + forces[i] * dt)
const nextPosition = obj.position.map((x, i) => x + (nextMomentum[i] / obj.mass) * dt)
return { mass: obj.mass, momentum: nextMomentum, position: nextPosition }
})
}
const G = 6.674e-11
function calculateGravity(obj, state) {
const force = [0, 0, 0]
for (const other of state) {
if (other === obj) continue
const d = [
other.position[0] - obj.position[0],
other.position[1] - obj.position[1],
other.position[2] - obj.position[2]
]
const r = Math.hypot(...d) // distance
const f = (G * obj.mass * other.mass) / (r * r * r)
force[0] += f * d[0]
force[1] += f * d[1]
force[2] += f * d[2]
}
return force
}the second update loop we've looked at is a field wave update loop driven by tension — pulling each point closer to its neighbors:
type Point = [number, number, number]
type PointState = { value: number, velocity: number }
let initialState: Map<Point, PointState> = Map.fill(
{ value: 0, velocity: 0 }
)
function update(state, dt) {
return state.map((pointState, point, ds) => {
const { value, velocity } = pointState
const nextVelocity = velocity + tension(state, point, ds) * dt
const nextValue = value + nextVelocity * dt
return { value: nextValue, velocity: nextVelocity }
})
}
const C = 1
function tension(field, [x, y, z], ds) {
const neighbors = [
field.get([x + ds, y, z]), // right
field.get([x - ds, y, z]), // left
field.get([x, y + ds, z]), // above
field.get([x, y - ds, z]), // below
field.get([x, y, z + ds]), // in front of
field.get([x, y, z - ds]) // behind
].map(ps => ps.value)
const neighborSum = neighbors.reduce((a, b) => a + b, 0)
const ownValue = field.get([x, y, z]).value
const result = neighborSum - neighbors.length * ownValue
return result * (C * C) / (ds * ds)
}(if we manipulate value instead of velocity, we get heat instead.)
finally, we've looked at the update loop for electricity, magnetism, and light — which turned out to be aspects of the same thing:
type Point = [number, number, number]
type Arrow = [number, number, number]
type Field = Map<Point, Arrow>
let initialState = {
electric: Field.fill([0, 0, 0]),
magnetic: Field.fill([0, 0, 0]),
}
const C = 1
function update({ electric, magnetic }, dt) {
const nextElectric = electric.map((e, point, ds) =>
e + curl(magnetic, point, ds) * (C * C * dt)
)
const nextMagnetic = magnetic.map((b, point, ds) =>
b - curl(nextElectric, point, ds) * dt
)
return {
electric: nextElectric,
magnetic: nextMagnetic
}
}
function curl(field, [x, y, z], ds) {
// which way the arrow points as you step along each axis
const dx = (field.get([x + ds, y, z]) - field.get([x - ds, y, z])) / (2 * ds)
const dy = (field.get([x, y + ds, z]) - field.get([x, y - ds, z])) / (2 * ds)
const dz = (field.get([x, y, z + ds]) - field.get([x, y, z - ds])) / (2 * ds)
// do the surrounding arrows turn as you loop around this point?
return [
dy.z - dz.y, // looping around the x-axis
dz.x - dx.z, // looping around the y-axis
dx.y - dy.x // looping around the z-axis
]
}note how newton's gravity was "global" — it had to read global state to update every object — while fields were "local" and only dealt with neighbors. instant changes at distance seem suspicious, and modern theories (including relativity theory) avoid that.
the common thing is that we're describing the behavior of a big thing (the universe) by specifying what we think happens at every point, at every moment, what to remember, and how it transforms.
another common thing is that all of these are models — they have conditions in which they break down. a broken model sometimes reveals a "more correct" model underneath, although even today there is no single model that would answer all of our questions.
finally, all of the update loops so far are from classical physics.
i originally wanted to write about quantum physics but i had too much fun writing about these loops, as i mostly learned about how they work while researching this article. these classical loops break down in pretty big ways, which is a topic for a future article.
but here's a sneak peek.
sneak peek: where is the wave
let's come back to our room, shoot some electrons into the hole, and detect them on the opposite wall. you've tried this with one, and it appears to fly like a stone and land on a particular spot:
great, so an electron is like a stone.
you turn on your electron gun and you leave it shooting a whole lot of electrons into the wall. then you look at the result:
why are they all over the place?
if an electron flies like a stone, it should fly straight and land right opposite. okay, you might say, we know light is a wave, and we know why this happens for light. waves "interfere" with each other.
so maybe, if you fire lots of electrons, they can also move like waves, radiating, overlapping, and canceling each other out.
maybe electrons are waves too. weird but okay.
so you try the experiment again, watching the buildup more closely. you don't want the presumed "waves" related to different electrons to mess each other up so you shoot one electron at a time.
still — shooting one of them at a time — they're kind of smearing around. maybe something is literally knocking them off course?
to check whether the problem is with waves or something knocking off course, you leave two holes open behind the electron gun.
your idea goes like this:
if something's just knocking them off course, you'll see two clusters of electrons, or at least the same picture shifted twice
if something wavy is going on, you'll see a "double slit diffraction" pattern from waves, similar to what light does:
you leave the electron shotgun firing and go for a coffee.
you come back and you see it built up — it's wavy!
oh well, so electrons are waves then.
but then...
what if you shoot them one by one from behind the two holes? then, even if they're wavy, they should never interfere with each other.
you see the pattern building up:
this is the same pattern as light from two holes interfering.
except here you're only shooting one electron at a time. you see each of them land. and each lands in one definite spot. the wavy pattern only shows up in statistics — a distribution over time.
you look at the wall one last time.
it looks as if two waves were interfering.
but waves of what?
and when did they interfere?
where are the waves???
i'm pretty new to learning (and teaching) physics. i'm a novice but i tried to keep things accurate, with a bit of editorial flair.
any material corrections are appreciated — ping . individual experiment descriptions are slightly editorialized but i expect them to be consistent with the current understanding.
some illustrations were taken and cropped from Roger Bach et al 2013 New J. Phys. 15 033018 DOI 10.1088/1367-2630/15/3/033018, CC BY 3.0. Airy disk photo by Roman Napreyev, CC BY-SA 4.0.
the few screenshots of youtube videos are taken from the videos embedded earlier on the page.