@dofek/training 0.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/activity-icons.d.ts +10 -0
  4. package/dist/activity-icons.d.ts.map +1 -0
  5. package/dist/activity-icons.js +102 -0
  6. package/dist/activity-icons.js.map +1 -0
  7. package/dist/climbing-grades.d.ts +8 -0
  8. package/dist/climbing-grades.d.ts.map +1 -0
  9. package/dist/climbing-grades.js +80 -0
  10. package/dist/climbing-grades.js.map +1 -0
  11. package/dist/derived-cardio.d.ts +17 -0
  12. package/dist/derived-cardio.d.ts.map +1 -0
  13. package/dist/derived-cardio.js +84 -0
  14. package/dist/derived-cardio.js.map +1 -0
  15. package/dist/endurance-types.d.ts +21 -0
  16. package/dist/endurance-types.d.ts.map +1 -0
  17. package/dist/endurance-types.js +39 -0
  18. package/dist/endurance-types.js.map +1 -0
  19. package/dist/grade-adjusted-pace.d.ts +34 -0
  20. package/dist/grade-adjusted-pace.d.ts.map +1 -0
  21. package/dist/grade-adjusted-pace.js +41 -0
  22. package/dist/grade-adjusted-pace.js.map +1 -0
  23. package/dist/muscle-groups.d.ts +35 -0
  24. package/dist/muscle-groups.d.ts.map +1 -0
  25. package/dist/muscle-groups.js +135 -0
  26. package/dist/muscle-groups.js.map +1 -0
  27. package/dist/pmc.d.ts +77 -0
  28. package/dist/pmc.d.ts.map +1 -0
  29. package/dist/pmc.js +83 -0
  30. package/dist/pmc.js.map +1 -0
  31. package/dist/power-analysis.d.ts +82 -0
  32. package/dist/power-analysis.d.ts.map +1 -0
  33. package/dist/power-analysis.js +162 -0
  34. package/dist/power-analysis.js.map +1 -0
  35. package/dist/training-distribution.d.ts +43 -0
  36. package/dist/training-distribution.d.ts.map +1 -0
  37. package/dist/training-distribution.js +68 -0
  38. package/dist/training-distribution.js.map +1 -0
  39. package/dist/training-load.d.ts +66 -0
  40. package/dist/training-load.d.ts.map +1 -0
  41. package/dist/training-load.js +89 -0
  42. package/dist/training-load.js.map +1 -0
  43. package/dist/training.d.ts +61 -0
  44. package/dist/training.d.ts.map +1 -0
  45. package/dist/training.js +564 -0
  46. package/dist/training.js.map +1 -0
  47. package/dist/workout-recommendation.d.ts +86 -0
  48. package/dist/workout-recommendation.d.ts.map +1 -0
  49. package/dist/workout-recommendation.js +338 -0
  50. package/dist/workout-recommendation.js.map +1 -0
  51. package/package.json +52 -0
@@ -0,0 +1,35 @@
1
+ export type BodySlug = "abs" | "biceps" | "calves" | "chest" | "deltoids" | "forearm" | "gluteal" | "hamstring" | "lower-back" | "obliques" | "quadriceps" | "trapezius" | "triceps" | "upper-back";
2
+ export declare function muscleGroupLabel(slug: string): string;
3
+ /** Expand a WHOOP muscle group name to library-compatible slugs. */
4
+ export declare function expandMuscleGroup(group: string): string[];
5
+ export interface MuscleGroupInput {
6
+ muscleGroup: string;
7
+ weeklyData: {
8
+ week: string;
9
+ sets: number;
10
+ }[];
11
+ }
12
+ /**
13
+ * Compute total sets per slug from the API data.
14
+ * Handles coarse-to-fine expansion (e.g., "BACK" distributes evenly
15
+ * across trapezius, upper-back, lower-back).
16
+ */
17
+ export declare function computeSlugTotals(data: MuscleGroupInput[]): Map<string, number>;
18
+ /**
19
+ * Normalize slug totals to 0-1 intensity values (relative to max).
20
+ */
21
+ export declare function computeIntensities(slugTotals: Map<string, number>): Map<string, number>;
22
+ /** Number of discrete color buckets for the gradient. */
23
+ export declare const COLOR_BUCKET_COUNT = 5;
24
+ /** Color for a trained muscle group at the given intensity (0-1). */
25
+ export declare function muscleGroupFillColor(intensity: number): string;
26
+ /**
27
+ * Pre-computed array of N colors from lightest to darkest green,
28
+ * suitable for the `highlightedColors` / `colors` props of body highlighter libs.
29
+ */
30
+ export declare const INTENSITY_COLORS: string[];
31
+ /**
32
+ * Convert a 0-1 intensity to a 1-based bucket index (for library `frequency`/`intensity` props).
33
+ */
34
+ export declare function intensityToBucket(intensity: number): number;
35
+ //# sourceMappingURL=muscle-groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"muscle-groups.d.ts","sourceRoot":"","sources":["../src/muscle-groups.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,QAAQ,GAChB,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,UAAU,GACV,SAAS,GACT,SAAS,GACT,WAAW,GACX,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,WAAW,GACX,SAAS,GACT,YAAY,CAAC;AAuBjB,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AA4CD,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAGzD;AAMD,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAc/E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASvF;AAMD,yDAAyD;AACzD,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAcpC,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAO9D;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,EAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG3D"}
@@ -0,0 +1,135 @@
1
+ import { surfaceColors } from "@dofek/scoring/colors";
2
+ // ---------------------------------------------------------------------------
3
+ // Labels — human-readable display names for slugs
4
+ // ---------------------------------------------------------------------------
5
+ const SLUG_LABELS = {
6
+ abs: "Abs",
7
+ biceps: "Biceps",
8
+ calves: "Calves",
9
+ chest: "Chest",
10
+ deltoids: "Shoulders",
11
+ forearm: "Forearms",
12
+ gluteal: "Glutes",
13
+ hamstring: "Hamstrings",
14
+ "lower-back": "Lower Back",
15
+ obliques: "Obliques",
16
+ quadriceps: "Quads",
17
+ trapezius: "Traps",
18
+ triceps: "Triceps",
19
+ "upper-back": "Upper Back",
20
+ };
21
+ export function muscleGroupLabel(slug) {
22
+ return SLUG_LABELS[slug] ?? titleCase(slug);
23
+ }
24
+ function titleCase(value) {
25
+ return value.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // WHOOP group → library slug mapping
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Maps a WHOOP muscle group name to body-highlighter library slugs.
32
+ * Coarse groups (BACK, LEGS, CORE) are distributed across multiple slugs.
33
+ */
34
+ const WHOOP_TO_SLUGS = {
35
+ CHEST: ["chest"],
36
+ BACK: ["trapezius", "upper-back", "lower-back"],
37
+ SHOULDERS: ["deltoids"],
38
+ BICEPS: ["biceps"],
39
+ TRICEPS: ["triceps"],
40
+ FOREARMS: ["forearm"],
41
+ CORE: ["abs", "obliques"],
42
+ ABS: ["abs"],
43
+ OBLIQUES: ["obliques"],
44
+ LEGS: ["quadriceps", "hamstring", "calves", "gluteal"],
45
+ QUADRICEPS: ["quadriceps"],
46
+ QUADS: ["quadriceps"],
47
+ HAMSTRINGS: ["hamstring"],
48
+ CALVES: ["calves"],
49
+ GLUTES: ["gluteal"],
50
+ GLUTEAL: ["gluteal"],
51
+ TRAPS: ["trapezius"],
52
+ TRAPEZIUS: ["trapezius"],
53
+ LATS: ["upper-back"],
54
+ MIDDLE_BACK: ["upper-back"],
55
+ UPPER_BACK: ["upper-back"],
56
+ LOWER_BACK: ["lower-back"],
57
+ DELTOIDS: ["deltoids"],
58
+ DELTS: ["deltoids"],
59
+ ARMS: ["biceps", "triceps", "forearm"],
60
+ ABDOMINALS: ["abs"],
61
+ LATISSIMUS: ["upper-back"],
62
+ };
63
+ /** Expand a WHOOP muscle group name to library-compatible slugs. */
64
+ export function expandMuscleGroup(group) {
65
+ const upper = group.toUpperCase();
66
+ return WHOOP_TO_SLUGS[upper] ?? [group.toLowerCase()];
67
+ }
68
+ /**
69
+ * Compute total sets per slug from the API data.
70
+ * Handles coarse-to-fine expansion (e.g., "BACK" distributes evenly
71
+ * across trapezius, upper-back, lower-back).
72
+ */
73
+ export function computeSlugTotals(data) {
74
+ const totals = new Map();
75
+ for (const group of data) {
76
+ const totalSets = group.weeklyData.reduce((sum, week) => sum + week.sets, 0);
77
+ const slugs = expandMuscleGroup(group.muscleGroup);
78
+ const setsPerSlug = totalSets / slugs.length;
79
+ for (const slug of slugs) {
80
+ totals.set(slug, (totals.get(slug) ?? 0) + setsPerSlug);
81
+ }
82
+ }
83
+ return totals;
84
+ }
85
+ /**
86
+ * Normalize slug totals to 0-1 intensity values (relative to max).
87
+ */
88
+ export function computeIntensities(slugTotals) {
89
+ const maxSets = Math.max(0, ...slugTotals.values());
90
+ if (maxSets === 0)
91
+ return new Map();
92
+ const intensities = new Map();
93
+ for (const [slug, sets] of slugTotals) {
94
+ intensities.set(slug, sets / maxSets);
95
+ }
96
+ return intensities;
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Color scale
100
+ // ---------------------------------------------------------------------------
101
+ /** Number of discrete color buckets for the gradient. */
102
+ export const COLOR_BUCKET_COUNT = 5;
103
+ // Green gradient endpoints
104
+ const MIN_RGB = { red: 200, green: 228, blue: 212 };
105
+ const MAX_RGB = { red: 45, green: 122, blue: 86 };
106
+ function lerp(from, to, ratio) {
107
+ return Math.round(from + (to - from) * ratio);
108
+ }
109
+ function toHex(value) {
110
+ return value.toString(16).padStart(2, "0");
111
+ }
112
+ /** Color for a trained muscle group at the given intensity (0-1). */
113
+ export function muscleGroupFillColor(intensity) {
114
+ if (intensity <= 0)
115
+ return surfaceColors.surfaceSecondary;
116
+ const clamped = Math.min(1, intensity);
117
+ const red = lerp(MIN_RGB.red, MAX_RGB.red, clamped);
118
+ const green = lerp(MIN_RGB.green, MAX_RGB.green, clamped);
119
+ const blue = lerp(MIN_RGB.blue, MAX_RGB.blue, clamped);
120
+ return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
121
+ }
122
+ /**
123
+ * Pre-computed array of N colors from lightest to darkest green,
124
+ * suitable for the `highlightedColors` / `colors` props of body highlighter libs.
125
+ */
126
+ export const INTENSITY_COLORS = Array.from({ length: COLOR_BUCKET_COUNT }, (_, index) => muscleGroupFillColor((index + 1) / COLOR_BUCKET_COUNT));
127
+ /**
128
+ * Convert a 0-1 intensity to a 1-based bucket index (for library `frequency`/`intensity` props).
129
+ */
130
+ export function intensityToBucket(intensity) {
131
+ if (intensity <= 0)
132
+ return 0;
133
+ return Math.max(1, Math.min(COLOR_BUCKET_COUNT, Math.ceil(intensity * COLOR_BUCKET_COUNT)));
134
+ }
135
+ //# sourceMappingURL=muscle-groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"muscle-groups.js","sourceRoot":"","sources":["../src/muscle-groups.ts"],"names":[],"mappings":"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"}
package/dist/pmc.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Performance Management Chart (PMC) domain logic.
3
+ *
4
+ * Pure functions for computing Training Stress Score (TSS), TRIMP,
5
+ * and FTP estimation. No database or infrastructure dependencies.
6
+ * Shared between web and iOS via @dofek/training.
7
+ */
8
+ export type ActivityRow = {
9
+ id: string;
10
+ date: string;
11
+ duration_min: number;
12
+ avg_hr: number;
13
+ max_hr: number;
14
+ avg_power: number | null;
15
+ power_samples: number;
16
+ hr_samples: number;
17
+ };
18
+ export interface PmcDataPoint {
19
+ date: string;
20
+ load: number;
21
+ ctl: number;
22
+ atl: number;
23
+ tsb: number;
24
+ }
25
+ export interface TssModelInfo {
26
+ type: "learned" | "generic";
27
+ pairedActivities: number;
28
+ r2: number | null;
29
+ ftp: number | null;
30
+ }
31
+ export interface PmcChartResult {
32
+ data: PmcDataPoint[];
33
+ model: TssModelInfo;
34
+ }
35
+ /**
36
+ * Compute Bannister TRIMP for an activity.
37
+ *
38
+ * TRIMP = duration_minutes * deltaHR_ratio * genderFactor * e^(exponent * deltaHR_ratio)
39
+ * where deltaHR_ratio = (avg_hr - resting_hr) / (max_hr - resting_hr)
40
+ *
41
+ * genderFactor and exponent default to 0.64 and 1.92 (Bannister generic)
42
+ * but can be personalized per user.
43
+ */
44
+ export declare function computeTrimp(durationMin: number, avgHr: number, maxHr: number, restingHr: number, genderFactor?: number, exponent?: number): number;
45
+ /**
46
+ * Compute hrTSS using Bannister TRIMP normalized to 1hr at threshold.
47
+ * This is the fallback when no learned model is available.
48
+ */
49
+ export declare function computeHrTss(durationMin: number, avgHr: number, maxHr: number, restingHr: number, genderFactor?: number, exponent?: number): number;
50
+ /**
51
+ * Compute power-based TSS.
52
+ * TSS = (NP / FTP)^2 * duration_hours * 100
53
+ * Uses Normalized Power (4th root of mean of 4th powers of 30s rolling avg).
54
+ */
55
+ export declare function computePowerTss(normalizedPower: number, ftp: number, durationMin: number): number;
56
+ /**
57
+ * Build a linear regression model: powerTss = slope * trimp + intercept.
58
+ * Returns null if insufficient data or poor fit.
59
+ */
60
+ export declare function buildTssModel(paired: {
61
+ trimp: number;
62
+ powerTss: number;
63
+ }[]): {
64
+ slope: number;
65
+ intercept: number;
66
+ r2: number;
67
+ } | null;
68
+ /**
69
+ * Estimate FTP from activity data.
70
+ * Uses highest avg_power from activities >= 20 min duration, multiplied by 0.95.
71
+ *
72
+ * Intentionally uses avg_power rather than Normalized Power (NP).
73
+ * NP inflates power for interval workouts via 4th-power averaging,
74
+ * which produces unrealistically high FTP estimates from variable efforts.
75
+ */
76
+ export declare function estimateFtp(activities: ActivityRow[]): number | null;
77
+ //# sourceMappingURL=pmc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pmc.d.ts","sourceRoot":"","sources":["../src/pmc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,YAAY,SAAO,EACnB,QAAQ,SAAO,GACd,MAAM,CAKR;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,YAAY,SAAO,EACnB,QAAQ,SAAO,GACd,MAAM,CAWR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAIjG;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAAE,GAC5C;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAazD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAOpE"}
package/dist/pmc.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Performance Management Chart (PMC) domain logic.
3
+ *
4
+ * Pure functions for computing Training Stress Score (TSS), TRIMP,
5
+ * and FTP estimation. No database or infrastructure dependencies.
6
+ * Shared between web and iOS via @dofek/training.
7
+ */
8
+ import { linearRegression } from "./power-analysis.js";
9
+ /**
10
+ * Compute Bannister TRIMP for an activity.
11
+ *
12
+ * TRIMP = duration_minutes * deltaHR_ratio * genderFactor * e^(exponent * deltaHR_ratio)
13
+ * where deltaHR_ratio = (avg_hr - resting_hr) / (max_hr - resting_hr)
14
+ *
15
+ * genderFactor and exponent default to 0.64 and 1.92 (Bannister generic)
16
+ * but can be personalized per user.
17
+ */
18
+ export function computeTrimp(durationMin, avgHr, maxHr, restingHr, genderFactor = 0.64, exponent = 1.92) {
19
+ if (maxHr <= restingHr || durationMin <= 0)
20
+ return 0;
21
+ const deltaHrRatio = (avgHr - restingHr) / (maxHr - restingHr);
22
+ if (deltaHrRatio <= 0)
23
+ return 0;
24
+ return durationMin * deltaHrRatio * genderFactor * Math.exp(exponent * deltaHrRatio);
25
+ }
26
+ /**
27
+ * Compute hrTSS using Bannister TRIMP normalized to 1hr at threshold.
28
+ * This is the fallback when no learned model is available.
29
+ */
30
+ export function computeHrTss(durationMin, avgHr, maxHr, restingHr, genderFactor = 0.64, exponent = 1.92) {
31
+ const trimp = computeTrimp(durationMin, avgHr, maxHr, restingHr, genderFactor, exponent);
32
+ if (trimp === 0)
33
+ return 0;
34
+ // Threshold HR at 85% of max HR
35
+ const thresholdDeltaRatio = 0.85;
36
+ const trimpOneHourAtThreshold = 60 * thresholdDeltaRatio * genderFactor * Math.exp(exponent * thresholdDeltaRatio);
37
+ if (trimpOneHourAtThreshold === 0)
38
+ return 0;
39
+ return (trimp / trimpOneHourAtThreshold) * 100;
40
+ }
41
+ /**
42
+ * Compute power-based TSS.
43
+ * TSS = (NP / FTP)^2 * duration_hours * 100
44
+ * Uses Normalized Power (4th root of mean of 4th powers of 30s rolling avg).
45
+ */
46
+ export function computePowerTss(normalizedPower, ftp, durationMin) {
47
+ if (ftp <= 0 || durationMin <= 0 || normalizedPower <= 0)
48
+ return 0;
49
+ const intensityFactor = normalizedPower / ftp;
50
+ return intensityFactor ** 2 * (durationMin / 60) * 100;
51
+ }
52
+ /**
53
+ * Build a linear regression model: powerTss = slope * trimp + intercept.
54
+ * Returns null if insufficient data or poor fit.
55
+ */
56
+ export function buildTssModel(paired) {
57
+ // Require at least 10 paired activities
58
+ if (paired.length < 10)
59
+ return null;
60
+ const xs = paired.map((point) => point.trimp);
61
+ const ys = paired.map((point) => point.powerTss);
62
+ const result = linearRegression(xs, ys);
63
+ // Require a reasonable fit (R² >= 0.3) and positive slope
64
+ if (result.r2 < 0.3 || result.slope <= 0)
65
+ return null;
66
+ return result;
67
+ }
68
+ /**
69
+ * Estimate FTP from activity data.
70
+ * Uses highest avg_power from activities >= 20 min duration, multiplied by 0.95.
71
+ *
72
+ * Intentionally uses avg_power rather than Normalized Power (NP).
73
+ * NP inflates power for interval workouts via 4th-power averaging,
74
+ * which produces unrealistically high FTP estimates from variable efforts.
75
+ */
76
+ export function estimateFtp(activities) {
77
+ const qualifying = activities.filter((act) => act.avg_power != null && act.avg_power > 0 && act.duration_min >= 20);
78
+ if (qualifying.length === 0)
79
+ return null;
80
+ const bestPower = Math.max(...qualifying.map((act) => Number(act.avg_power)));
81
+ return Math.round(bestPower * 0.95);
82
+ }
83
+ //# sourceMappingURL=pmc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pmc.js","sourceRoot":"","sources":["../src/pmc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Power analysis: duration curves, Critical Power model, Normalized Power.
3
+ *
4
+ * Pure functions with no database or infrastructure dependencies.
5
+ * Shared between web and iOS via @dofek/training.
6
+ */
7
+ export declare const STANDARD_DURATIONS: number[];
8
+ /** Human-readable labels for standard duration curve durations. */
9
+ export declare const DURATION_LABELS: Record<number, string>;
10
+ /** Simple linear regression: y = slope * x + intercept */
11
+ export declare function linearRegression(xs: number[], ys: number[]): {
12
+ slope: number;
13
+ intercept: number;
14
+ r2: number;
15
+ };
16
+ export interface CriticalPowerModel {
17
+ cp: number;
18
+ wPrime: number;
19
+ r2: number;
20
+ }
21
+ /**
22
+ * Fit Morton's 2-parameter Critical Power model (Monod-Scherrer).
23
+ *
24
+ * Model: P(t) = CP + W'/t
25
+ * Linearized: Work = P*t = CP*t + W'
26
+ * Linear regression of Work vs Time gives slope=CP, intercept=W'.
27
+ *
28
+ * Only uses durations 120–600s. This range avoids both anaerobic-dominated
29
+ * efforts (<120s) and long-duration bests that are suppressed by interval
30
+ * training recovery periods (>600s).
31
+ */
32
+ export declare function fitCriticalPower(points: {
33
+ durationSeconds: number;
34
+ bestPower: number;
35
+ }[]): CriticalPowerModel | null;
36
+ export interface ActivityGroup<T> {
37
+ rows: T[];
38
+ activityDate: string;
39
+ intervalSeconds: number;
40
+ }
41
+ /** Group pre-sorted samples by activity_id in a single pass. */
42
+ export declare function groupByActivity<T extends {
43
+ activity_id: string;
44
+ activity_date: string;
45
+ interval_s: number;
46
+ }>(samples: T[]): ActivityGroup<T>[];
47
+ export interface PowerCurvePoint {
48
+ durationSeconds: number;
49
+ bestPower: number;
50
+ activityDate: string;
51
+ }
52
+ interface PowerCurveSample {
53
+ activity_id: string;
54
+ activity_date: string;
55
+ power: number;
56
+ interval_s: number;
57
+ }
58
+ /**
59
+ * Compute best average power for each standard duration across all activities.
60
+ * Uses prefix sums for O(N × D) performance where N = total samples, D = 14 durations.
61
+ */
62
+ export declare function computePowerCurve(samples: PowerCurveSample[]): PowerCurvePoint[];
63
+ export interface NormalizedPowerResult {
64
+ activityDate: string;
65
+ activityName: string | null;
66
+ normalizedPower: number;
67
+ }
68
+ interface NormalizedPowerSample {
69
+ activity_id: string;
70
+ activity_date: string;
71
+ activity_name: string | null;
72
+ power: number;
73
+ interval_s: number;
74
+ }
75
+ /**
76
+ * Compute Normalized Power per activity using 30-second rolling averages.
77
+ * NP = (mean(rolling_30s_avg^4))^0.25 — accounts for the metabolic cost
78
+ * of variable-intensity efforts.
79
+ */
80
+ export declare function computeNormalizedPower(samples: NormalizedPowerSample[]): NormalizedPowerResult[];
81
+ export {};
82
+ //# sourceMappingURL=power-analysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power-analysis.d.ts","sourceRoot":"","sources":["../src/power-analysis.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,eAAO,MAAM,kBAAkB,UAE9B,CAAC;AAEF,mEAAmE;AACnE,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAelD,CAAC;AAIF,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EAAE,EACZ,EAAE,EAAE,MAAM,EAAE,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAmBlD;AAID,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,GACvD,kBAAkB,GAAG,IAAI,CAkB3B;AAID,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,CAAC,SAAS;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAC5E,OAAO,EAAE,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,EAAE,CAiBlC;AAID,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,gBAAgB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,eAAe,EAAE,CAoChF;AAID,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,qBAAqB,EAAE,GAAG,qBAAqB,EAAE,CAiChG"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Power analysis: duration curves, Critical Power model, Normalized Power.
3
+ *
4
+ * Pure functions with no database or infrastructure dependencies.
5
+ * Shared between web and iOS via @dofek/training.
6
+ */
7
+ // ── Standard durations for power/HR/pace curves ─────────────────────
8
+ export const STANDARD_DURATIONS = [
9
+ 5, 15, 30, 60, 120, 180, 300, 420, 600, 1200, 1800, 3600, 5400, 7200,
10
+ ];
11
+ /** Human-readable labels for standard duration curve durations. */
12
+ export const DURATION_LABELS = {
13
+ 5: "5s",
14
+ 15: "15s",
15
+ 30: "30s",
16
+ 60: "1min",
17
+ 120: "2min",
18
+ 180: "3min",
19
+ 300: "5min",
20
+ 420: "7min",
21
+ 600: "10min",
22
+ 1200: "20min",
23
+ 1800: "30min",
24
+ 3600: "60min",
25
+ 5400: "90min",
26
+ 7200: "120min",
27
+ };
28
+ // ── Linear regression ───────────────────────────────────────────────
29
+ /** Simple linear regression: y = slope * x + intercept */
30
+ export function linearRegression(xs, ys) {
31
+ const pointCount = xs.length;
32
+ const sumX = xs.reduce((a, b) => a + b, 0);
33
+ const sumY = ys.reduce((a, b) => a + b, 0);
34
+ const sumXY = xs.reduce((a, x, i) => a + x * (ys[i] ?? 0), 0);
35
+ const sumX2 = xs.reduce((a, x) => a + x * x, 0);
36
+ const denom = pointCount * sumX2 - sumX * sumX;
37
+ if (denom === 0)
38
+ return { slope: 0, intercept: 0, r2: 0 };
39
+ const slope = (pointCount * sumXY - sumX * sumY) / denom;
40
+ const intercept = (sumY - slope * sumX) / pointCount;
41
+ const yMean = sumY / pointCount;
42
+ const ssTotal = ys.reduce((a, y) => a + (y - yMean) ** 2, 0);
43
+ const ssResidual = ys.reduce((a, y, i) => a + (y - (slope * (xs[i] ?? 0) + intercept)) ** 2, 0);
44
+ const r2 = ssTotal > 0 ? 1 - ssResidual / ssTotal : 0;
45
+ return { slope, intercept, r2 };
46
+ }
47
+ /**
48
+ * Fit Morton's 2-parameter Critical Power model (Monod-Scherrer).
49
+ *
50
+ * Model: P(t) = CP + W'/t
51
+ * Linearized: Work = P*t = CP*t + W'
52
+ * Linear regression of Work vs Time gives slope=CP, intercept=W'.
53
+ *
54
+ * Only uses durations 120–600s. This range avoids both anaerobic-dominated
55
+ * efforts (<120s) and long-duration bests that are suppressed by interval
56
+ * training recovery periods (>600s).
57
+ */
58
+ export function fitCriticalPower(points) {
59
+ const valid = points.filter((p) => p.durationSeconds >= 120 && p.durationSeconds <= 600 && p.bestPower > 0);
60
+ if (valid.length < 3)
61
+ return null;
62
+ const xs = valid.map((p) => p.durationSeconds);
63
+ const ys = valid.map((p) => p.bestPower * p.durationSeconds);
64
+ const { slope: cp, intercept: wPrime, r2 } = linearRegression(xs, ys);
65
+ if (cp <= 0)
66
+ return null;
67
+ return {
68
+ cp: Math.round(cp),
69
+ wPrime: Math.round(wPrime),
70
+ r2: Math.round(r2 * 1000) / 1000,
71
+ };
72
+ }
73
+ /** Group pre-sorted samples by activity_id in a single pass. */
74
+ export function groupByActivity(samples) {
75
+ const groups = [];
76
+ let current = null;
77
+ for (const sample of samples) {
78
+ if (!current || current.rows.at(0)?.activity_id !== sample.activity_id) {
79
+ current = {
80
+ rows: [],
81
+ activityDate: sample.activity_date,
82
+ intervalSeconds: sample.interval_s,
83
+ };
84
+ groups.push(current);
85
+ }
86
+ current.rows.push(sample);
87
+ }
88
+ return groups;
89
+ }
90
+ /**
91
+ * Compute best average power for each standard duration across all activities.
92
+ * Uses prefix sums for O(N × D) performance where N = total samples, D = 14 durations.
93
+ */
94
+ export function computePowerCurve(samples) {
95
+ const activities = groupByActivity(samples);
96
+ const bestPerDuration = new Map();
97
+ for (const { rows, activityDate, intervalSeconds } of activities) {
98
+ const rowCount = rows.length;
99
+ const cumsum = new Float64Array(rowCount + 1);
100
+ for (let i = 0; i < rowCount; i++) {
101
+ cumsum[i + 1] = (cumsum[i] ?? 0) + (rows[i]?.power ?? 0);
102
+ }
103
+ for (const duration of STANDARD_DURATIONS) {
104
+ const windowSize = Math.round(duration / intervalSeconds);
105
+ if (windowSize > rowCount || windowSize < 1)
106
+ continue;
107
+ let maxAvg = 0;
108
+ for (let i = windowSize; i <= rowCount; i++) {
109
+ const avg = ((cumsum[i] ?? 0) - (cumsum[i - windowSize] ?? 0)) / windowSize;
110
+ if (avg > maxAvg)
111
+ maxAvg = avg;
112
+ }
113
+ if (maxAvg > 0) {
114
+ const prev = bestPerDuration.get(duration);
115
+ if (!prev || maxAvg > prev.power) {
116
+ bestPerDuration.set(duration, { power: Math.round(maxAvg), date: activityDate });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return STANDARD_DURATIONS.flatMap((d) => {
122
+ const best = bestPerDuration.get(d);
123
+ if (!best)
124
+ return [];
125
+ return [{ durationSeconds: d, bestPower: best.power, activityDate: best.date }];
126
+ });
127
+ }
128
+ /**
129
+ * Compute Normalized Power per activity using 30-second rolling averages.
130
+ * NP = (mean(rolling_30s_avg^4))^0.25 — accounts for the metabolic cost
131
+ * of variable-intensity efforts.
132
+ */
133
+ export function computeNormalizedPower(samples) {
134
+ const activities = groupByActivity(samples);
135
+ const results = [];
136
+ for (const { rows, activityDate, intervalSeconds } of activities) {
137
+ const windowSize = Math.max(1, Math.round(30 / intervalSeconds));
138
+ const rowCount = rows.length;
139
+ const cumsum = new Float64Array(rowCount + 1);
140
+ for (let i = 0; i < rowCount; i++) {
141
+ cumsum[i + 1] = (cumsum[i] ?? 0) + (rows[i]?.power ?? 0);
142
+ }
143
+ let sum4thPower = 0;
144
+ let count = 0;
145
+ for (let i = windowSize; i <= rowCount; i++) {
146
+ const avg = ((cumsum[i] ?? 0) - (cumsum[i - windowSize] ?? 0)) / windowSize;
147
+ sum4thPower += avg ** 4;
148
+ count++;
149
+ }
150
+ if (count === 0)
151
+ continue;
152
+ const normalizedPower = Math.round((sum4thPower / count) ** 0.25 * 10) / 10;
153
+ results.push({
154
+ activityDate,
155
+ activityName: rows[0]?.activity_name ?? null,
156
+ normalizedPower,
157
+ });
158
+ }
159
+ results.sort((a, b) => a.activityDate.localeCompare(b.activityDate));
160
+ return results;
161
+ }
162
+ //# sourceMappingURL=power-analysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power-analysis.js","sourceRoot":"","sources":["../src/power-analysis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ export interface KarvonenZoneWeek {
2
+ zone0: number;
3
+ zone1: number;
4
+ zone2: number;
5
+ zone3: number;
6
+ zone4: number;
7
+ zone5: number;
8
+ }
9
+ export interface IntensityDistributionZone {
10
+ zone: number;
11
+ label: string;
12
+ seconds: number;
13
+ percent: number;
14
+ }
15
+ export interface IntensityDistribution {
16
+ model: "karvonen-five-zone";
17
+ activityScope: "endurance";
18
+ totalSeconds: number;
19
+ zones: IntensityDistributionZone[];
20
+ explanation: string;
21
+ }
22
+ export type PolarizationStatus = "polarized" | "not_polarized" | "insufficient_data";
23
+ export interface TreffPolarizationWeekInput {
24
+ week: string;
25
+ z1Seconds: number;
26
+ z2Seconds: number;
27
+ z3Seconds: number;
28
+ polarizationIndex: number | null;
29
+ }
30
+ export interface TreffPolarizationWeek extends TreffPolarizationWeekInput {
31
+ totalSeconds: number;
32
+ zonePercentages: {
33
+ z1: number;
34
+ z2: number;
35
+ z3: number;
36
+ };
37
+ status: PolarizationStatus;
38
+ statusLabel: string;
39
+ explanation: string;
40
+ }
41
+ export declare function buildKarvonenIntensityDistribution(weeks: KarvonenZoneWeek[]): IntensityDistribution;
42
+ export declare function buildTreffPolarizationWeek(input: TreffPolarizationWeekInput): TreffPolarizationWeek;
43
+ //# sourceMappingURL=training-distribution.d.ts.map