glutamatt HF Staff commited on
Commit
e2402ea
·
verified ·
1 Parent(s): bef818b
Files changed (1) hide show
  1. src/utils/metricAcwr.ts +145 -41
src/utils/metricAcwr.ts CHANGED
@@ -11,8 +11,8 @@ function formatDateLocal(date: Date): string {
11
  }
12
 
13
  /**
14
- * Calculate optimal activity values for the next 7 days to reach and maintain target ACWR
15
- * Uses a greedy algorithm that optimizes each day to get closer to target ACWR
16
  * @param allDates - Array of all historical dates
17
  * @param dailyValues - Map of date strings to activity values
18
  * @param targetACWR - Target ACWR to achieve
@@ -32,93 +32,197 @@ function calculateOptimalFutureDays(
32
  futureAverage28d: number[];
33
  futureAcwr: number[];
34
  } {
35
- const futureDates: string[] = [];
36
- const futureValues: number[] = [];
37
- const futureAverage7d: number[] = [];
38
- const futureAverage28d: number[] = [];
39
- const futureAcwr: number[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- // Create a mutable copy of daily values to simulate future
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  const simulatedDailyValues = new Map(dailyValues);
43
  const simulatedAllDates = [...allDates];
44
-
45
- // Get the last date in the data
46
  const lastDate = new Date(allDates[allDates.length - 1]);
47
 
48
  for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
49
  const futureDate = new Date(lastDate);
50
- // Add 1 + startOffset + dayOffset to go beyond lastDate
51
- // startOffset=0: start from day after lastDate (tomorrow if lastDate is today)
52
- // startOffset=1: skip one day, start from 2 days after lastDate
53
  futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
54
  const futureDateStr = formatDateLocal(futureDate);
55
 
56
- futureDates.push(futureDateStr);
57
  simulatedAllDates.push(futureDate);
58
-
59
  const currentIndex = simulatedAllDates.length - 1;
60
 
61
- // Calculate what the 7-day sum would be (last 6 days + today)
62
  let acuteSum = 0;
63
  for (let i = Math.max(0, currentIndex - 6); i < currentIndex; i++) {
64
  const checkDateStr = formatDateLocal(simulatedAllDates[i]);
65
  acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
66
  }
67
 
68
- // Calculate what the 28-day sum would be (last 27 days + today)
69
  let chronicSum = 0;
70
  for (let i = Math.max(0, currentIndex - 27); i < currentIndex; i++) {
71
  const checkDateStr = formatDateLocal(simulatedAllDates[i]);
72
  chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
73
  }
74
 
75
- // Calculate optimal value for today using the formula:
76
- // targetACWR = [(acuteSum + X) / 7] / [(chronicSum + X) / 28]
77
- // Solving for X:
78
- // X = (4 * acuteSum - targetACWR * chronicSum) / (targetACWR - 4)
79
-
80
  const numerator = 4 * acuteSum - targetACWR * chronicSum;
81
  const denominator = targetACWR - 4;
82
-
83
  let optimalValue = 0;
84
 
85
  if (Math.abs(denominator) > 0.001) {
86
  optimalValue = numerator / denominator;
87
-
88
- // Ensure non-negative values (can't have negative activity)
89
- if (optimalValue < 0) {
90
- optimalValue = 0;
91
- }
92
  } else {
93
- // Special case: when targetACWR ≈ 4, we need a different approach
94
- // In this case, set to average of recent values to maintain stability
95
- const recentSum = acuteSum;
96
- optimalValue = recentSum / 7;
97
  }
98
 
99
- // Store the optimal value for this future day
100
- futureValues.push(optimalValue);
101
  simulatedDailyValues.set(futureDateStr, optimalValue);
 
102
 
103
- // Calculate the resulting 7-day average (acute load)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  let newAcuteSum = 0;
105
  for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
106
- const checkDateStr = formatDateLocal(simulatedAllDates[i]);
107
- newAcuteSum += simulatedDailyValues.get(checkDateStr) || 0;
108
  }
109
  const newAcuteAvg = newAcuteSum / 7;
110
  futureAverage7d.push(newAcuteAvg);
111
 
112
- // Calculate the resulting 28-day average (chronic load)
113
  let newChronicSum = 0;
114
  for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
115
- const checkDateStr = formatDateLocal(simulatedAllDates[i]);
116
- newChronicSum += simulatedDailyValues.get(checkDateStr) || 0;
117
  }
118
  const newChronicAvg = newChronicSum / 28;
119
  futureAverage28d.push(newChronicAvg);
120
 
121
- // Calculate the resulting ACWR
122
  const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
123
  futureAcwr.push(newACWR);
124
  }
 
11
  }
12
 
13
  /**
14
+ * Calculate optimal activity values for the next 7 days to minimize MSE between ACWR and target
15
+ * Uses optimization to find values that maintain ACWR close to target across all predicted days
16
  * @param allDates - Array of all historical dates
17
  * @param dailyValues - Map of date strings to activity values
18
  * @param targetACWR - Target ACWR to achieve
 
32
  futureAverage28d: number[];
33
  futureAcwr: number[];
34
  } {
35
+ // Variance weight parameter (0-1): controls trade-off between ACWR accuracy and value consistency
36
+ // Higher weight = more preference for consistent daily values
37
+ const varianceWeight = 0.5;
38
+
39
+ // Helper function to calculate variance of predicted values
40
+ const calculateVariance = (values: number[]): number => {
41
+ if (values.length === 0) return 0;
42
+ const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
43
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
44
+ return variance;
45
+ };
46
+
47
+ // Helper function to simulate predictions and calculate error (MSE + variance penalty)
48
+ const simulateAndCalculateError = (values: number[]): { error: number; acwrValues: number[]; variance: number } => {
49
+ const simulatedDailyValues = new Map(dailyValues);
50
+ const simulatedAllDates = [...allDates];
51
+ const lastDate = new Date(allDates[allDates.length - 1]);
52
+ const acwrValues: number[] = [];
53
+
54
+ for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
55
+ const futureDate = new Date(lastDate);
56
+ futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
57
+ const futureDateStr = formatDateLocal(futureDate);
58
+
59
+ simulatedDailyValues.set(futureDateStr, values[dayOffset]);
60
+ simulatedAllDates.push(futureDate);
61
+
62
+ const currentIndex = simulatedAllDates.length - 1;
63
+
64
+ // Calculate 7-day average
65
+ let acuteSum = 0;
66
+ for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
67
+ const checkDateStr = formatDateLocal(simulatedAllDates[i]);
68
+ acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
69
+ }
70
+ const acuteAvg = acuteSum / 7;
71
+
72
+ // Calculate 28-day average
73
+ let chronicSum = 0;
74
+ for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
75
+ const checkDateStr = formatDateLocal(simulatedAllDates[i]);
76
+ chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
77
+ }
78
+ const chronicAvg = chronicSum / 28;
79
+
80
+ const acwr = chronicAvg > 0 ? acuteAvg / chronicAvg : 0;
81
+ acwrValues.push(acwr);
82
+ }
83
 
84
+ // Calculate MSE for ACWR
85
+ let mse = 0;
86
+ for (const acwr of acwrValues) {
87
+ const diff = acwr - targetACWR;
88
+ mse += diff * diff;
89
+ }
90
+ mse /= numFutureDays;
91
+
92
+ // Calculate variance of predicted values
93
+ const variance = calculateVariance(values);
94
+
95
+ // Normalize variance by mean to make it scale-independent
96
+ const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
97
+ const normalizedVariance = mean > 0 ? variance / (mean * mean) : 0;
98
+
99
+ // Combined error: MSE + weighted variance penalty
100
+ const error = mse + varianceWeight * normalizedVariance;
101
+
102
+ return { error, acwrValues, variance };
103
+ };
104
+
105
+ // Initialize with greedy solution as starting point
106
+ const initialValues: number[] = [];
107
  const simulatedDailyValues = new Map(dailyValues);
108
  const simulatedAllDates = [...allDates];
 
 
109
  const lastDate = new Date(allDates[allDates.length - 1]);
110
 
111
  for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
112
  const futureDate = new Date(lastDate);
 
 
 
113
  futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
114
  const futureDateStr = formatDateLocal(futureDate);
115
 
 
116
  simulatedAllDates.push(futureDate);
 
117
  const currentIndex = simulatedAllDates.length - 1;
118
 
119
+ // Calculate sums for greedy solution
120
  let acuteSum = 0;
121
  for (let i = Math.max(0, currentIndex - 6); i < currentIndex; i++) {
122
  const checkDateStr = formatDateLocal(simulatedAllDates[i]);
123
  acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
124
  }
125
 
 
126
  let chronicSum = 0;
127
  for (let i = Math.max(0, currentIndex - 27); i < currentIndex; i++) {
128
  const checkDateStr = formatDateLocal(simulatedAllDates[i]);
129
  chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
130
  }
131
 
132
+ // Greedy optimal value
 
 
 
 
133
  const numerator = 4 * acuteSum - targetACWR * chronicSum;
134
  const denominator = targetACWR - 4;
 
135
  let optimalValue = 0;
136
 
137
  if (Math.abs(denominator) > 0.001) {
138
  optimalValue = numerator / denominator;
139
+ if (optimalValue < 0) optimalValue = 0;
 
 
 
 
140
  } else {
141
+ optimalValue = acuteSum / 7;
 
 
 
142
  }
143
 
144
+ initialValues.push(optimalValue);
 
145
  simulatedDailyValues.set(futureDateStr, optimalValue);
146
+ }
147
 
148
+ // Iterative refinement to minimize error (MSE + variance penalty)
149
+ let bestValues = [...initialValues];
150
+ let bestError = simulateAndCalculateError(bestValues).error;
151
+
152
+ // Simple gradient descent-like optimization
153
+ const iterations = 50;
154
+ const learningRate = 0.3;
155
+
156
+ for (let iter = 0; iter < iterations; iter++) {
157
+ const currentResult = simulateAndCalculateError(bestValues);
158
+
159
+ // Try adjusting each day's value
160
+ for (let dayIdx = 0; dayIdx < numFutureDays; dayIdx++) {
161
+ const originalValue = bestValues[dayIdx];
162
+
163
+ // Calculate gradient by finite difference
164
+ const delta = Math.max(1, originalValue * 0.1);
165
+ const testValues = [...bestValues];
166
+ testValues[dayIdx] = originalValue + delta;
167
+ const upResult = simulateAndCalculateError(testValues);
168
+
169
+ const gradient = (upResult.error - currentResult.error) / delta;
170
+
171
+ // Update value using gradient
172
+ let newValue = originalValue - learningRate * gradient * originalValue;
173
+ newValue = Math.max(0, newValue); // Ensure non-negative
174
+
175
+ // Test if this improves error
176
+ testValues[dayIdx] = newValue;
177
+ const newResult = simulateAndCalculateError(testValues);
178
+
179
+ if (newResult.error < bestError) {
180
+ bestValues[dayIdx] = newValue;
181
+ bestError = newResult.error;
182
+ }
183
+ }
184
+ }
185
+
186
+ // Build final results with optimized values
187
+ const futureDates: string[] = [];
188
+ const futureValues: number[] = bestValues;
189
+ const futureAverage7d: number[] = [];
190
+ const futureAverage28d: number[] = [];
191
+ const futureAcwr: number[] = [];
192
+
193
+ const finalSimulatedDailyValues = new Map(dailyValues);
194
+ const finalSimulatedAllDates = [...allDates];
195
+
196
+ for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
197
+ const futureDate = new Date(lastDate);
198
+ futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
199
+ const futureDateStr = formatDateLocal(futureDate);
200
+
201
+ futureDates.push(futureDateStr);
202
+ finalSimulatedDailyValues.set(futureDateStr, bestValues[dayOffset]);
203
+ finalSimulatedAllDates.push(futureDate);
204
+
205
+ const currentIndex = finalSimulatedAllDates.length - 1;
206
+
207
+ // Calculate 7-day average
208
  let newAcuteSum = 0;
209
  for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
210
+ const checkDateStr = formatDateLocal(finalSimulatedAllDates[i]);
211
+ newAcuteSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
212
  }
213
  const newAcuteAvg = newAcuteSum / 7;
214
  futureAverage7d.push(newAcuteAvg);
215
 
216
+ // Calculate 28-day average
217
  let newChronicSum = 0;
218
  for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
219
+ const checkDateStr = formatDateLocal(finalSimulatedAllDates[i]);
220
+ newChronicSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
221
  }
222
  const newChronicAvg = newChronicSum / 28;
223
  futureAverage28d.push(newChronicAvg);
224
 
225
+ // Calculate ACWR
226
  const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
227
  futureAcwr.push(newACWR);
228
  }