Optimizing sets and reps for strength training

Sets and repetitions are essential building blocks in all strength training programs. While there is no obvious “best” repetition scheme, advanced athletes naturally want low-level building blocks to support high-level, long-term periodization schemes where volume and intensity is strategically manipulated.

In this article I present a novel idea: formulating the creation of repetition schemes as a combinatorial optimization problem. A tree search algorithm with pruning solves the problem in milliseconds.

Table of contents

Introduction

In strength training, weights are lifted in sets. Each set is composed of repetitions. For instance, a repetition scheme \(s\) such as \(s = [6, 5, 4, 4]\) means that an athlete performs six repetitions, rests, performs five repetitions, rests, four repetitions, and so forth. The repetition scheme \([6, 5, 4, 4]\) consists of \(4\) sets in total and \(19\) repetitions in total.

What weight should one put on the bar for e.g. 6 repetitions? This depends on the intensity-mapping \(f: \mathbb{Z}_+ \to \mathbb{R}_+\), which maps from repetitions to intensity. By definition \(f(1)\) equals an intensity of \(100 \%\), i.e., the one-rep-max. The exact nature of the mapping \(f\) depends on the specific lift and athlete, but \(6\) repetitions will typically map to the range \([82\%, 85 \%]\) of the one-rep-max. It should be obvious that \(f\) is monotonically decreasing.

In the remainder of this article, we’ll use this intensity-mapping \(f\):

 Repetitions  1    2  3  4 5    6
Intensity     100 95 90 85 80 75

The numbers above are chosen for expositional convenience, not realism.

We denote the number of reps in set \(j\) by \(r_j\) in the repetition scheme. In other words, we denote a repetition scheme as \(s = [r_1, r_2, \ldots, r_j, \ldots]\). By definition we assume repetition schemes are non-increasing, i.e. we take it to be true that \(r_j \geq r_{j+1}\) for every set \(j\) in the scheme. We define the total repetitions of a repetition scheme as \(R(s) := \sum_{r_j \in s} r_j\). We also define the average intensity \(I(s)\) of a rep scheme \(s\) as

\begin{align*} I(s) := \frac{\sum_{r_j \in s} r_j  f(r_j) }{\sum_{r_j \in s} r_j} = \frac{\sum_{r_j \in s} r_j  f(r_j) }{R(s)}. \end{align*}

For instance, giving the mapping \(f\) above and the repetition scheme \(s = [6, 5, 4, 4]\), the average intensity is:

\begin{align*} I(s) = \frac{6 \times 75 + 5 \times 80 + 4 \times 85 + 4 \times 85}{6 + 5 + 4 +4} = \frac{1530}{19} \approx 80.53 \end{align*}

Problem statement

Given a desired average intensity \(I^\star\) and desired total repetitions \(R^\star\), what is the optimal repetition scheme? By optimal we mean as close to the vector \((I^\star, R^\star)\) as possible in the Euclidean norm. Given a repetition scheme \(s\) we evaluate its error \(E\), defined as:

\begin{align*} E(s) = \left( I^\star - I(s) \right)^2 + \left( R^\star - R(s) \right)^2 \end{align*}

There are two additional objectives that are not captured in the equation above:

  1. We prefer repetition schemes without large jumps in weight lifted. For instance, \([6, 4]\) is preferable to \([8, 2]\) since \([8, 2]\) involves a large jump in weight for the athlete.
  2. We prefer repetition schemes without too many unique repetitions, so \([6, 5, 4]\) is better than \([5, 4, 3, 2, 1]\). The latter scheme involves moving too many plates back and forth.

Solution approaches

Over the years I have solved this problem in three different ways:

Approach A: Generate random schemes and evaluate them. This an obvious first attempt. We can formulate this as a multi-objective optimization problem with both the objective \(E\) and objectives (1) and (2) above. Since we want a single best scheme we can scalarize the objective (i.e., compute a weighted sum) and return the best solution. While generating random repetition schemes and evaluating them has no guarantee of finding the best solution, the method produces respectable solutions in general. The downside is the lack of guaranteed optimality, the computational complexity, the worst-case scenario and the general computational waste.

Approach B: Solve the problem as a Mixed Integer Program (MIP). The function \(E\) and the objectives (1) and (2) above can both be linearized. A general purpose MIP solver can then produce an optimal solution. The drawback is that the linearization is non-trivial, and the resulting code for the MIP is somewhat convoluted. Changing the objectives can be cumbersome, since re-linearization might be warranted. The code becomes hard to read, and it takes some time to initialize the MIP solver and parse the problem data. Still, we do get an optimal solution.

Approach C: Generate all feasible schemes by pruned graph search and evaluate every scheme. This is the solution approach I ended up using. It is guaranteed to find the best solution, does not require a MIP solver and runs quickly. It’s also easy to change the objective function, which is not required to be linear or even convex.

The rest of this article explains this approach in detail.

Generating all feasible repetition schemes

Let \(\mathcal{S}\) be the space of all repetition schemes. Given that we are only allowed to choose repetitions from \(\{2, 3, 4\}\), how many repetition schemes \(s \in \mathcal{S}\) have \(6\) repetitions in total? The answer is \([2, 2, 2]\), \([4, 2]\) and \([3, 3]\).

Formally we denote this set of repetition schemes as:

\begin{align*} \{s \in \mathcal{S} \mid 2 \leq r_j \leq 5 \, \forall \, r_j \in s \wedge R(s) = 6\} \end{align*}

The graph below shows how to generate the repetition schemes using tree search. We initialize the search with the trivial scheme \([]\) and progress in a depth-first fashion by repeatedly adding repetitions. Notice that the branching rule ensures that no decreasing sequence is produced.

                        --------------------- [] ----------------------
                        |                      |                      |
                        v                      v                      v
              -------- [2] --------           [3] --------           [4]
              |         |         |            |         |            |
              v         v         v            v         v            v
     ------- [2, 2]    [2, 3]    [2, 4]       [3, 3]--  [3, 4]       [4, 4]
     |        |  |       | |         |         |     |   |            |
     v        |  |       | |         |         v     |   v            v
 [2, 2, 2]    v  |       | |         |     [3, 3, 3] |  [3, 4, 4]    [4, 4, 4]
 | | | [2, 2, 3] v       | |         |      | |      |         |      |
 | | |   | | [2, 2, 4]   | |         |      | |      v         |      |
 | | |   | |        |    | |         |      | |  [3, 3, 4]     |      |
 | | |   | |        |    | |         |      | |         |      |      |
 | | |   | |        |    | |         |      | |         |      |      |

A Python 3.7 code draft is shown below. Cleaner and more well-documented code is found in the streprogen source code.

def repetition_schemes(reps: list, total_reps: int, slack: int, max_diff: int = 99,
                       max_unique: int = 99, _i=0, _scheme=None):
    """Yield schemes that can be created using the repetitions given in `reps`, 
    that are: (1) within `slack` repetitions of the `total_reps`, (2) have no two 
    consecutive repetitions with difference larger than `max_diff` and (3) no more 
    than `max_unique` unique repetitions."""
    reps = sorted(set(reps))
    if _scheme is None:
        _scheme = []

    # Stop the recursion on conditions (pruning).
    too_many_unique = len(set(_scheme)) > max_unique
    too_many_total_reps = sum(_scheme) > total_reps + slack
    if too_many_unique or too_many_total_reps:
        return

    # Yield the result if it's within the allowed slack.
    if _scheme and sum(_scheme) >= total_reps - slack:
        yield _scheme[::-1]  # Reverse for output

    # Recursion step (only non-increasing schemes)
    for j in range(_i, len(reps)):
        if not _scheme or reps[j] - _scheme[-1] <= max_diff:
            yield from repetition_schemes(reps, total_reps, slack, max_diff, 
                                          max_unique, j, _scheme + [reps[j]])

Below are some examples showing input and outputs.

>>> list(repetition_schemes([2, 3, 4], 6, 0)) # Schemes with R(s) = 6 reps
[[2, 2, 2], [4, 2], [3, 3]]

>>> list(repetition_schemes([2, 3, 4], 6, 1)) # 5 <= R(s) <= 7
[[2, 2, 2], [3, 2, 2], [3, 2], [4, 2], [3, 3], [4, 3]]

>>> list(repetition_schemes([2, 3, 4], 6, 2))
[[2, 2], [2, 2, 2], [2, 2, 2, 2], [3, 2, 2, 2], [3, 2, 2], [4, 2, 2], ...]

Evaluating repetition schemes

Consider a typical example. An athlete wants to do \(22\) repetitions at \(82\%\) intensity. These are soft constraints, and the objective is to find a scheme as close as possible, by minimizing the Euclidean norm. He wants to choose repetitions from \(\{3, 4, 5, 6\}\). Furthermore, he wants no more than \(3\) unique repetitions and a difference of no more than \(2\) repetitions per set. These are hard constraints.

The athlete defines the intensity mapping \(f: \mathbb{Z}_+ \to \mathbb{R}_+\) as well as the intensity function \(I: \mathcal{S} \to \mathbb{R}_+\) and the repetition function \(R: \mathcal{S} \to \mathbb{Z}_+\). Then he calls the generator function and chooses the \(s \in \mathcal{S}\) that minimizes \(E: \mathcal{S} \to \mathbb{R}_+\).

def f(reps: int) -> float:
    """Intensity_mapping."""
    return list(range(100, 60, -5))[reps - 1]


def intensity(scheme, f) -> float:
    """Evaluate I(s), given s and f"""
    R = sum(scheme)
    return sum(r_j * f(r_j) for r_j in scheme) / R


def E(scheme, f, R_star: int, I_star: float) -> float:
    """2-norm of error."""
    R = sum(scheme)
    I = intensity(scheme, f)
    return (R_star - R) ** 2 + (I_star - I) ** 2


schemes = repetition_schemes(reps=[3, 4, 5, 6], 
                             total_reps=22, 
                             slack=10, 
                             max_diff=2, 
                             max_unique=3)

import functools

objective = functools.partial(E, f=f, R_star=22, I_star=82)
print(min(schemes, key=objective))  # [5, 5, 5, 4, 3]

The generator yields \(265\) schemes that satisfy the hard constraints. The solution is the repetition scheme \(s^\star\) that minimizes the objective function \(E\). In the example above, the solution is \(s^\star = [5, 5, 5, 4, 3]\), which has \(I(s^\star) \approx 82.3\) and \(R(s^\star) = 22\). Since the hard constraints are used to prune the graph search, the problem is solved in a few milliseconds.

I chose to model the problem with hard constraints for two reasons: (1) I see no loss in modeling with hard constraints because the space \(\mathcal{S}\) is so rich that excluding parts of it entirely does not hurt in practice, and (2) it allows me to prune the graph search. It’s possible to relax the hard constraints and bring them into \(E\) instead, but trading off between many goals will make \(E\) more complex and unwieldy. In a typical real-world problem, hundreds of repetition schemes are evaluated. This far surpasses what a human could do in reasonable time.  

Summary

Sets and repetitions are essential building blocks in all strength training programs. We have formulated the creation of repetition schemes as a combinatorial optimization problem. A good repetition scheme is (1) close to the desired intensity, (2) close to the desired repetitions (volume), (3) does not have too many unique repetitions and (4) does not have large jumps in weights lifted. The solution approach is to constrain the full space of all repetition schemes. A feasible sub-space is obtained by using (3) and (4) as hard constraints. The algorithm returns the scheme in the feasible set that minimizes the Euclidean norm of (1) and (2).

This is an excellent algorithm that solves a real-world problem for me for all practical purposes. I use it in all my strength programs, and you should too.

References

  • The first chapters of “Artificial Intelligence: A Modern Approach” by Norvig et al. contains a wonderful exposition of intelligent graph search algorithms.