This document details the reverse-engineered logic from the RP Hypertrophy app's reps and weight recommendation system. The goal is to achieve feature parity in our application.
RIR represents how many reps you could theoretically perform before failure. The RP app uses a progressive overload model where RIR decreases each week:
| Week | Target RIR | Training Intensity |
|---|---|---|
| Week 1 | 2 RIR | Moderate |
| Week 2 | 1 RIR | Hard |
| Week 3 | 0 RIR | Maximum Effort |
| Week 4 | Deload | Recovery |
A typical mesocycle consists of:
- 3 Accumulation Weeks: Progressive overload with decreasing RIR
- 1 Deload Week: Reduced volume and intensity for recovery
function getTargetRIR(weekNumber: number, totalAccumulationWeeks: number = 3): number | null {
// Deload weeks return null (no RIR target)
if (weekNumber > totalAccumulationWeeks) {
return null;
}
// RIR decreases by 1 each week
// Week 1 2 RIR, Week 2 1 RIR, Week 3 0 RIR
return totalAccumulationWeeks - weekNumber;
}
```
### B. Recommended Reps Calculation
The recommended reps for the current week are calculated based on the previous week's performance:
```typescript
interface SetHistory {
weight: number;
reps: number;
rir: number;
weekNumber: number;
setNumber: number;
}
function getRecommendedReps(
previousSetData: SetHistory | null,
currentTargetRIR: number
): number | null {
if (!previousSetData) {
return null; // No recommendation without history
}
// Calculate RIR difference
const rirDecrease = previousSetData.rir - currentTargetRIR;
// Each 1 RIR decrease allows approximately 1 more rep
return previousSetData.reps + rirDecrease;
}
```
**Example Progression:**
- Week 1, Set 1: 25 reps @ 2 RIR (baseline)
- Week 2, Set 1: 26 reps @ 1 RIR (25 + 1)
- Week 3, Set 1: 27 reps @ 0 RIR (26 + 1)
### C. Weight Range Calculation
Weight recommendations use a percentage-based range centered on the previous week's weight:
```typescript
interface WeightRange {
min: number;
max: number;
}
function getWeightRange(
previousWeight: number,
equipmentType: 'dumbbell' | 'barbell' | 'bodyweight' | 'machine'
): WeightRange | null {
if (!previousWeight || previousWeight <= 0) {
return null;
}
// Base variance of 12.5% from previous weight
const variance = 0.125;
const rawMin = previousWeight * (1 - variance);
const rawMax = previousWeight * (1 + variance);
// Round to equipment-appropriate increments
const increment = getEquipmentIncrement(equipmentType);
return {
min: roundToIncrement(rawMin, increment),
max: roundToIncrement(rawMax, increment)
};
}
function getEquipmentIncrement(equipmentType: string): number {
switch (equipmentType) {
case 'dumbbell':
return 2.5; // 2.5 lb increments
case 'barbell':
return 5; // 5 lb increments (plates)
case 'machine':
return 5; // Typically 5 lb increments
default:
return 0.25; // Fine-grained for calculations
}
}
function roundToIncrement(value: number, increment: number): number {
return Math.round(value / increment) * increment;
}
```
**Observed Examples:**
| Previous Weight | Calculated Range | Displayed Range |
|-----------------|------------------|-----------------|
| 30 lbs | 26.25 - 33.75 | 26.25 - 33.75 lbs |
| 40 lbs | 35 - 45 | 35 - 45 lbs |
| 80 lbs | 70 - 90 | 70 - 90 lbs |
| 230 lbs | 201.25 - 258.75 | 201.25 - 258.75 lbs |
---
## Implementation Requirements
### 1. Data Models
```typescript
interface Exercise {
id: string;
name: string;
equipmentType: 'dumbbell' | 'barbell' | 'bodyweight' | 'machine' | 'cable';
muscleGroup: string;
}
interface WorkoutSet {
id: string;
exerciseId: string;
setNumber: number;
weight: number | null;
reps: number | null;
targetRIR: number;
completed: boolean;
loggedAt: Date | null;
}
interface WorkoutDay {
id: string;
mesocycleId: string;
weekNumber: number;
dayNumber: number;
dayName: string;
sets: WorkoutSet[];
}
interface Mesocycle {
id: string;
name: string;
totalWeeks: number;
accumulationWeeks: number;
daysPerWeek: number;
workoutDays: WorkoutDay[];
}
```
### 2. Core Calculator Service
```typescript
class RepWeightCalculator {
/**
* Get recommendations for a specific set in the current workout
*/
getSetRecommendations(
exercise: Exercise,
currentWeek: number,
currentSetNumber: number,
mesocycleHistory: WorkoutDay[]
): SetRecommendation {
const targetRIR = this.getTargetRIR(currentWeek);
const previousSetData = this.getPreviousWeekSetData(
exercise.id,
currentWeek,
currentSetNumber,
mesocycleHistory
);
return {
targetRIR,
recommendedReps: this.getRecommendedReps(previousSetData, targetRIR),
weightRange: this.getWeightRange(previousSetData, exercise.equipmentType),
hasHistory: previousSetData !== null
};
}
/**
* Find corresponding set from previous week
*/
private getPreviousWeekSetData(
exerciseId: string,
currentWeek: number,
setNumber: number,
history: WorkoutDay[]
): SetHistory | null {
const previousWeek = currentWeek - 1;
if (previousWeek < 1) return null;
const previousWorkout = history.find(
day => day.weekNumber === previousWeek
);
if (!previousWorkout) return null;
const matchingSet = previousWorkout.sets.find(
set => set.exerciseId === exerciseId &&
set.setNumber === setNumber &&
set.completed
);
if (!matchingSet || !matchingSet.weight || !matchingSet.reps) {
return null;
}
return {
weight: matchingSet.weight,
reps: matchingSet.reps,
rir: matchingSet.targetRIR,
weekNumber: previousWeek,
setNumber: setNumber
};
}
}
```
### 3. Exercise Change Handler
When the user changes an exercise, we need to recalculate recommendations:
```typescript
function onExerciseChange(
newExerciseId: string,
setNumber: number,
currentWeek: number
): void {
const exercise = getExerciseById(newExerciseId);
const history = getExerciseHistory(newExerciseId);
// Look for the most recent data for this exercise across all mesocycles
const previousData = findMostRecentSetData(history, setNumber);
if (!previousData) {
// No history - show placeholder with no recommendation
updateUI({
weightPlaceholder: 'lbs',
repsPlaceholder: getDefaultRepsForExercise(exercise),
weightRecommendation: 'No weight recommendation at this time',
repsRecommendation: `We recommend ${getTargetRIR(currentWeek)} RIR`
});
return;
}
// Calculate and display recommendations
const recommendations = calculator.getSetRecommendations(
exercise,
currentWeek,
setNumber,
history
);
updateUI({
weightPlaceholder: String(previousData.weight),
repsPlaceholder: String(recommendations.recommendedReps),
weightRecommendation: formatWeightRange(recommendations.weightRange),
repsRecommendation: formatRepsRecommendation(recommendations)
});
}
```
### 4. UI Display Formatting
```typescript
function formatWeightRange(range: WeightRange | null): string {
if (!range) {
return 'No weight recommendation at this time';
}
if (range.min === range.max) {
return `We recommend ${range.min} lbs`;
}
return `We recommend ${range.min} - ${range.max} lbs`;
}
function formatRepsRecommendation(rec: SetRecommendation): string {
if (rec.recommendedReps !== null) {
return `We recommend ${rec.recommendedReps} reps or ${rec.targetRIR} RIR`;
}
return `We recommend ${rec.targetRIR} RIR`;
}
```
---
## Special Cases to Handle
### 1. Bodyweight Exercises
```typescript
function handleBodyweightExercise(
exercise: Exercise,
userBodyweight: number
): WeightRecommendation {
// Weight is fixed at user's bodyweight
return {
weight: userBodyweight,
isEditable: false,
displayText: `bodyweight @ ${userBodyweight} lbs`
};
}
```
### 2. First Week (No History)
- Display "No weight recommendation at this time"
- Show only RIR target: "We recommend 2 RIR"
- Rep placeholder should use exercise-appropriate defaults
### 3. Deload Weeks
```typescript
function getDeloadRecommendations(
lastAccumulationWeekData: SetHistory
): SetRecommendation {
return {
targetRIR: null, // No RIR target during deload
recommendedReps: Math.round(lastAccumulationWeekData.reps * 0.6),
weightRange: {
min: lastAccumulationWeekData.weight * 0.5,
max: lastAccumulationWeekData.weight * 0.7
},
isDeload: true
};
}
```
### 4. Multi-Set Exercises
Each set should have independent recommendations based on the corresponding set from the previous week:
- Set 1 Week 3 based on Set 1 Week 2
- Set 2 Week 3 based on Set 2 Week 2
- etc.
---
## Implementation Checklist
- [ ] Implement RIR calculation based on week number
- [ ] Create set history lookup function
- [ ] Implement rep progression calculation (+1 rep per RIR decrease)
- [ ] Implement weight range calculation (12.5%)
- [ ] Add equipment-specific rounding increments
- [ ] Handle bodyweight exercises (fixed weight)
- [ ] Handle first week (no history) case
- [ ] Handle exercise substitution/change
- [ ] Implement deload week calculations
- [ ] Add UI formatting helpers
- [ ] Store and retrieve exercise history across mesocycles
- [ ] Display placeholder text in input fields
- [ ] Show recommendation tooltips/text below inputs
---
## Testing Scenarios
1. **Week 1 baseline**: Should show only RIR recommendation, no weight range
2. **Week 2 progression**: Should show +1 rep from Week 1 with weight range
3. **Week 3 to failure**: Should show +1 rep from Week 2, 0 RIR target
4. **Exercise change**: Should pull history from previous mesocycles
5. **Bodyweight exercise**: Should lock weight to user's bodyweight
6. **Deload week**: Should show reduced volume/intensity recommendations
7. **Multi-set consistency**: Each set should track its own progression
---
## References
- RP Strength Hypertrophy App (training.rpstrength.com)
- Renaissance Periodization methodology
- Epley Formula for rep-max estimation: `1RM = weight (1 + reps/30)`