Training Plan Progressions


In [19]:
from datetime import date, timedelta
import types
from collections import namedtuple

Helper Functions


In [20]:
def determine_next_weekday(now, weekday):
    '''
    datetime, int -> datetime
    '''
    days_ahead = weekday - now.weekday() + 7
    return now + timedelta(days_ahead)

In [21]:
def plan_range(start_date, num_weeks, start_week, step):
    '''
    Generator function to return week type, i.e. progression, rest or race.
    '''
    for wk in range(start_week, num_weeks, step):
        date = start_date + timedelta(weeks=wk)
        if wk == (num_weeks - 1):
            week_type = 'race'
        elif rest_week(wk, num_weeks):
            week_type = 'rest'
        else:
            week_type = 'prog'
        yield (wk, date, week_type)

In [22]:
def week_type(week, length):
    if wk == (length - 1):
        return 'race'
    elif rest_week(week, length):
        return 'rest'
    else:
        return 'prog'

In [23]:
def rest_week(week, plan_length):
    '''
    int -> boolean

    Determine if current week is a rest week.

    Plans work on a 4 week block, with every 4th week being an easier week.
    Runner has at least 2 weeks, and a maximum of 5 weeks before they get an
    easier week.  So if they were on a 6 week plan they would only have an
    easier week on race week.

    Returns True if rest week and False if progression week.
    '''
    build_up = plan_length % 4
    if week <= build_up and build_up < 3:
        return False
    elif (week - build_up + 1) % 4 == 0:
        return True
    else:
        return False

In [24]:
def rest_pc_or_abs(week_cut, dur):
    '''
    Return rest week duration based on whether rest duration reduction is applied as an absolute
    or % value
    '''
    if isinstance(week_cut, str):
        return (float(week_cut.strip('%')) / 100) * dur
    else:
        return dur - week_cut

Base Classes


In [25]:
class Exercise:

    def __init__(self, description, duration):
        self.description = description
        self.duration = duration
    
    def __repr__(self):
        '''
        Return a more human-readable representation
        '''
        return '{0}({1})'.format(self.description, self.duration)

    @staticmethod
    def mins_to_seconds_formatter(dur_in_mins):
        '''
        Return duration nicely formatted minutes based on value in seconds
        '''
        return "{}s".format(int(dur_in_mins * 60))

In [26]:
class WorkoutSet:

    def __init__(self, reps):
        self.reps = reps
        self.exercises = []
        self._duration = 0

    def __repr__(self):
        '''
        Return a more human-readable representation
        '''
        ex = ', '.join(str(exercise) for exercise in self.exercises)
        return '{0}x ({1})'.format(self.reps, ex)

    @property
    def duration(self):
        return self.reps * self._duration

    def add_exercise(self, exercise):
        self.exercises.append(exercise)
        self._duration += exercise.duration

In [85]:
class Workout:
    '''
    Represents a workout session
    '''

    formatting_dict = {
        'Event Day': {'color': '#001F3F',
                      'textColor': 'hsla(210, 100%, 75%, 1.0)'},
        'RunEasy': {'color': '#2ECC40',
                    'textColor': 'hsla(127, 63%, 15%, 1.0)'},
        'Intervals': {'color': '#FF4136',
                      'textColor': 'hsla(3, 100%, 25%, 1.0)'},
        'Hillsprint': {'color': '#FFDC00',
                       'textColor': 'hsla(52, 100%, 20%, 1.0)'},
        'Tempo': {'color': '#0074D9',
                  'textColor': 'hsla(208, 100%, 85%, 1.0)'}
    }

    def __init__(self, date, title):
        self.date = date
        self.title = title
        self.duration = 0
        self.workoutsets = []

    def __repr__(self):
        '''
        Return a more human-readable representation
        '''
        return '{0} - {1}'.format(self.date.strftime('%d %b %Y'),
                                  self.title)

    def __str__(self):
        '''
        Return a more human-readable representation
        '''
        # TODO: add if for EventDay
        ws = '\n'.join('{0}x {1}'.format(workoutset.reps,
                                         workoutset.exercises) for workoutset in self.workoutsets)
        return '{0}\n{1}'.format(self.title, ws)

    @property
    def color(self):
        return self.formatting_dict[self.title]['color']

    @property
    def textColor(self):
        return self.formatting_dict[self.title]['textColor']

    def add_workoutset(self, workoutset):
        self.workoutsets.append(workoutset)
        self.duration += workoutset.duration

Progressions


In [86]:
RuneasySettings = namedtuple('RuneasySettings', 'init_dur prog_freq rest race max_dur')
IntervalSettings = namedtuple('IntervalSettings', 'init_dur prog_freq rest race max_dur')

In [87]:
class Progression:
    
    def __init__(self, start_date, length, progressions):
        self.start_date = start_date
        self.length = length
        self.sessions = []
        
        progress_dict = {
            "runeasy": self.runeasy,
            "interval": self.interval
        }
        
        start = 0
        step = len(progressions)
        
        for progression in progressions:
            self.sessions += [wk for wk in progress_dict[progression[0]](start, step, progression[1])]     
            start += 1  
        
        
    def runeasy(self, start, step, settings):
        '''
        '''

        wk = start

        dur = settings.init_dur

        while wk < self.length:
            if week_type(wk, self.length) == 'prog':
                if (wk + 1) % settings.prog_freq == 0 and dur < settings.max_dur:
                    dur += 5
                wk_dur = dur
            elif week_type(wk, self.length) == 'rest':
                wk_dur = rest_pc_or_abs(settings.rest, dur)
            else:
                wk_dur = rest_pc_or_abs(settings.race, dur)

            # Build workout
            date = self.start_date + timedelta(weeks=wk)
            w = Workout(date, 'RunEasy')
            ws = WorkoutSet(1)
            e = Exercise('Easy', wk_dur)
            ws.add_exercise(e)
            w.add_workoutset(ws)           

            yield w

            wk += step
    
    def interval(self, start, step, settings):
        '''
        '''

        wk = start

        dur = settings.init_dur

        while wk < self.length:
            if week_type(wk, self.length) == 'prog':
                if (wk + 1) % settings.prog_freq == 0 and dur < settings.max_dur:
                    dur += 5
                wk_dur = dur
            elif week_type(wk, self.length) == 'rest':
                wk_dur = rest_pc_or_abs(settings.rest, dur)
            else:
                wk_dur = rest_pc_or_abs(settings.race, dur)

            # Build workout
            date = self.start_date + timedelta(weeks=wk)
            w = Workout(date, 'Interval')
            ws = WorkoutSet(1)
            e = Exercise('Easy', wk_dur)
            ws.add_exercise(e)
            w.add_workoutset(ws)           

            yield w

            wk += step

Runeasy

Interval


In [88]:
class Plan:
    '''
    Represents running training plan for prescribed event and level.
    '''
    
    beginner = [
            [("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
            [("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
             ("interval", RuneasySettings(25, 1, 5, 5, 35))],
            [("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
        ]

    def __init__(self, start_date, event_date, event_title, level):
        self.start_date = start_date
        self._event_date = event_date
        self.event_title = event_title
        self.level = level

        # Populate schedule with event
        self.schedule = [Workout(self._event_date, 'Event Day')]

    @property
    def length(self):
        '''
        Length of the training plan in weeks
        '''
        return self.weeks_between_dates(self.start_date, self._event_date)

    def create(self, days):
        '''
        Creates schedule based on ability level and training days
        '''

        def builder_dict(level, days):
            level_dict = {
                'Beginner': self.beginner_plan,
                'Intermediate': self.intermediate_plan,
                'Advanced': self.advanced_plan
            }.get(level, None)
            return level_dict(days)

        self.schedule += builder_dict(self.level, days)

    def __repr__(self):
        '''
        Return a more human-readable representation
        '''

        return "{0} week {1} Plan for the {2}".format(self.length,
                                                      self.level,
                                                      self.event_title)

    @property
    def event_date(self):
        '''
        Event date property, formatted as a string
        '''
        return self._event_date.strftime('%d %b %Y')

    @staticmethod
    def weeks_between_dates(start_date, end_date):
        '''
        Return the number of weeks between two dates
        '''
        return int((determine_next_weekday(end_date, 0) -
                    determine_next_weekday(start_date, 0)).days / 7)

    def beginner_plan(self, days):
        raise NotImplementedError

    def intermediate_plan(self, days):
        raise NotImplementedError

    def advanced_plan(self, days):
        raise NotImplementedError

In [89]:
class Plan5k(Plan):

    def beginner_plan(self, days):
        '''
        Return beginner plan schedule based on number of training days a week.
        '''

        details = [
            [("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
            [("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
             ("interval", RuneasySettings(25, 1, 5, 5, 35))],
            [("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
        ]

        schedule = []

        for day, detail in zip(days, details):
            session_start = determine_next_weekday(self.start_date, day)
            p = Progression(session_start, self.length, detail)
            schedule += p.sessions

        return schedule

    def intermediate_plan(self, days):
        '''
        '''

        pass

    def advanced_plan(self, days):
        pass

In [90]:
start_date = date(2017, 8, 25)
event_date = start_date + timedelta(weeks=8)
p = Plan5k(start_date, event_date, 'RACE DAY', 'Beginner')

days = [0, 2, 4]
p.create(days)

In [91]:
for wo in p.schedule:
    print('{wo.date}:\n{wo}\n'.format(wo=wo))


2017-10-20:
Event Day


2017-08-28:
RunEasy
1x [Easy(10)]

2017-09-04:
RunEasy
1x [Easy(15)]

2017-09-11:
RunEasy
1x [Easy(20)]

2017-09-18:
RunEasy
1x [Easy(15)]

2017-09-25:
RunEasy
1x [Easy(25)]

2017-10-02:
RunEasy
1x [Easy(30)]

2017-10-09:
RunEasy
1x [Easy(35)]

2017-10-16:
RunEasy
1x [Easy(30)]

2017-08-30:
RunEasy
1x [Easy(15)]

2017-09-13:
RunEasy
1x [Easy(20)]

2017-09-27:
RunEasy
1x [Easy(25)]

2017-10-11:
RunEasy
1x [Easy(30)]

2017-09-06:
Interval
1x [Easy(30)]

2017-09-20:
Interval
1x [Easy(25)]

2017-10-04:
Interval
1x [Easy(35)]

2017-10-18:
Interval
1x [Easy(30)]

2017-09-01:
RunEasy
1x [Easy(35)]

2017-09-08:
RunEasy
1x [Easy(35)]

2017-09-15:
RunEasy
1x [Easy(35)]

2017-09-22:
RunEasy
1x [Easy(30)]

2017-09-29:
RunEasy
1x [Easy(35)]

2017-10-06:
RunEasy
1x [Easy(35)]

2017-10-13:
RunEasy
1x [Easy(35)]

2017-10-20:
RunEasy
1x [Easy(30)]


In [92]:
# def gen_1(maxi):
#     i = 0
#     dur = 5
#     while True:
#         yield "Runeasy ({})".format(dur)
#         dur += 5
#         i += 1

# def gen_2(maxi):
#     i = 0
#     dur = 5
#     while True:
#         yield "Intervals ({})".format(dur)
#         dur += 5
#         i += 1

def gen_1(start, step, length):
    i = start
    while i < length:
        yield "Runeasy ({})".format(i)
        i += step

def gen_2(start, step, length):
    i = start
    while i < length:
        yield "Intervals ({})".format(i)
        i += step

In [3]:
from itertools import cycle

In [4]:
length = 10
schedule = []

gens = [gen_1, gen_2]

wk = 0
start = 0
step = len(gens)
for gen in gens:
    schedule += [wk for wk in gen(start, step, length)]     
    start += 1

In [5]:
schedule


Out[5]:
['Runeasy (0)',
 'Runeasy (2)',
 'Runeasy (4)',
 'Runeasy (6)',
 'Runeasy (8)',
 'Intervals (1)',
 'Intervals (3)',
 'Intervals (5)',
 'Intervals (7)',
 'Intervals (9)']

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]: