Skip to main content

Linear Programming at the Gym

·2305 words·11 mins
Table of Contents

I’m lazy. Not lazy enough to not exercise, but lazy enough to want my gym programs to make themselves.

Coincidentally, I’ve started reading “Linear Algebra and its applications”, and one section caught my attention: How to apply linear algebra to real world problems.

As it turns out, there are lots of ways to optimize processes around you if you know how to convert your real-life problem, into a mathematical model.

The objective
#

Exercising at the gym is a balance. Too little intensity, and you don’t progress as much as you want. Too much intensity, and you get tired (and don’t progress as much as you want).

When you make your programs, you have to take into account your objectives primarily. However, how you attain your objectives is a whole other subject:

  • How do you split your trainings: Push/Pull/Legs ; Lower/Upper ; Mixed
  • How much time do you want to spend daily / weekly at the gym
  • What priorities, for which muscle group
  • Is my left shoulder intact this week ?

So, what if, there was a way to generate a program efficiently, by taking into account all those factors ?

What’s a program
#

Before we begin, there are a few terms I need to explain I (and most people) call a program a set of workouts: sets of exercises, grouped by day

- Monday: Push 
	- Triceps:
		- Deltoid Anterior: 4 sets
		- Cable Rope Overhead Triceps extension: 4 sets
		- Triceps Pushdown: 4 sets
	- Deltoids:
		- Dumbbell Overhead Press: 4 sets
	- ...
- Tuesday: Pull
	- Back:
		- Cable row: 3 sets
	- Biceps:
		- Curl: 4 sets
	- ...
- ...

Rinse and repeat until satisfied (you never are.)

You get the idea. Push/Pull/Leg are terms to describe workouts that are more “push oriented”, “pull oriented”, or “legs”. For instance, a pull up is a pull exercise (of course), and a push up is a push exercise (of course).

These programs are usually made by hand, bought or downloaded from some fitness influencer. But they rarely are tailored to your needs.

Disclaimer
#

I’m not a mathematician, nor am I an operational researcher. I will make mistakes that will make your eyes bleed. This is a small side-project devised to explore a new concept I’ve been learning about

Data gathering
#

I’ll keep it short. I made a dataset of ~50 exercises. Every one of them had:

  • ID: integer
  • Name: string
  • Muscle groups:
    • Primary: array of int
    • Secondary: array of int
  • Is it a push/pull or leg exercise ?

We have the input: data, conditions & target.

We know the expected output: a workout program.

Now, how to create the part in-between ?

Linear Programming
#

Linear Programming (LP) is a mathematical optimization process that allows you to optimize a linear objective function (maximize/minimize it).

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

By “translating” your natural-language problem “I want to generate a workout program that fits my constraints” to a problem of linear equations, you can use a solver to approach the best solution possible.

In the end, you’ll really need to find:

  • An objective function: The target you want to achieve, eg. maximize the volume.
  • Constraints: eg. I don’t want the same exercise to repeat twice over \(x\) days

In a more mathematical setting, this is how they are expressed.

$$ \begin{align*} & \text{Find a vector} && \mathbf{x} \\ & \text{that maximizes} && \mathbf{c}^\mathsf{T} \mathbf{x} \\ & \text{subject to} && A \mathbf{x} \le \mathbf{b} \\ & \text{and} && A \mathbf{x} \ge \mathbf{0} \\ \end{align*} $$

From Wikipedia

Defining the variables
#

Because we are using Linear Programming, we are quite limited as to how the (objective/conditions) function(s) look.

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

In fact: we can only set the weights \(a_n\), and the solver will find the \(x_n\), if possible

tip

This means that this is an impossible problem for a LP solver: \(f(x_1, x_2, x_3) = x_1x_2x_3\)

But we must define what our variables are:

  • What bounds \(x \in [-\infty; \infty]\) or \(x \in [-5;10]\)
  • Integer/Binary/Continuous ?; is it one or zero (binary) ; is \(x\) an integer, or a float ?

For this project, I used two main decision variables:

  • \(x,e,d\): number of sets for exercise \(e\) on day \(d\): integer
  • \(y,e,d\): whether the exercise \(e\) is included in day \(d\): binary

Constraints
#

To guide the algorithms towards the most satisfying solution I’ve had to implement a few constraints.

Constraints are linear (in)equalities. These are valid constraints:

$$ x_1a_1 \geq 0 \\ x_1 + x_2 = 1 $$

This is invalid:

$$ x_1x_2x_3 \geq 0 $$

Let’s get started:

Min sets & max sets
#

It’s the easiest. I don’t want one day with 230 sets of benchpress.

To put it as a constraint, for \(N\) exercises over \(6\) days:

$$ \sum^{N}_{e=0} \sum^{6}_{d=0} x_{d,e} \leq 5 * y_{d,e} \\\ \sum^{N}_{e=0} \sum^{6}_{d=0} x_{d,e} \geq 3 * y_{d,e} $$

“For every exercise, of every day, I want the number of sets to be lesser or equal to 5 if the exercise is selected, or 0 if it is not”

“For every exercise, of every day, I want the number of sets to be greater or equal to 3 if the exercise is selected, or 0 if it is not”

Min/Max number of exercises
#

Sort of easy, also.

For \(N\) exercises over \(6\) days:

$$ \sum^{N}_{e=0} \sum^{6}_{d=0} y_{d,e} \leq 6 * y_{d,e} $$$$ \sum^{N}_{e=0} \sum^{6}_{d=0} y_{d,e} \geq 3 * y_{d,e} $$

Muscle contribution
#

Now, given my targets, I don’t want to train wrists as much as abs, for instance, or I want to keep the legdays to a comfortable 1 time a year week.

Remember the dataset: each exercise has primary and secondary muscle groups.

I set a weight (\(p_{e,m}\)) of \(1\), if the exercise \(e\) is in the group \(m\) as a primary, or \(0.25\) if secondary. \(0\), of course, if not included

Let’s say there are \(M \in \mathbb{N}\) muscle groups

$$ c_m = \sum^{N}_{e=0} \sum^{6}_{d=0} x_{d,e} * p_{e,m} $$

And by doing so, we create a constraint that sets a new variable. And it’s the most important thing to take away from this article. You can build elaborate systems, by using constraints to define variables from the value of other variables.

See auxiliary variables section

Muscle group targets
#

Now that we have our muscle contributions variables (\(c_m\) with \(m\) the muscle group), we can set “targets” as to how much each muscle group should be used. I want abs, not humongous calves.

How I tackled this problem was through another 2 vectors of variables that will represent how above or below the target a solution is.

Let’s define two more variables: \(a_m\) being how much a muscle group has been scheduled above the target, and \(b_m\) being how much a muscle group is below target.

$$ c_m + a_m - b_m = \text{target} $$
tip

This can only work if you declare your variables bounds to be positive integers. If you allow \(a_m\) or \(b_m\) to go negative, then they will loose all the sense created by the \(c_m + a_m - b_m \) form.

Smoothness factor
#

Last but not least, I wanted to smooth out the workouts: no big day with 20+ exercises, but 6 average days.

If we copy the “trick” from the muscle group targets, we can define 4 more variables:

  • \(t_d\): for the total of sets per day
  • \(T\): Grand total: \(\sum^{6}_{d=0}t_d\)
  • \(o_d\): Days above average
  • \(n_d\): Days below average

And add a new constraint

$$ \sum^{6}_{d=0} t_d - T = o_d - n_d $$

Don’t forget bounds for \(o_d\), and \(n_d\), both should be positive integers.

Objective function
#

We now have two sets of constraints. I’ve not found a direct term to name them, I’ll use my own words:

  • Solution constraints: They cut off undesirable regions of solutions
  • Auxiliary constraints: They allow me to create variables based on other variables values

The solution constraints are necessary because some solutions are unacceptable. Empty days, days with 20+ workouts

But we also need to define what the system should find. What to optimize.

Remember what linear functions look like. We need to provide our solver with one function that looks like this. With \(x\) a vector of variables, here, they could be a mix of \(o_d\), \(y_{e_d}\), etc.)

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

Of course, you can expect an actual objective function to be much larger. In a professional setting, I’ve seen systems with hundreds of thousands of variables.

I’ll build my objective function part by part. I’ve chosen to minimize the function, but you can do the inverse. It all depends on your specific problem to solve.

Targets
#

It’s the main objective: hitting targets

We’ll define a penalty for being over/under the target number of sets for a muscle group.

Below:

$$ 10 \sum^{M}_{m=0} b_m $$

Above:

$$ 1 \sum^{M}_{m=0} a_m $$

In my personal opinion, being above is way less undesirable than being below

Compactness
#

Another objective, can be to guide the solution toward a more compact form.

$$ 0.05\sum^{6}_{d=0} \sum^{N}_{e=0} x_{d,e} \\\ 0.01\sum^{6}_{d=0} \sum^{N}_{e=0} y_{d,e} $$
note

I have mixed feelings about this part of the objective function, as it tends to prioritize exercises that hit multiple muscle groups.

Smoothness
#

And finally, we can create a smoothness objective, using the variables defined with constraints

$$ 0.01\sum^{6}_{d=0} o_d + n_d $$

Every day that’s over/under the others will add to the objective function (that must be minimized), thus making it slightly less likely to be the optimal solution

Results
#

Solving took approximately 3 seconds. That’s quite fast.

Day 1
- Front Raise (lyfta_id=133): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Seated Reverse Fly (lyfta_id=223): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 2
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Cable Kneeling Crunch (lyfta_id=1626): 3 sets
- Lever Preacher Curl (lyfta_id=217): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 3
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Seated Reverse Fly (lyfta_id=223): 3 sets
- Seated Shoulder Press (lyfta_id=176): 4 sets
Day 4
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Front Raise (lyfta_id=133): 3 sets
- Lever Preacher Curl (lyfta_id=217): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 5
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Preacher Curl (lyfta_id=217): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 6
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Seated Reverse Fly (lyfta_id=223): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 7
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Leg Extension (lyfta_id=212): 4 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
- Triceps Pushdown (lyfta_id=107): 3 sets
Muscle targets (effective sets):
- Biceps Brachii: 7.2 / 8.0
- Deltoid, Lateral: 5.7 / 10.0
- Hamstrings: 4.5 / 8.0
- Latissimus Dorsi: 5.4 / 10.0
- Pectoralis Major, Sternal: 5.4 / 12.0
- Quadriceps: 4.0 / 10.0
- Rectus Abdominis: 3.0 / 6.0

Auxiliary variables
#

LP is not flexible. Remember that equations must be linear:

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

How would you add constraints such as: two Push day, two Pull day, two Leg day, per week.

You wouldn’t be able, only using constraints, or objective functions.

This problem is solvable in two steps:

  • Make 3 variables, for each day: is it push, is it pull, is it leg. These variables are sums of \(y_{d_e}\) for the day, and for values of \(e\) tied to exercises which a push/pull/legs. We’ll call them \(s_d\), \(e_d\) and \(l_d\)
  • Create 3 new constraints:
    • \(\sum^{6}_{d=0}\ s_d \geq 2\): We want at least two days of push
    • \(\sum^{6}_{d=0}\ v_d \geq 2\): We want at least two days of pull
    • \(\sum^{6}_{d=0}\ l_d \geq 2\): We want at least two days of legs :(
  • We need also a new constraint per day: Each day can either be Push/Pull/Leg, not 2 or 3 at the same time : \(s_d + v_d + l_d = 1\)

This is a direct use of auxiliary variables.

What use for LP ?
#

Throughout online reading, I’ve seen that LP can be used to model a LOT of real world problems:

  • Production optimization
  • Traffic & road planning
  • Route planning

It’s an impressive subject, and there are resources everywhere.

I’ve seen some “AI” products that I suspect to be an LP problem in disguise, or at least, a LLM that could be replaced with an appropriate LP problem.

Tooling
#

For convenience, I’ve used a Jupyter Notebook in a Docker container. I’ve adopted disposable docker contains as part as my workflow for a few weeks now, and the idea behind is to avoid having to reinstall Linux monthly (I use Arch btw.)

The implementation is made using Python 3.13, and PuLP

Methodology
#

As for the methodology, it’s mostly trial and error. No grand plan, no intricate knowledge, just trying things. I’ve also had ChatGPT explain to me some concepts, and suggest constraints.

quote

You can just do things™

Some random on , probably

What’s funny about LP, is that a matrix is able to find flaws in your reasoning. And make you pay (in CPU time) for them

  • You wanted 30 exercises. Here, have a monday with 24 exercises and a tuesday with 6.
  • Yes, you wanted to target legs. Did you know you could just hit 300 sets of leg press on monday ?
A dog, smirking
I got ragebaited by an algorithm

Acknowledgments
#

  • My maths teacher Ms B, at Adimaker Lille, for transmitting her knowledge as much as she could (and transmitting her passion for teaching !)
  • Paul, for proofreading.
  • ChatGPT, for some explanations, and guidance
Raphaël Fleury-le veso
Author
Raphaël Fleury-le veso
24 years old. Low-level software & hardware enthusiast