Ejemplo n.º 1
0
    def max_reps(self):
        """Return max reps. If a Program attribute it set and the exercise
        attribute is None, use the program attribute."""
        if (self.day is not None) and (self.day.program is not None):
            program = self.day.program
        else:
            return self._max_reps

        return prioritized_not_None(self._max_reps, program.max_reps)
Ejemplo n.º 2
0
    def _progress_information(self):
        """Return a tuple (start_weight, final_weight, percent_inc_per_week).

        Can only be inferred in the context of a Program argument.

        """

        if self.day is None:
            raise Exception("Exercise {self.name} must be attached to a Day.")

        if self.day.program is None:
            raise Exception(
                "Day {self.day.name} must be attached to a Program.")

        program = self.day.program

        # Get increase per week
        inc_week = prioritized_not_None(self.percent_inc_per_week,
                                        program.percent_inc_per_week)

        # Case 1: Start weight and final weight is given
        if (self.start_weight is not None) and (self.final_weight is not None):
            start_w, final_w = self.start_weight, self.final_weight
            inc_week = ((final_w / start_w) - 1) / program.duration * 100
            answer = (start_w, final_w, inc_week)

        # Case 2: Start weight and increase is given
        elif (self.start_weight is not None) and (inc_week is not None):
            factor = 1 + (inc_week / 100) * program.duration
            start_w = self.start_weight
            final_w = self.start_weight * factor
            answer = (start_w, final_w, inc_week)

        # Case 3: Final weight and increase is given
        elif (self.final_weight is not None) and (inc_week is not None):
            factor = 1 + (inc_week / 100) * program.duration
            start_w = self.final_weight / factor
            final_w = self.final_weight
            answer = (start_w, final_w, inc_week)

        else:
            raise Exception(f"Exercise {self} is overspecified.")

        rounder = functools.partial(round_to_nearest, nearest=0.01)

        return tuple(map(rounder, answer))
Ejemplo n.º 3
0
    def __init__(
        self,
        name: str = "Untitled",
        duration: int = 8,
        reps_per_exercise: int = 25,
        min_reps: int = 3,
        max_reps: int = 8,
        rep_scaler_func: typing.Callable[[int], float] = None,
        intensity: float = 83,
        intensity_scaler_func: typing.Callable[[int], float] = None,
        units: str = "kg",
        round_to: float = 2.5,
        percent_inc_per_week: float = 1.5,
        progression_func: typing.Callable = None,
        reps_to_intensity_func: typing.Callable[[int], float] = None,
        verbose: bool = False,
    ):
        """Initialize a new program.


        Parameters
        ----------
        name
            The name of the training program, e.g. 'TommyAugust2017'.

        duration
            The duration of the training program in weeks, e.g. 8.

        reps_per_exercise
            The baseline number of repetitions per dynamic exercise.
            Typically a value in the range [15, 30].

        min_reps
            The minimum number of repetitions for the exercises, e.g. 3.
            This value can be set globally for the program, or for a specific
            dynamic exercise. If set at the dynamic exercise level, it will
            override the global program value.

        max_reps
            The maximum number of repetitions for the exercises, e.g. 8.
            This value can be set globally for the program, or for a specific
            dynamic exercise. If set at the dynamic exercise level, it will
            override the global program value.

        rep_scaler_func
            A function mapping from a week in the range [1, `duration`] to a
            scaling value (factor). The scaling value will be multiplied with
            the `reps_per_exercise` parameter for that week. Should typically
            return factors between 0.7 and 1.3.
            Alternatively, a list of length `duration` may be passed.

        intensity
            The baseline intensity for each dynamic exercise. The intensity
            of an exercise for a given week is how heavy the average
            repetition is compared to the expected 1RM (max weight one can
            lift) for that given week. Typically a value around 80.

        intensity_scaler_func
            A function mapping from a week in the range [1, `duration`] to a
            scaling value (factor). The scaling value will be multiplied with
            the `intensity` parameter for that week.
            Should typically return factors between 0.9 and 1.1.
            Alternatively, a list of length `duration` may be passed.

        units
            The units used for exporting and printing the program, e.g. 'kg'.

        round_to
            Round the dynamic exercise to the nearest multiple of this
            parameter. Typically 2.5, 5 or 10.
            This value can be set globally for the program, or for a specific
            dynamic exercise. If set at the dynamic exercise level, it will
            override the global program value.

        percent_inc_per_week
            If `final_weight` is not set, this value will be used. Percentage
            increase per week can be set globally for the program, or for each
            dynamic exercise. If set at the dynamic exercise level, it will
            override the global program value. The increase is  additive, not
            multipliactive. For instance, if the increase is set to
            `percent_inc_per_week=2`, then after 2 weeks the increase is 4,
            not (1.02 * 1.02 - 1) * 100 = 4.04. The `final_weight` parameter
            must be set to `None` for this parameter to have effect.

        progression_func
            The function used to model overall 1RM progression in the
            training program. The function must have a signature like:
                func(week, start_weight, final_weight, start_week, end_week)

        reps_to_intensity_func
            The function used to model the relationship between repetitions
            and intensity. Maps from a repetition to an intensity in the range 0-100.

        verbose
            If True, information will be outputted as the program is created.


        Returns
        -------
        Program
            A Program instance.


        Examples
        -------
        >>> program = Program('My training program')
        >>> program._rendered
        False

        """
        self.name = escape_string(name)

        assert isinstance(duration, numbers.Integral) and duration > 1
        self.duration = duration

        assert isinstance(reps_per_exercise,
                          numbers.Integral) and reps_per_exercise > 0
        self.reps_per_exercise = reps_per_exercise

        assert isinstance(min_reps, numbers.Integral) and min_reps > 0
        assert isinstance(max_reps, numbers.Integral) and max_reps > 0
        self.min_reps = min_reps
        self.max_reps = max_reps
        if self.min_reps and self.max_reps:
            if self.min_reps > self.max_reps:
                raise ValueError("'min_reps' larger than 'max_reps'")

        assert isinstance(intensity, numbers.Number) and intensity > 0
        self.intensity = intensity

        assert isinstance(units, str)
        self.units = units
        self.round_to = round_to
        self.round = functools.partial(round_to_nearest, nearest=round_to)
        self.verbose = verbose

        # ------ REP SCALERS ------
        # Set functions to user supplied, or defaults if None was passed
        user, default = (
            rep_scaler_func,
            functools.partial(self._default_rep_scaler_func,
                              final_week=self.duration),
        )
        rep_scaler_func = prioritized_not_None(user, default)
        if callable(rep_scaler_func):
            self.rep_scalers = [
                rep_scaler_func(w + 1) for w in range(self.duration)
            ]
            self.rep_scaler_func = rep_scaler_func
        else:
            self.rep_scalers = list(rep_scaler_func)
        assert isinstance(self.rep_scalers, list)

        # ------ INTENSITY SCALERS------
        user, default = (
            intensity_scaler_func,
            functools.partial(self._default_intensity_scaler_func,
                              final_week=self.duration),
        )
        intensity_scaler_func = prioritized_not_None(user, default)
        if callable(intensity_scaler_func):
            self.intensity_scalers = [
                intensity_scaler_func(w + 1) for w in range(self.duration)
            ]
            self.intensity_scaler_func = intensity_scaler_func
        else:
            self.intensity_scalers = list(intensity_scaler_func)
        assert isinstance(self.intensity_scalers, list)

        user, default = progression_func, self._default_progression_func
        self.progression_func = prioritized_not_None(user, default)
        assert callable(self.progression_func)

        user, default = reps_to_intensity_func, self._default_reps_to_intensity_func
        self.reps_to_intensity_func = prioritized_not_None(user, default)
        assert callable(self.reps_to_intensity_func)

        # Setup variables that the user has no control over
        self.days = []
        self.active_day = None  # Used for Program.Day context manager API
        self._rendered = False
        self._set_jinja2_enviroment()

        assert isinstance(percent_inc_per_week, numbers.Number)
        self.percent_inc_per_week = percent_inc_per_week

        # TODO: make explicit
        self.optimizer = RepSchemeOptimizer()
Ejemplo n.º 4
0
    def render(self, validate=True):
        """Render the training program to perform the calculations.
        The program can be rendered several times to produce new
        information given the same input parameters.

        Parameters
        ----------
        validate
            Boolean that indicates whether or not to run a validation
            heurestic on the program before rendering. The validation
            will warn the user if inputs seem unreasonable.

        """
        start_time = time.time()

        # Check that exercise names are unique within each day
        for day in self.days:
            seen_names = set()
            for exercise in day.dynamic_exercises + day.static_exercises:
                if exercise.name in seen_names:
                    raise ValueError(
                        f"Exercise name not unique: {exercise.name}")
                else:
                    seen_names.add(exercise.name)

        # --------------------------------
        # Prepare for rendering the dynamic exercises
        # --------------------------------

        # Initialize the structure of the _rendered dictionary
        self._initialize_render_dictionary()

        # Set the day names
        for i, day in enumerate(self.days):
            day.name = prioritized_not_None(day.name, "Day {}".format(i + 1))

        # Validate the program if the user wishes to validate
        if validate:
            self._validate()

        # --------------------------------
        # Render the dynamic exercises
        # --------------------------------

        for (week, day, dyn_ex) in self._yield_week_day_dynamic():

            # Set min and max reps from program, if not set on exercise
            min_reps = dyn_ex.min_reps
            max_reps = dyn_ex.max_reps

            if min_reps > max_reps:
                msg = "'min_reps' larger than 'max_reps' for exercise '{}'."
                raise ValueError(msg.format(dyn_ex.name))

            # Use the local rounding function if available,
            # if not use the global rounding function
            round_func = prioritized_not_None(dyn_ex.round, self.round)

            # The desired repetitions to work up to
            total_reps = prioritized_not_None(dyn_ex.reps,
                                              self.reps_per_exercise)

            # If the index is not valid (due to shifting), use function
            index_to_lookup = week - 1 + dyn_ex.shift
            if 0 <= index_to_lookup < self.duration:
                desired_reps = round(total_reps *
                                     self.rep_scalers[index_to_lookup])
            else:
                if hasattr(self, "rep_scaler_func"):
                    desired_reps = round(
                        total_reps * self.rep_scaler_func(week + dyn_ex.shift))
                else:
                    raise TypeError(
                        "Using `shift` requires `rep_scaler_func` to be a function, not a list."
                    )

            self._rendered[week][day][dyn_ex]["desired_reps"] = int(
                desired_reps)

            # The desired average intensity
            intensity_unscaled = prioritized_not_None(dyn_ex.intensity,
                                                      self.intensity)
            if 0 <= index_to_lookup < self.duration:
                scale_factor = self.intensity_scalers[index_to_lookup]
            else:
                if hasattr(self, "intensity_scaler_func"):
                    scale_factor = self.intensity_scaler_func(week +
                                                              dyn_ex.shift)
                else:
                    raise TypeError(
                        "Using `shift` requires `intensity_scaler_func` to be a function, not a list."
                    )

            desired_intensity = intensity_unscaled * scale_factor
            self._rendered[week][day][dyn_ex][
                "desired_intensity"] = desired_intensity

            # A dictionary is returned with keys 'reps' and 'intensities'
            render_args = dyn_ex, desired_reps, desired_intensity, validate
            out = self._render_dynamic(*render_args)

            # Get increase from program if not available on the exercise
            inc_week = prioritized_not_None(dyn_ex.percent_inc_per_week,
                                            self.percent_inc_per_week)

            # Compute the progress
            (start_w, final_w, inc_week) = dyn_ex._progress_information()

            weight = self.progression_func(week + dyn_ex.shift, start_w,
                                           final_w, 1, self.duration)

            # Test that the weight is not too far from min and max
            upper_threshold = max(start_w, final_w) + abs(start_w - final_w)
            lower_threshold = min(start_w,
                                  final_w) - 2 * abs(start_w - final_w)
            if not (lower_threshold <= weight <= upper_threshold):
                msg = f"\nWARNING: Weight for '{dyn_ex.name}' was {round(weight, 2)} in week {week}. "
                msg += f"This is far from start and final weights. Start weight is {start_w}. "
                msg += f"Final weight is {final_w}."
                warnings.warn(msg)

            # Define a function to prettify the weights
            def pretty_weight(weight, i, round_function):
                weight = round_function(weight * i / 100)
                if weight % 1 == 0:
                    return int(weight)
                return weight

            # Create pretty strings
            tuples_gen = zip(out["intensities"], out["reps"])
            pretty_gen = ((str(r), str(pretty_weight(weight, i, round_func)) +
                           self.units) for (i, r) in tuples_gen)
            out["strings"] = list(
                self.REP_SET_SEP.join(list(k)) for k in pretty_gen)
            out["1RM_this_week"] = round(weight, 2)
            out["weights"] = [
                pretty_weight(weight, i, round_func)
                for i in out["intensities"]
            ]

            # Update with the ['intensities', 'reps', 'strings', ...] keys
            self._rendered[week][day][dyn_ex].update(out)

        if self.verbose:
            delta_time = round(time.time() - start_time, 3)
            print(f"Rendered program in {delta_time} seconds.")