← learning by building
1 / 5
the lab · projects
siddharth1

the lab · projects

balancing a pendulum, the hard way

a pendulum on a sliding cart, and a pile of learning algorithms racing to balance it. no RL libraries.

by siddharth · 2026 · from scratch · 3 min read

turn the page →

the lab · projectsthe bench
siddharth2
the lab · projectsthe bench
siddharth3
the lab · projectsthe bench
siddharth4
Fig 1Pendulum benchDrag, ← →, or the timeline belowepisode 1
Episodes · 500
Bob: the episode replaying nowDrum: the longest each episode stayed uprightmonte carlo, easy mode: 500 of its 1,000 training episodes, evenly spaced, replayed at true speed on a 10 m rail with a 2 m arm. The motor dial shows the push each step. The drum and the timeline chart each episode's longest stretch with the bob above the rail, out of a 10.0 s limit.
click to explore ↗

a block on a rail with a pendulum hanging off it. push the block left or right, get the pendulum upright, keep it there. the twist is that i wrote all of it myself: the physics, the reward, the training loop, and the graphs. no gymnasium, no RL libraries. a very confident thing to say right before deriving equations of motion by hand.

§ 01

the setup

a 15 kg block on a 10 metre rail, a 2 metre arm, a 5 kg bob on the end, and a motor that pushes with up to 100 newtons. physics runs 240 times a second. the agent gets a say 30 times a second, and that number isn't a guess: an upright pendulum this long starts to fall in about half a second, and catching it takes ten-odd decisions in that time.

two modes. easy starts almost upright, so it's just balancing. normal starts hanging straight down, so it has to swing itself up first. that's where things got interesting (read: stalled).

§ 02

the reward, round two

the first reward paid the running total of time spent upright, every step. so step 100 paid a hundred times what step 1 did, and an episode's score grew with the square of its length. oops.

spinning averages half the reward for zero skill.

the fixed one scores one step at a time: mostly how upright the arm is, minus a little for drifting toward the walls, minus a little for spinning. that last one is there because a pendulum whirling round and round is above the rail half the time, and spinning averages half the reward for zero skill.

none of it gets checked by staring at numbers. it gets checked by ordering: upright and still has to beat hanging, the middle has to beat the wall, still has to beat spinning. the first version rewarded spinning (a sign error) and failed that test straight away.

§ 03

a very big spreadsheet

first real learner: tabular Q-learning. chop every number the agent sees into bins, keep a table of how good each push is from each bin, nudge the table after every step.

with eight angle bins, the whole region where balancing is even possible fit inside two of them. so to the agent, nearly upright and about to fall looked identical. thirty-six bins fixed that, and it solved easy mode.

also, on a fresh all-zero table, picking the best push always picked the first one: full left. it pushed left 80% of the time until i broke ties at random.

normal mode was another story. on 100,000 episodes of the swing-up, the best it ever managed was 12.0 seconds above the rail, and by the end it was averaging 2.1. one table has to cover a big, coarse swing and fine control at the top, and every extra bin multiplies the table.

§ 04

monte carlo

next up, Monte Carlo control. instead of updating after every step, it plays a whole episode, then walks back through it and credits every push with what actually happened afterwards. no guessing about the future, it just waits and finds out.

it solved easy mode inside 1,000 episodes: every one of its last 100 held for the full 10.0 seconds. the Q-learning run i'm comparing it to was still averaging 5.7 at the end of 10,000. (they don't use the same bins or the same number of pushes yet, so call it a promising heat, not a fair race.)

on the swing-up it gets above the rail and holds it for up to 10.3 seconds, averaging 2.8 by the end. better, not solved.

the original plan was model-based RL: learn the physics, then plan through it. that's still the next stop.

Easy mode: seconds balanced while trainingrandom: 1,000 episodes, ending at 1.9 seconds on average. q-learning: 10,000 episodes, ending at 5.7 seconds on average. monte carlo: 1,000 episodes, ending at 10.0 seconds on average. Capped at 10 seconds.0s5s10s1101001k10ktraining episodes (log) →
easy mode · longest stretch balanced per training episode, exploration on, rolling mean · capped at 10s
  • random
  • q-learning
  • monte carlo
click to explore ↗

a block on a rail with a pendulum hanging off it. push the block left or right, get the pendulum upright, keep it there. the twist is that i wrote all of it myself: the physics, the reward, the training loop, and the graphs. no gymnasium, no RL libraries. a very confident thing to say right before deriving equations of motion by hand.

§ 01

the setup

a 15 kg block on a 10 metre rail, a 2 metre arm, a 5 kg bob on the end, and a motor that pushes with up to 100 newtons. physics runs 240 times a second. the agent gets a say 30 times a second, and that number isn't a guess: an upright pendulum this long starts to fall in about half a second, and catching it takes ten-odd decisions in that time.

two modes. easy starts almost upright, so it's just balancing. normal starts hanging straight down, so it has to swing itself up first. that's where things got interesting (read: stalled).

§ 02

the reward, round two

the first reward paid the running total of time spent upright, every step. so step 100 paid a hundred times what step 1 did, and an episode's score grew with the square of its length. oops.

spinning averages half the reward for zero skill.

the fixed one scores one step at a time: mostly how upright the arm is, minus a little for drifting toward the walls, minus a little for spinning. that last one is there because a pendulum whirling round and round is above the rail half the time, and spinning averages half the reward for zero skill.

none of it gets checked by staring at numbers. it gets checked by ordering: upright and still has to beat hanging, the middle has to beat the wall, still has to beat spinning. the first version rewarded spinning (a sign error) and failed that test straight away.

§ 03

a very big spreadsheet

first real learner: tabular Q-learning. chop every number the agent sees into bins, keep a table of how good each push is from each bin, nudge the table after every step.

with eight angle bins, the whole region where balancing is even possible fit inside two of them. so to the agent, nearly upright and about to fall looked identical. thirty-six bins fixed that, and it solved easy mode.

also, on a fresh all-zero table, picking the best push always picked the first one: full left. it pushed left 80% of the time until i broke ties at random.

normal mode was another story. on 100,000 episodes of the swing-up, the best it ever managed was 12.0 seconds above the rail, and by the end it was averaging 2.1. one table has to cover a big, coarse swing and fine control at the top, and every extra bin multiplies the table.

§ 04

monte carlo

next up, Monte Carlo control. instead of updating after every step, it plays a whole episode, then walks back through it and credits every push with what actually happened afterwards. no guessing about the future, it just waits and finds out.

it solved easy mode inside 1,000 episodes: every one of its last 100 held for the full 10.0 seconds. the Q-learning run i'm comparing it to was still averaging 5.7 at the end of 10,000. (they don't use the same bins or the same number of pushes yet, so call it a promising heat, not a fair race.)

on the swing-up it gets above the rail and holds it for up to 10.3 seconds, averaging 2.8 by the end. better, not solved.

the original plan was model-based RL: learn the physics, then plan through it. that's still the next stop.

Normal mode: seconds above the rail while trainingrandom: 1,000 episodes, ending at 0.0 seconds on average. q-learning: 100,000 episodes, ending at 2.1 seconds on average. monte carlo: 10,000 episodes, ending at 2.8 seconds on average. Capped at 20 seconds.0s10s20s1101001k10k100ktraining episodes (log) →
normal mode · longest stretch above the rail per training episode, exploration on, rolling mean · capped at 20s
  • random
  • q-learning
  • monte carlo
click to explore ↗

a block on a rail with a pendulum hanging off it. push the block left or right, get the pendulum upright, keep it there. the twist is that i wrote all of it myself: the physics, the reward, the training loop, and the graphs. no gymnasium, no RL libraries. a very confident thing to say right before deriving equations of motion by hand.

§ 01

the setup

a 15 kg block on a 10 metre rail, a 2 metre arm, a 5 kg bob on the end, and a motor that pushes with up to 100 newtons. physics runs 240 times a second. the agent gets a say 30 times a second, and that number isn't a guess: an upright pendulum this long starts to fall in about half a second, and catching it takes ten-odd decisions in that time.

two modes. easy starts almost upright, so it's just balancing. normal starts hanging straight down, so it has to swing itself up first. that's where things got interesting (read: stalled).

§ 02

the reward, round two

the first reward paid the running total of time spent upright, every step. so step 100 paid a hundred times what step 1 did, and an episode's score grew with the square of its length. oops.

spinning averages half the reward for zero skill.

the fixed one scores one step at a time: mostly how upright the arm is, minus a little for drifting toward the walls, minus a little for spinning. that last one is there because a pendulum whirling round and round is above the rail half the time, and spinning averages half the reward for zero skill.

none of it gets checked by staring at numbers. it gets checked by ordering: upright and still has to beat hanging, the middle has to beat the wall, still has to beat spinning. the first version rewarded spinning (a sign error) and failed that test straight away.

§ 03

a very big spreadsheet

first real learner: tabular Q-learning. chop every number the agent sees into bins, keep a table of how good each push is from each bin, nudge the table after every step.

with eight angle bins, the whole region where balancing is even possible fit inside two of them. so to the agent, nearly upright and about to fall looked identical. thirty-six bins fixed that, and it solved easy mode.

also, on a fresh all-zero table, picking the best push always picked the first one: full left. it pushed left 80% of the time until i broke ties at random.

normal mode was another story. on 100,000 episodes of the swing-up, the best it ever managed was 12.0 seconds above the rail, and by the end it was averaging 2.1. one table has to cover a big, coarse swing and fine control at the top, and every extra bin multiplies the table.

§ 04

monte carlo

next up, Monte Carlo control. instead of updating after every step, it plays a whole episode, then walks back through it and credits every push with what actually happened afterwards. no guessing about the future, it just waits and finds out.

it solved easy mode inside 1,000 episodes: every one of its last 100 held for the full 10.0 seconds. the Q-learning run i'm comparing it to was still averaging 5.7 at the end of 10,000. (they don't use the same bins or the same number of pushes yet, so call it a promising heat, not a fair race.)

on the swing-up it gets above the rail and holds it for up to 10.3 seconds, averaging 2.8 by the end. better, not solved.

the original plan was model-based RL: learn the physics, then plan through it. that's still the next stop.

the lab · projectsthe scoreboard
siddharth5

the scoreboard · seconds upright, end of training

how they did

random1,000 easy · 1,000 normal episodes

easy

1.9s

normal

0.0s

q-learning10,000 easy · 100,000 normal episodes

easy

5.7s

normal

2.1s

monte carlo1,000 easy · 10,000 normal episodes

easy

10.0s

normal

2.8s