@dgpholdings/greatoak-shared 1.2.16 → 1.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +3 -1
- package/dist/utils/scoring/calculateCalories.d.ts +67 -0
- package/dist/utils/scoring/calculateCalories.js +329 -0
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +66 -0
- package/dist/utils/scoring/calculateMuscleFatiue.js +290 -0
- package/dist/utils/scoring/calculateQualityScore.d.ts +69 -0
- package/dist/utils/scoring/calculateQualityScore.js +333 -0
- package/dist/utils/scoring/constants.d.ts +191 -0
- package/dist/utils/scoring/constants.js +227 -0
- package/dist/utils/scoring/helpers.d.ts +119 -0
- package/dist/utils/scoring/helpers.js +229 -0
- package/dist/utils/scoring/index.d.ts +28 -0
- package/dist/utils/scoring/index.js +44 -0
- package/dist/utils/scoring/parseRecords.d.ts +97 -0
- package/dist/utils/scoring/parseRecords.js +280 -0
- package/dist/utils/scoring/types.d.ts +84 -0
- package/dist/utils/scoring/types.js +11 -0
- package/package.json +1 -1
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.calculateExerciseScore = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
|
|
3
|
+
exports.calculateExerciseScoreV2 = exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.calculateExerciseScore = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
|
|
4
4
|
var number_util_1 = require("./number.util");
|
|
5
5
|
Object.defineProperty(exports, "toNumber", { enumerable: true, get: function () { return number_util_1.toNumber; } });
|
|
6
6
|
var time_util_1 = require("./time.util");
|
|
@@ -26,3 +26,5 @@ Object.defineProperty(exports, "isAnonymousEmail", { enumerable: true, get: func
|
|
|
26
26
|
Object.defineProperty(exports, "isEmail", { enumerable: true, get: function () { return email_utils_1.isEmail; } });
|
|
27
27
|
var noop_utils_1 = require("./noop.utils");
|
|
28
28
|
Object.defineProperty(exports, "NOOP", { enumerable: true, get: function () { return noop_utils_1.NOOP; } });
|
|
29
|
+
var scoring_1 = require("./scoring");
|
|
30
|
+
Object.defineProperty(exports, "calculateExerciseScoreV2", { enumerable: true, get: function () { return scoring_1.calculateExerciseScoreV2; } });
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Pillar 1: Calorie Burn
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Estimates energy expenditure (kcal) for an exercise using the MET system.
|
|
7
|
+
*
|
|
8
|
+
* Formula (per set):
|
|
9
|
+
* calories = effectiveMET × userWeightKg × (activeDurationSecs / 3600)
|
|
10
|
+
*
|
|
11
|
+
* Where effectiveMET is the exercise's baseMET adjusted for:
|
|
12
|
+
* - Effort level (RPE/RIR → scales within metRange)
|
|
13
|
+
* - Weight intensity (for weight-reps: how heavy relative to bodyweight)
|
|
14
|
+
* - Duration category (for duration: short/medium/long multiplier)
|
|
15
|
+
* - Speed (for cardio: interpolated from pace or speed range)
|
|
16
|
+
* - Compound multiplier (multi-joint exercises burn more)
|
|
17
|
+
*
|
|
18
|
+
* After summing all sets:
|
|
19
|
+
* - Rest calories are added (elevated MET during rest periods)
|
|
20
|
+
* - EPOC (afterburn) is added as a percentage of work calories
|
|
21
|
+
*
|
|
22
|
+
* References:
|
|
23
|
+
* - Ainsworth BE et al. "Compendium of Physical Activities" (2011)
|
|
24
|
+
* - Katch, McArdle & Katch, "Exercise Physiology" (8th ed.)
|
|
25
|
+
*/
|
|
26
|
+
import type { IParsedSet, IUserContext } from "./types";
|
|
27
|
+
interface IMetabolicData {
|
|
28
|
+
baseMET: number;
|
|
29
|
+
metRange: [number, number];
|
|
30
|
+
compoundMultiplier: number;
|
|
31
|
+
muscleGroupFactor: number;
|
|
32
|
+
intensityScaling: "linear" | "exponential" | "plateau";
|
|
33
|
+
epocFactor: number;
|
|
34
|
+
weightFactors?: {
|
|
35
|
+
lightWeight: number;
|
|
36
|
+
moderateWeight: number;
|
|
37
|
+
heavyWeight: number;
|
|
38
|
+
};
|
|
39
|
+
durationFactors?: {
|
|
40
|
+
shortDuration: number;
|
|
41
|
+
mediumDuration: number;
|
|
42
|
+
longDuration: number;
|
|
43
|
+
};
|
|
44
|
+
paceFactors?: Record<string, number>;
|
|
45
|
+
lightWeight?: number;
|
|
46
|
+
moderateWeight?: number;
|
|
47
|
+
heavyWeight?: number;
|
|
48
|
+
shortDuration?: number;
|
|
49
|
+
mediumDuration?: number;
|
|
50
|
+
longDuration?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Calculate total calorie burn for an exercise.
|
|
54
|
+
*
|
|
55
|
+
* @param sets Parsed & validated sets (from parseRecords)
|
|
56
|
+
* @param metabolicData Exercise's metabolic configuration
|
|
57
|
+
* @param user Validated user context
|
|
58
|
+
* @param difficultyLevel Exercise difficulty (0–4), used for bodyweight estimation
|
|
59
|
+
* @returns Total calories burned (kcal), rounded to 1 decimal
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const calories = calculateCalories(parsedSets, exercise.metabolicData, userCtx, exercise.difficultyLevel);
|
|
63
|
+
* // → 6.4 (for 3 light bench press sets)
|
|
64
|
+
* // → 142.3 (for a 20-min treadmill run)
|
|
65
|
+
*/
|
|
66
|
+
export declare function calculateCalories(sets: IParsedSet[], metabolicData: IMetabolicData, user: IUserContext, difficultyLevel: number): number;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// calculateCalories.ts
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Pillar 1: Calorie Burn
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Estimates energy expenditure (kcal) for an exercise using the MET system.
|
|
9
|
+
*
|
|
10
|
+
* Formula (per set):
|
|
11
|
+
* calories = effectiveMET × userWeightKg × (activeDurationSecs / 3600)
|
|
12
|
+
*
|
|
13
|
+
* Where effectiveMET is the exercise's baseMET adjusted for:
|
|
14
|
+
* - Effort level (RPE/RIR → scales within metRange)
|
|
15
|
+
* - Weight intensity (for weight-reps: how heavy relative to bodyweight)
|
|
16
|
+
* - Duration category (for duration: short/medium/long multiplier)
|
|
17
|
+
* - Speed (for cardio: interpolated from pace or speed range)
|
|
18
|
+
* - Compound multiplier (multi-joint exercises burn more)
|
|
19
|
+
*
|
|
20
|
+
* After summing all sets:
|
|
21
|
+
* - Rest calories are added (elevated MET during rest periods)
|
|
22
|
+
* - EPOC (afterburn) is added as a percentage of work calories
|
|
23
|
+
*
|
|
24
|
+
* References:
|
|
25
|
+
* - Ainsworth BE et al. "Compendium of Physical Activities" (2011)
|
|
26
|
+
* - Katch, McArdle & Katch, "Exercise Physiology" (8th ed.)
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.calculateCalories = calculateCalories;
|
|
30
|
+
const constants_1 = require("./constants");
|
|
31
|
+
const helpers_1 = require("./helpers");
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Main Calorie Calculation
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* Calculate total calorie burn for an exercise.
|
|
37
|
+
*
|
|
38
|
+
* @param sets Parsed & validated sets (from parseRecords)
|
|
39
|
+
* @param metabolicData Exercise's metabolic configuration
|
|
40
|
+
* @param user Validated user context
|
|
41
|
+
* @param difficultyLevel Exercise difficulty (0–4), used for bodyweight estimation
|
|
42
|
+
* @returns Total calories burned (kcal), rounded to 1 decimal
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const calories = calculateCalories(parsedSets, exercise.metabolicData, userCtx, exercise.difficultyLevel);
|
|
46
|
+
* // → 6.4 (for 3 light bench press sets)
|
|
47
|
+
* // → 142.3 (for a 20-min treadmill run)
|
|
48
|
+
*/
|
|
49
|
+
function calculateCalories(sets, metabolicData, user, difficultyLevel) {
|
|
50
|
+
if (sets.length === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
// Normalize metRange so [0] = min, [1] = max (sample data has them reversed)
|
|
53
|
+
const [metMin, metMax] = normalizeMetRange(metabolicData.metRange, metabolicData.baseMET);
|
|
54
|
+
let totalWorkCalories = 0;
|
|
55
|
+
let totalRestCalories = 0;
|
|
56
|
+
for (const set of sets) {
|
|
57
|
+
// 1. Calculate effective MET for this set
|
|
58
|
+
const effectiveMET = calculateEffectiveMET(set, metabolicData, metMin, metMax, user, difficultyLevel);
|
|
59
|
+
// 2. Work calories: MET × weight × time
|
|
60
|
+
const workHours = set.activeDurationSecs / 3600;
|
|
61
|
+
totalWorkCalories += effectiveMET * user.weightKg * workHours;
|
|
62
|
+
// 3. Rest calories: elevated resting MET during recovery
|
|
63
|
+
if (set.restDurationSecs !== null && set.restDurationSecs > 0) {
|
|
64
|
+
const restHours = set.restDurationSecs / 3600;
|
|
65
|
+
totalRestCalories += constants_1.REST_MET * user.weightKg * restHours;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 4. EPOC (Excess Post-exercise Oxygen Consumption) — afterburn effect
|
|
69
|
+
const epocCalories = totalWorkCalories * metabolicData.epocFactor;
|
|
70
|
+
// 5. Total = work + rest + afterburn
|
|
71
|
+
const total = totalWorkCalories + totalRestCalories + epocCalories;
|
|
72
|
+
return Math.round(total * 10) / 10; // Round to 1 decimal place
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Effective MET Calculation (per set)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/**
|
|
78
|
+
* Calculate the effective MET for a single set based on its type and intensity.
|
|
79
|
+
*
|
|
80
|
+
* The general approach:
|
|
81
|
+
* 1. Interpolate base MET within [metMin, metMax] using effort fraction
|
|
82
|
+
* 2. Apply type-specific intensity multiplier
|
|
83
|
+
* 3. Apply compound multiplier
|
|
84
|
+
*
|
|
85
|
+
* The effort fraction (from RPE/RIR) determines WHERE in the MET range
|
|
86
|
+
* this set falls. Low effort → closer to metMin, high effort → closer to metMax.
|
|
87
|
+
*/
|
|
88
|
+
function calculateEffectiveMET(set, metabolicData, metMin, metMax, user, difficultyLevel) {
|
|
89
|
+
var _a, _b, _c, _d;
|
|
90
|
+
// Start with effort-scaled MET
|
|
91
|
+
// effortFraction is 0.5–1.3 (from helpers), we normalize to 0–1 for interpolation
|
|
92
|
+
const effortNormalized = (0, helpers_1.clamp)((set.effortFraction - 0.5) / 0.8, 0, 1);
|
|
93
|
+
let met = metMin + (metMax - metMin) * effortNormalized;
|
|
94
|
+
// Apply type-specific intensity scaling
|
|
95
|
+
switch (set.type) {
|
|
96
|
+
case "weight-reps":
|
|
97
|
+
met *= getWeightMultiplier((_a = set.kg) !== null && _a !== void 0 ? _a : 0, user.weightKg, metabolicData);
|
|
98
|
+
break;
|
|
99
|
+
case "reps-only":
|
|
100
|
+
met *= getRepsOnlyMultiplier((_b = set.auxWeightKg) !== null && _b !== void 0 ? _b : 0, user.weightKg, difficultyLevel);
|
|
101
|
+
break;
|
|
102
|
+
case "duration":
|
|
103
|
+
met *= getDurationMultiplier((_c = set.durationSecs) !== null && _c !== void 0 ? _c : 0, (_d = set.auxWeightKg) !== null && _d !== void 0 ? _d : 0, user.weightKg, metabolicData);
|
|
104
|
+
break;
|
|
105
|
+
case "cardio-machine":
|
|
106
|
+
// For cardio, we override the effort-based MET with speed-based MET
|
|
107
|
+
met = getCardioMachineMET(set, metabolicData, metMin, metMax, effortNormalized);
|
|
108
|
+
break;
|
|
109
|
+
case "cardio-free":
|
|
110
|
+
met = getCardioFreeMET(set, metabolicData, metMin, metMax, effortNormalized);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
// Apply compound multiplier (multi-joint exercises have higher energy cost)
|
|
114
|
+
met *= metabolicData.compoundMultiplier;
|
|
115
|
+
// Safety clamp: MET should never go below 1 or above 25
|
|
116
|
+
return (0, helpers_1.clamp)(met, 1, 25);
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Type-Specific Multipliers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* Weight multiplier for weight-reps exercises.
|
|
123
|
+
*
|
|
124
|
+
* Classifies the weight as light/moderate/heavy relative to bodyweight
|
|
125
|
+
* and returns the corresponding multiplier.
|
|
126
|
+
*
|
|
127
|
+
* Example: 10kg at 75kg bodyweight → ratio 0.13 → "light" → 0.8 multiplier
|
|
128
|
+
*/
|
|
129
|
+
function getWeightMultiplier(kg, userWeightKg, metabolicData) {
|
|
130
|
+
const ratio = kg / userWeightKg;
|
|
131
|
+
// Resolve weight factors (handle both nested and flat shapes)
|
|
132
|
+
const factors = resolveWeightFactors(metabolicData);
|
|
133
|
+
if (ratio < constants_1.WEIGHT_CATEGORY_THRESHOLDS.lightMax) {
|
|
134
|
+
return factors.light;
|
|
135
|
+
}
|
|
136
|
+
else if (ratio < constants_1.WEIGHT_CATEGORY_THRESHOLDS.moderateMax) {
|
|
137
|
+
return factors.moderate;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
return factors.heavy;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Multiplier for reps-only (bodyweight) exercises.
|
|
145
|
+
*
|
|
146
|
+
* The "load" is the user's bodyweight (scaled by difficulty) + any aux weight.
|
|
147
|
+
* We return a gentle multiplier that boosts MET slightly for added weight.
|
|
148
|
+
*
|
|
149
|
+
* Example: Push-up (difficulty 1) with 2.5kg vest at 75kg bodyweight
|
|
150
|
+
* bodyweightLoad = (1/4) × 0.65 × 75 = 12.19 kg equivalent
|
|
151
|
+
* totalLoad = 12.19 + 2.5 = 14.69
|
|
152
|
+
* ratio = 14.69 / 75 = 0.196
|
|
153
|
+
* multiplier = 1.0 + 0.196 × 0.5 = 1.098
|
|
154
|
+
*/
|
|
155
|
+
function getRepsOnlyMultiplier(auxWeightKg, userWeightKg, difficultyLevel) {
|
|
156
|
+
const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * userWeightKg;
|
|
157
|
+
const totalLoad = bodyweightLoad + auxWeightKg;
|
|
158
|
+
const ratio = totalLoad / userWeightKg;
|
|
159
|
+
// Gentle scaling: aux weight adds intensity but not dramatically
|
|
160
|
+
return 1.0 + ratio * 0.5;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Multiplier for duration (isometric/hold) exercises.
|
|
164
|
+
*
|
|
165
|
+
* Two factors:
|
|
166
|
+
* 1. Duration category (short/medium/long) — longer holds are metabolically harder
|
|
167
|
+
* 2. Aux weight boost — holding weight during a plank increases energy cost
|
|
168
|
+
*/
|
|
169
|
+
function getDurationMultiplier(durationSecs, auxWeightKg, userWeightKg, metabolicData) {
|
|
170
|
+
// Resolve duration factors (handle both nested and flat shapes)
|
|
171
|
+
const factors = resolveDurationFactors(metabolicData);
|
|
172
|
+
let durationMultiplier;
|
|
173
|
+
if (durationSecs < constants_1.DURATION_CATEGORY_THRESHOLDS.shortMax) {
|
|
174
|
+
durationMultiplier = factors.short;
|
|
175
|
+
}
|
|
176
|
+
else if (durationSecs <= constants_1.DURATION_CATEGORY_THRESHOLDS.mediumMax) {
|
|
177
|
+
durationMultiplier = factors.medium;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
durationMultiplier = factors.long;
|
|
181
|
+
}
|
|
182
|
+
// Aux weight boosts MET (holding a plate during plank, weighted vest, etc.)
|
|
183
|
+
const auxBoost = 1.0 + (auxWeightKg / userWeightKg) * 0.3;
|
|
184
|
+
return durationMultiplier * auxBoost;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* MET calculation for cardio-machine exercises (treadmill, stationary bike, etc.)
|
|
188
|
+
*
|
|
189
|
+
* Uses average speed to interpolate within the MET range.
|
|
190
|
+
* If paceFactors are available, we interpolate from the pace table instead.
|
|
191
|
+
*/
|
|
192
|
+
function getCardioMachineMET(set, metabolicData, metMin, metMax, effortNormalized) {
|
|
193
|
+
var _a, _b;
|
|
194
|
+
const speedMin = (_a = set.speedMin) !== null && _a !== void 0 ? _a : 0;
|
|
195
|
+
const speedMax = (_b = set.speedMax) !== null && _b !== void 0 ? _b : 0;
|
|
196
|
+
// Average speed from the set data
|
|
197
|
+
const avgSpeed = (speedMin + speedMax) / 2;
|
|
198
|
+
if (avgSpeed > 0) {
|
|
199
|
+
// If we have pace factors, use them for more accurate MET lookup
|
|
200
|
+
if (metabolicData.paceFactors) {
|
|
201
|
+
return interpolatePaceFactors(avgSpeed, metabolicData.paceFactors);
|
|
202
|
+
}
|
|
203
|
+
// Otherwise, interpolate linearly within metRange based on speed
|
|
204
|
+
const speedFraction = (0, helpers_1.clamp)((avgSpeed - constants_1.CARDIO_SPEED_RANGE.min) /
|
|
205
|
+
(constants_1.CARDIO_SPEED_RANGE.max - constants_1.CARDIO_SPEED_RANGE.min), 0, 1);
|
|
206
|
+
return metMin + (metMax - metMin) * speedFraction;
|
|
207
|
+
}
|
|
208
|
+
// No speed data: fall back to effort-based interpolation
|
|
209
|
+
return metMin + (metMax - metMin) * effortNormalized;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* MET calculation for cardio-free exercises (outdoor running, cycling, etc.)
|
|
213
|
+
*
|
|
214
|
+
* Derives speed from distance ÷ time, then interpolates MET.
|
|
215
|
+
*/
|
|
216
|
+
function getCardioFreeMET(set, metabolicData, metMin, metMax, effortNormalized) {
|
|
217
|
+
var _a, _b;
|
|
218
|
+
const distance = (_a = set.distance) !== null && _a !== void 0 ? _a : 0;
|
|
219
|
+
const durationSecs = (_b = set.cardioDurationSecs) !== null && _b !== void 0 ? _b : 0;
|
|
220
|
+
if (distance > 0 && durationSecs > 0) {
|
|
221
|
+
// Speed in km/h
|
|
222
|
+
const speed = distance / (durationSecs / 3600);
|
|
223
|
+
if (metabolicData.paceFactors) {
|
|
224
|
+
return interpolatePaceFactors(speed, metabolicData.paceFactors);
|
|
225
|
+
}
|
|
226
|
+
const speedFraction = (0, helpers_1.clamp)((speed - constants_1.CARDIO_SPEED_RANGE.min) /
|
|
227
|
+
(constants_1.CARDIO_SPEED_RANGE.max - constants_1.CARDIO_SPEED_RANGE.min), 0, 1);
|
|
228
|
+
return metMin + (metMax - metMin) * speedFraction;
|
|
229
|
+
}
|
|
230
|
+
// No distance/time data: fall back to effort-based
|
|
231
|
+
return metMin + (metMax - metMin) * effortNormalized;
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Resolution Helpers (handle flat vs nested metabolicData shapes)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
/**
|
|
237
|
+
* Resolve weight factors from metabolicData.
|
|
238
|
+
* Sample data sometimes has them nested under `weightFactors`, sometimes flat.
|
|
239
|
+
*/
|
|
240
|
+
function resolveWeightFactors(metabolicData) {
|
|
241
|
+
var _a, _b;
|
|
242
|
+
if (metabolicData.weightFactors) {
|
|
243
|
+
return {
|
|
244
|
+
light: metabolicData.weightFactors.lightWeight,
|
|
245
|
+
moderate: metabolicData.weightFactors.moderateWeight,
|
|
246
|
+
heavy: metabolicData.weightFactors.heavyWeight,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// Check flat shape
|
|
250
|
+
if (metabolicData.lightWeight !== undefined) {
|
|
251
|
+
return {
|
|
252
|
+
light: metabolicData.lightWeight,
|
|
253
|
+
moderate: (_a = metabolicData.moderateWeight) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_WEIGHT_FACTORS.moderate,
|
|
254
|
+
heavy: (_b = metabolicData.heavyWeight) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_WEIGHT_FACTORS.heavy,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return constants_1.DEFAULT_WEIGHT_FACTORS;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Resolve duration factors from metabolicData.
|
|
261
|
+
* Same flat-vs-nested handling as weight factors.
|
|
262
|
+
*/
|
|
263
|
+
function resolveDurationFactors(metabolicData) {
|
|
264
|
+
var _a, _b;
|
|
265
|
+
if (metabolicData.durationFactors) {
|
|
266
|
+
return {
|
|
267
|
+
short: metabolicData.durationFactors.shortDuration,
|
|
268
|
+
medium: metabolicData.durationFactors.mediumDuration,
|
|
269
|
+
long: metabolicData.durationFactors.longDuration,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// Check flat shape
|
|
273
|
+
if (metabolicData.shortDuration !== undefined) {
|
|
274
|
+
return {
|
|
275
|
+
short: metabolicData.shortDuration,
|
|
276
|
+
medium: (_a = metabolicData.mediumDuration) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_DURATION_FACTORS.medium,
|
|
277
|
+
long: (_b = metabolicData.longDuration) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_DURATION_FACTORS.long,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return constants_1.DEFAULT_DURATION_FACTORS;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Interpolate MET from a pace factors table.
|
|
284
|
+
*
|
|
285
|
+
* paceFactors is a map of speed (km/h) → MET value.
|
|
286
|
+
* We find the two closest speeds and linearly interpolate between them.
|
|
287
|
+
*
|
|
288
|
+
* Example:
|
|
289
|
+
* paceFactors = { "5.0": 4.3, "8.0": 8.3, "12.0": 11.0, "16.0": 15.0 }
|
|
290
|
+
* speed = 10 km/h → interpolate between 8.0 (8.3 MET) and 12.0 (11.0 MET)
|
|
291
|
+
* result ≈ 9.65 MET
|
|
292
|
+
*/
|
|
293
|
+
function interpolatePaceFactors(speed, paceFactors) {
|
|
294
|
+
const entries = Object.keys(paceFactors)
|
|
295
|
+
.map((k) => [parseFloat(k), paceFactors[k]])
|
|
296
|
+
.filter(([k]) => !isNaN(k))
|
|
297
|
+
.sort((a, b) => a[0] - b[0]);
|
|
298
|
+
if (entries.length === 0)
|
|
299
|
+
return 3; // fallback moderate MET
|
|
300
|
+
// Below minimum pace
|
|
301
|
+
if (speed <= entries[0][0])
|
|
302
|
+
return entries[0][1];
|
|
303
|
+
// Above maximum pace
|
|
304
|
+
if (speed >= entries[entries.length - 1][0])
|
|
305
|
+
return entries[entries.length - 1][1];
|
|
306
|
+
// Find surrounding entries and interpolate
|
|
307
|
+
for (let i = 0; i < entries.length - 1; i++) {
|
|
308
|
+
const [speedLow, metLow] = entries[i];
|
|
309
|
+
const [speedHigh, metHigh] = entries[i + 1];
|
|
310
|
+
if (speed >= speedLow && speed <= speedHigh) {
|
|
311
|
+
const fraction = (speed - speedLow) / (speedHigh - speedLow);
|
|
312
|
+
return metLow + (metHigh - metLow) * fraction;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return entries[entries.length - 1][1]; // shouldn't reach here
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Normalize metRange so [0] is always min and [1] is always max.
|
|
319
|
+
* Sample data has metRange as [5, 2] (max first) for some exercises.
|
|
320
|
+
*/
|
|
321
|
+
function normalizeMetRange(metRange, baseMET) {
|
|
322
|
+
const min = Math.min(metRange[0], metRange[1]);
|
|
323
|
+
const max = Math.max(metRange[0], metRange[1]);
|
|
324
|
+
// If range seems invalid, create one around baseMET
|
|
325
|
+
if (min === max || max === 0) {
|
|
326
|
+
return [baseMET * 0.7, baseMET * 1.5];
|
|
327
|
+
}
|
|
328
|
+
return [min, max];
|
|
329
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Pillar 2: Muscle Fatigue
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Calculates per-muscle fatigue scores (0–100) for a single exercise.
|
|
7
|
+
*
|
|
8
|
+
* HOW IT WORKS:
|
|
9
|
+
*
|
|
10
|
+
* 1. For each set, compute a "fatigue stimulus" — how much mechanical work
|
|
11
|
+
* the muscles experienced. This is type-specific:
|
|
12
|
+
* weight-reps: kg × reps
|
|
13
|
+
* reps-only: (bodyweightLoad + auxWeight) × reps
|
|
14
|
+
* duration: durationSecs × (difficultyLevel + 1) × auxBoost
|
|
15
|
+
* cardio: durationSecs × speedFactor
|
|
16
|
+
*
|
|
17
|
+
* 2. Multiply by effort (RPE/RIR → 0.5–1.3) and the exercise's
|
|
18
|
+
* fatigueMultiplier from timingGuardrails.
|
|
19
|
+
*
|
|
20
|
+
* 3. Accumulate across sets with DIMINISHING RETURNS — later sets contribute
|
|
21
|
+
* less because the muscle is pre-fatigued and can't generate as much force.
|
|
22
|
+
* Decay: setDecay = 1 / (1 + 0.15 × setIndex)
|
|
23
|
+
*
|
|
24
|
+
* 4. Distribute the accumulated fatigue to muscles:
|
|
25
|
+
* Primary muscles → 100% of stimulus
|
|
26
|
+
* Secondary muscles → 35% of stimulus
|
|
27
|
+
*
|
|
28
|
+
* 5. Normalize to 0–100 using an EXERCISE-AWARE reference maximum.
|
|
29
|
+
* The reference max is what "5 hard sets of THIS exercise" would produce,
|
|
30
|
+
* scaled by the exercise's difficultyLevel so bicep curls and squats
|
|
31
|
+
* get fair scores.
|
|
32
|
+
*
|
|
33
|
+
* IMPORTANT: This function scores ONE exercise. The caller aggregates
|
|
34
|
+
* across exercises for full-workout muscle fatigue maps.
|
|
35
|
+
*/
|
|
36
|
+
import type { IParsedSet, IUserContext } from "./types";
|
|
37
|
+
import type { ITimingGuardrails } from "./parseRecords";
|
|
38
|
+
/** Muscle key from EBodyParts (e.g., "pectoralis-major", "quadriceps") */
|
|
39
|
+
type MuscleKey = string;
|
|
40
|
+
/**
|
|
41
|
+
* Minimal exercise metadata needed for fatigue calculation.
|
|
42
|
+
*/
|
|
43
|
+
interface IFatigueExerciseData {
|
|
44
|
+
primaryMuscles: MuscleKey[];
|
|
45
|
+
secondaryMuscles: MuscleKey[];
|
|
46
|
+
difficultyLevel: number;
|
|
47
|
+
metabolicData: {
|
|
48
|
+
compoundMultiplier: number;
|
|
49
|
+
muscleGroupFactor: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Calculate per-muscle fatigue scores for an exercise.
|
|
54
|
+
*
|
|
55
|
+
* @param sets Parsed & validated sets (from parseRecords)
|
|
56
|
+
* @param exercise Exercise metadata (muscles, difficulty, metabolic)
|
|
57
|
+
* @param user Validated user context
|
|
58
|
+
* @param timingGuardrails For fatigueMultiplier
|
|
59
|
+
* @returns Map of muscle key → fatigue score (0–100)
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const fatigue = calculateMuscleFatigue(parsedSets, exercise, userCtx, exercise.timingGuardrails);
|
|
63
|
+
* // → { "pectoralis-major": 24, "pectoralis-minor": 24, "deltoids-anterior": 8, ... }
|
|
64
|
+
*/
|
|
65
|
+
export declare function calculateMuscleFatigue(sets: IParsedSet[], exercise: IFatigueExerciseData, user: IUserContext, timingGuardrails?: ITimingGuardrails): Record<string, number>;
|
|
66
|
+
export {};
|