Last active
October 1, 2025 20:20
-
-
Save mjkpolo/b1b9d1c7606cffa85fc77bd7a1af2499 to your computer and use it in GitHub Desktop.
Generate workouts based on your one rep max for [5/3/1](https://t-nation.com/t/5-3-1-how-to-build-pure-strength/281694)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import fire | |
| import pickle | |
| import os | |
| from sys import stderr, stdout | |
| from dataclasses import dataclass | |
| import datetime | |
| from typing import Optional, TextIO | |
| from decimal import Decimal | |
| # https://t-nation.com/t/5-3-1-how-to-build-pure-strength/281694 | |
| main_rest_time = datetime.timedelta(minutes=3, seconds=30) | |
| ass_rest_time = datetime.timedelta(minutes=1, seconds=30) | |
| main_lift_time = datetime.timedelta(minutes=1, seconds=0) | |
| ass_lift_time = datetime.timedelta(minutes=1, seconds=0) | |
| main_sets = 3 | |
| perc_of_orm = { | |
| 0: [.65, .75, .85], | |
| 1: [.70, .80, .90], | |
| 2: [.75, .85, .95], | |
| 3: [.40, .50, .60], | |
| } | |
| # main changes per *cycle* | |
| main_reps = { | |
| 0: [5, 5, 5], | |
| 1: [3, 3, 3], | |
| 2: [5, 3, 1], | |
| 3: [5, 5, 5], | |
| } | |
| main_lift = { | |
| 0: "Shoulder Press", | |
| 1: "Deadlift", | |
| 2: "Bench Press", | |
| 3: "Squat", | |
| } | |
| # ass changes per *workout* | |
| ass_sets = 5 | |
| ass_reps = { | |
| 0: [15, 10], | |
| 1: [12, 15], | |
| 2: [15, 10], | |
| 3: [15, 10], | |
| } | |
| ass_lift = { | |
| 0: ["Dip", "Chin-Up"], | |
| 1: ["Good Morning", "Hanging Leg Raise"], | |
| 2: ["Dumbell Chest Press", "Dumbell Row"], | |
| 3: ["Leg Press", "Leg Curl"], | |
| } | |
| def fancyprint(msg: str, dim: bool, bold: bool, type: str, color: str, out: TextIO): | |
| styles = '' | |
| if bold: | |
| styles += '\033[1m' | |
| if dim: | |
| styles += '\033[2m' | |
| if type == '': | |
| print( | |
| f'\033[1;{color}m{msg}\033[0m', file=out) | |
| else: | |
| print( | |
| f'\033[1;{color}m{type}\033[0m{styles} {msg}\033[0m', file=out) | |
| def err(msg: str, dim: bool = False, bold: bool = False): | |
| fancyprint(msg, dim, bold, 'ERR: ', "31", stderr) | |
| def warn(msg: str, dim: bool = False, bold: bool = False): | |
| fancyprint(msg, dim, bold, 'WARN:', "33", stderr) | |
| def info(msg: str, dim: bool = False, bold: bool = False): | |
| fancyprint(msg, dim, bold, 'INFO:', "34", stderr) | |
| def emph(type: str, msg: str, dim: bool = False, bold: bool = False): | |
| fancyprint(msg, dim, bold, type, "33", stdout) | |
| def norm(msg: str, dim: bool = False, bold: bool = False): | |
| fancyprint(msg, dim, bold, '', "", stdout) | |
| @dataclass | |
| class Progress: | |
| name: str | |
| start_date: datetime.date | |
| skip_days: int | |
| squat_orm: int | |
| bench_orm: int | |
| deadlift_orm: int | |
| ohp_orm: int | |
| def __str__(self): | |
| return ( | |
| f'Name: {self.name}\n' | |
| f'Start Date: {self.start_date}\n' | |
| f'Skip Days: {self.skip_days}' | |
| f'Squat One Rep Max: {self.squat_orm}\n' | |
| f'Bench One Rep Max: {self.bench_orm}\n' | |
| f'Deadlift One Rep Max: {self.deadlift_orm}\n' | |
| f'Overhead Press One Rep Max: {self.ohp_orm}' | |
| ) | |
| def weight_to_plate(weight: int, deadlift: bool): | |
| assert weight > 45, "Too small" | |
| assert weight % 5 == 0, "Needs to be multiple of 5" | |
| weight -= 45 # bar | |
| if deadlift: | |
| plates = [110, 90, 70, 50, 20, 10] | |
| else: | |
| plates = [90, 70, 50, 20, 10, 5] | |
| plate_strs = [] | |
| for plate in plates: | |
| n_plate = weight // plate | |
| if n_plate > 0: | |
| weight -= n_plate * plate | |
| plate_strs.append(f"{n_plate}x{Decimal(plate)/2}") | |
| return ', '.join(plate_strs) | |
| def get_workout(progress: Progress): | |
| today = datetime.date.today() | |
| days_since_start = (today - progress.start_date).days - progress.skip_days | |
| assert days_since_start >= 0 | |
| if days_since_start % 2 == 0: | |
| print(days_since_start) | |
| norm("Have a break!") | |
| return | |
| workout = (((days_since_start % 8) + 1) // 2)-1 | |
| assert workout >= 0 and workout <= 3 | |
| cycles_since_start = days_since_start // 8 | |
| cycle = cycles_since_start % 4 | |
| deadlift = False | |
| if main_lift[workout] == "Shoulder Press": | |
| main_orm = progress.ohp_orm | |
| elif main_lift[workout] == "Deadlift": | |
| main_orm = progress.deadlift_orm | |
| deadlift = True | |
| elif main_lift[workout] == "Bench Press": | |
| main_orm = progress.bench_orm | |
| elif main_lift[workout] == "Squat": | |
| main_orm = progress.squat_orm | |
| else: | |
| raise ValueError(f"Unkown main lift: {main_lift[workout]}") | |
| norm(f"Cycle: {cycle+1}") | |
| norm(f"Workout: {workout+1}") | |
| total_time = datetime.timedelta() | |
| for s in range(main_sets): | |
| set_weight = int(5 * round(perc_of_orm[cycle][s]*main_orm*.9 / 5)) | |
| weight_to_plate(set_weight, False) | |
| emph(main_lift[workout], f"| {main_reps[cycle][s]}{'+ reps' if s == main_sets-1 and cycle != 3 else ' reps '} | {set_weight:3d} lbs. | {main_rest_time} rest | Plates: {weight_to_plate(set_weight, deadlift)}") | |
| total_time += main_rest_time | |
| total_time += main_lift_time | |
| for ass_idx in range(2): | |
| for s in range(ass_sets): | |
| emph(ass_lift[workout][ass_idx], | |
| f"| {ass_reps[workout][ass_idx]} reps | {ass_rest_time} rest") | |
| total_time += ass_rest_time | |
| total_time += ass_lift_time | |
| norm(f"Est. time: {total_time}") | |
| def main( | |
| progress_pickle: str, | |
| show_progress: bool = False, | |
| name: Optional[str] = None, | |
| start_date: Optional[str] = None, | |
| squat_orm: Optional[int] = None, | |
| bench_orm: Optional[int] = None, | |
| deadlift_orm: Optional[int] = None, | |
| ohp_orm: Optional[int] = None, | |
| skip_days: Optional[int] = None, | |
| ): | |
| save_pickle = False | |
| if os.path.exists(progress_pickle): | |
| info("Loading progress pickle...") | |
| with open(progress_pickle, 'rb') as fp: | |
| progress = pickle.load(fp) | |
| assert isinstance(progress, Progress), "Pickle wasn't Progress class" | |
| if show_progress: | |
| print(progress) | |
| return | |
| if squat_orm is not None: | |
| info( | |
| f"Updating Squat one rep max {progress.squat_orm}->{squat_orm}") | |
| progress.squat_orm = squat_orm | |
| save_pickle = True | |
| if bench_orm is not None: | |
| info( | |
| f"Updating Bench one rep max {progress.bench_orm}->{bench_orm}") | |
| progress.bench_orm = bench_orm | |
| save_pickle = True | |
| if deadlift_orm is not None: | |
| info( | |
| f"Updating Deadlift one rep max {progress.deadlift_orm}->{deadlift_orm}") | |
| progress.deadlift_orm = deadlift_orm | |
| save_pickle = True | |
| if ohp_orm is not None: | |
| info( | |
| f"Updating Overhead Press one rep max {progress.ohp_orm}->{ohp_orm}") | |
| progress.ohp_orm = ohp_orm | |
| save_pickle = True | |
| if skip_days is not None: | |
| info( | |
| f"Updating skip days {progress.skip_days}->{skip_days}") | |
| progress.skip_days = skip_days | |
| save_pickle = True | |
| else: | |
| info("Creating progress pickle...") | |
| assert name is not None | |
| assert start_date is not None | |
| assert squat_orm is not None | |
| assert bench_orm is not None | |
| assert deadlift_orm is not None | |
| assert ohp_orm is not None | |
| progress = Progress( | |
| name, | |
| datetime.date.fromisoformat(start_date), | |
| squat_orm, | |
| bench_orm, | |
| deadlift_orm, | |
| ohp_orm, | |
| 0 if skip_days is None else skip_days, | |
| ) | |
| save_pickle = True | |
| if save_pickle: | |
| info("Saving pickle...") | |
| with open(progress_pickle, 'wb') as fp: | |
| pickle.dump(progress, fp) | |
| get_workout(progress) | |
| if __name__ == "__main__": | |
| fire.Fire(main) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment