Train. Track. Transcend.
Every hard set asks the same question: how many reps did I have left? Guess high and you leave growth on the bar. Guess low and you dig a hole you'll pay for on Thursday. agoge answers it with numbers — from two honest sets it learns your lift, then tells you the cost of every set after that.
I the number every program is written in
Reps in reserve — how many you could still have done — is the unit serious programming runs on. It's how you know whether today counted, whether you've recovered, and whether next week goes up or holds.
And self-reported RIR is unreliable, in a consistent direction. Lifters routinely stop with more left than they think, and log it as a hard set. Nobody's being lazy — perceived effort simply isn't a calibrated instrument, and it drifts as you fatigue within a session.
agoge doesn't ask how it felt. It asks what you lifted, and works backwards from a model of your own lift.
II two sets in, and it knows the lift
Give it two honest sets at different weights — one heavy and low-rep, one lighter and higher-rep. That's enough to fit the curve joining load to reps for you, on that lift. No 1RM test, no maximal single, no grinding to failure under a loaded bar.
calibrate
from ablevlabs import agoge bench = agoge.calibrate( weight1=315, reps1=5, # the heavy set weight2=275, reps2=9, # the lighter one name="Bench", )
Notice the two markers sit slightly off the line. That's not sloppiness — it's the fit. Real sets never lie perfectly on a curve, and a model that passed exactly through both would be describing your two sets rather than your lift.
agoge also tells you how much to trust the fit. Two sets close together in weight barely constrain the curve; two far apart pin it down. It scores that confidence out loud instead of quietly treating both as equally good, and flags any set that contradicts the pattern the others describe.
ask it anything on the curve
# I've never benched 295. What should I expect? bench.reps_at(295) # reps if taken to failure bench.weight_for(reps=12) # the load for a 12-rep set bench.rir_at(weight=275, reps=7) # what a 275x7 actually cost
III a readout on every set
You did 275 for 8. Was that a hard set, or a warm-up wearing a hard set's clothes? agoge answers in RIR and RPE, labels it in plain language, and tells you what to do with the next one.
start a session, log a set
w = agoge.Workout(bench) w.target_reps(weight=275, rir=2) # how many to leave 2 in the tank, right now w.log_set(weight=275, reps=8) # log it, get coached w.log_set(weight=275, reps=7) w.log_set(weight=275, reps=6) w.summary() # the whole session, totalled
Right now is doing real work in that first line. Your fifth set is not your first set. agoge tracks fatigue as the session accumulates, so the rep target at set five is lower than the one at set one — because by then you are, measurably, a different athlete. A program written on paper can't know that.
IV set five is not set one
Ask for RIR 2 across five sets at a fixed weight and the honest answer isn't "8, 8, 8, 8, 8." It's a staircase down. Holding the reps constant instead means each set creeps closer to failure than the last — which is exactly how a session that looked reasonable on paper turns into a week of bad recovery.
V three ways to write the session
Tell agoge the shape you want and it fills in the numbers, fatigue already priced in.
Same proximity to failure every set. Reps fall as fatigue climbs — which is the point, and what a fixed rep target gets wrong.
Straight sets across. agoge tells you what each one costs in RIR, so you can see the last one is far harder than the first.
One heavy set, then volume behind it at a load that leaves you standing.
all three
# I. hold RIR 2 for five sets - reps will step down w.build_plan(weight=275, target_rir=2, sets=5) # II. five sets of eight - shows what each one costs w.build_plan(weight=275, reps=8, sets=5) # III. a heavy top set, then back-offs behind it w.build_plan(weight=315, target_rir=1, sets=1, backoff_pct=0.85, backoff_sets=3)
VI start to finish
Nothing hidden, nothing wrapped in an app. Calibrate, plan, log, review.
bench day
from ablevlabs import agoge # once, ever - from two sets you already did bench = agoge.calibrate(weight1=315, reps1=5, weight2=275, reps2=9, name="Bench") # today w = agoge.Workout(bench) w.build_plan(weight=275, target_rir=2, sets=4) # as it happens w.log_set(weight=275, reps=9) # beat the target - it adjusts w.log_set(weight=275, reps=7) w.log_set(weight=275, reps=6) w.log_set(weight=275, reps=5) w.summary() # tonnage, average RIR, fatigue accrued
VII it becomes yours
The default fatigue curve is a sensible starting point, not a claim about you. Hand agoge your own logged sessions and it fits the model to your data — then, the part that matters, validates it against sets it wasn't allowed to see. If the personalised model doesn't actually beat the default on unseen sets, it says so, instead of flattering you with a curve that only describes the past.
fit the fatigue model to your own logs
sessions = [
{"weight": 275, "reps": 9, "set_number": 1},
{"weight": 275, "reps": 7, "set_number": 2},
{"weight": 275, "reps": 6, "set_number": 3},
# ... as many as you have
]
agoge.fit_fatigue(sessions) # fits, then validates on held-out sets
VIII what's underneath
agoge runs on VPFM — a formal model of how close to failure a set puts you, given load, reps, and accumulated fatigue. It's derived and written up in full, openly, so you can disagree with the reasoning rather than accept a number on faith.
That's the posture throughout. A coaching tool that won't show its working is asking for trust it hasn't earned. This one hands you the derivation and invites the argument.
IX cheat sheet
install & import
# install or upgrade to the latest pip install -U ablevlabs from ablevlabs import agoge
calibrate a lift
bench = agoge.calibrate(weight1=315, reps1=5, weight2=275, reps2=9, name="Bench")
run a session
w = agoge.Workout(bench) w.target_reps(weight=275, rir=2) # reps to leave 2 in reserve w.log_set(weight=275, reps=8) # log a set, get coached w.build_plan(weight=275, target_rir=2, sets=5) # prescribe today's sets w.summary() # the session, totalled
learn it
agoge.help() # the ideas behind RIR and fatigue, explained agoge.info() # every function, in plain English
| calibrate() argument | what it does |
|---|---|
weight1 / reps1 | The heavier set — lower reps. Required. |
weight2 / reps2 | The lighter set — higher reps. The further apart these two are, the more confident the fit. |
name | What to call the lift, so readouts say "Bench" rather than "lift". |
| build_plan() argument | what it does |
|---|---|
weight | The load you'll be working at. Required. |
target_rir | Hold this proximity to failure across the sets; reps step down as fatigue builds. |
reps | Hold this rep count instead, and be shown what each set costs in RIR. |
sets | How many working sets to prescribe. |
backoff_pct / backoff_sets | Run a top set, then volume behind it at a fraction of the load. |
Λ start here
Next time you bench, do one heavy set and one lighter one, and write down what you actually got. That's the whole setup. From there agoge can tell you the cost of every set that follows.