Alternative GAP

John from runningwritings.com has created a custom GAP calculator, based on experimental rather than population data. The actual calculator and logic for the formulas behind it can be found here
I’ve tried to turn this into a custom GAP stream below, but I’m having trouble getting a result out of it:

// Black et al. data arrays
blackGam = {
“speed_m_s”: [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2, 2.05, 2.1, 2.15, 2.2, 2.25, 2.3, 2.35, 2.4, 2.45, 2.5, 2.55, 2.6, 2.65, 2.7, 2.75, 2.8, 2.85, 2.9, 2.95, 3, 3.05, 3.1, 3.15, 3.2, 3.25, 3.3, 3.35, 3.4, 3.45, 3.5, 3.55, 3.6, 3.65, 3.7, 3.75, 3.8, 3.85, 3.9, 3.95, 4, 4.05, 4.1, 4.15, 4.2, 4.25, 4.3, 4.35, 4.4, 4.45, 4.5, 4.55, 4.6, 4.65, 4.7, 4.75, 4.8, 4.85, 4.9, 4.95, 5, 5.05, 5.1, 5.15, 5.2, 5.25, 5.3, 5.35, 5.4, 5.45, 5.5, 5.55, 5.6, 5.65, 5.7, 5.75, 5.8, 5.85, 5.9, 5.95, 6, 6.05, 6.1, 6.15, 6.2, 6.25, 6.3, 6.35, 6.4, 6.45, 6.5, 6.55, 6.6, 6.65, 6.7, 6.75, 6.8, 6.85, 6.9, 6.95, 7, 7.05, 7.1, 7.15, 7.2, 7.25, 7.3, 7.35, 7.4, 7.45, 7.5, 7.55, 7.6, 7.65, 7.7, 7.75, 7.8, 7.85, 7.9, 7.95, 8, 8.05, 8.1, 8.15, 8.2, 8.25, 8.3, 8.35, 8.4, 8.45, 8.5, 8.55, 8.6, 8.65, 8.7, 8.75, 8.8, 8.85, 8.9, 8.95, 9, 9.05, 9.1, 9.15, 9.2, 9.25, 9.3, 9.35, 9.4, 9.45, 9.5, 9.55, 9.6, 9.65, 9.7, 9.75, 9.8, 9.85, 9.9, 9.95, 10],
“energy_j_kg_m”: [6.0976, 6.0592, 6.0208, 5.9824, 5.944, 5.9056, 5.8672, 5.8289, 5.7905, 5.7521, 5.7137, 5.6753, 5.6369, 5.5985, 5.5601, 5.5217, 5.4833, 5.4449, 5.4066, 5.3682, 5.3298, 5.2914, 5.253, 5.2146, 5.1762, 5.1378, 5.0994, 5.061, 5.0227, 4.9843, 4.9459, 4.9075, 4.8691, 4.8307, 4.7923, 4.7539, 4.7155, 4.6771, 4.6387, 4.6004, 4.562, 4.5236, 4.4852, 4.4468, 4.4084, 4.37, 4.3317, 4.2936, 4.2559, 4.2187, 4.1821, 4.1463, 4.1115, 4.0777, 4.0451, 4.0139, 3.9841, 3.956, 3.9297, 3.9053, 3.883, 3.8628, 3.845, 3.8294, 3.816, 3.8046, 3.795, 3.7872, 3.7811, 3.7764, 3.7732, 3.7713, 3.7704, 3.7707, 3.7718, 3.7737, 3.7763, 3.7794, 3.783, 3.7868, 3.791, 3.7955, 3.8002, 3.8051, 3.8103, 3.8157, 3.8213, 3.827, 3.8329, 3.8389, 3.845, 3.8512, 3.8575, 3.8638, 3.8701, 3.8765, 3.8828, 3.8892, 3.8955, 3.9019, 3.9082, 3.9146, 3.9209, 3.9273, 3.9336, 3.94, 3.9463, 3.9527, 3.959, 3.9654, 3.9717, 3.9781, 3.9844, 3.9908, 3.9971, 4.0035, 4.0098, 4.0162, 4.0225, 4.0289, 4.0352, 4.0416, 4.0479, 4.0543, 4.0606, 4.067, 4.0733, 4.0797, 4.086, 4.0924, 4.0987, 4.1051, 4.1114, 4.1178, 4.1241, 4.1305, 4.1368, 4.1432, 4.1495, 4.1559, 4.1622, 4.1686, 4.1749, 4.1813, 4.1876, 4.194, 4.2003, 4.2067, 4.213, 4.2194, 4.2257, 4.2321, 4.2384, 4.2448, 4.2511, 4.2575, 4.2638, 4.2702, 4.2765, 4.2829, 4.2892, 4.2956, 4.3019, 4.3083, 4.3146, 4.321, 4.3273, 4.3337, 4.34, 4.3464, 4.3527, 4.3591, 4.3654, 4.3718, 4.3781, 4.3845, 4.3908, 4.3972, 4.4035, 4.4099, 4.4162, 4.4226, 4.4289, 4.4353, 4.4416, 4.448, 4.4543, 4.4607, 4.467, 4.4734, 4.4797, 4.4861, 4.4924, 4.4988, 4.5051, 4.5115, 4.5178, 4.5242, 4.5305, 4.5369, 4.5432],
“energy_j_kg_s”: [0, 0.303, 0.6021, 0.8974, 1.1888, 1.4764, 1.7602, 2.0401, 2.3162, 2.5884, 2.8568, 3.1214, 3.3821, 3.639, 3.8921, 4.1413, 4.3867, 4.6282, 4.8659, 5.0998, 5.3298, 5.556, 5.7783, 5.9968, 6.2115, 6.4223, 6.6293, 6.8324, 7.0317, 7.2272, 7.4188, 7.6066, 7.7905, 7.9707, 8.1469, 8.3194, 8.488, 8.6527, 8.8136, 8.9707, 9.1239, 9.2733, 9.4189, 9.5606, 9.6985, 9.8325, 9.9629, 10.09, 10.2142, 10.3358, 10.4553, 10.5731, 10.6898, 10.8058, 10.9217, 11.0381, 11.1556, 11.2747, 11.3962, 11.5207, 11.6489, 11.7817, 11.9196, 12.0628, 12.2112, 12.3648, 12.5235, 12.6872, 12.8557, 13.0287, 13.2062, 13.388, 13.5736, 13.763, 13.9557, 14.1515, 14.3499, 14.5508, 14.7536, 14.958, 15.1641, 15.3717, 15.5808, 15.7914, 16.0034, 16.2168, 16.4315, 16.6475, 16.8647, 17.0831, 17.3026, 17.523, 17.7444, 17.9666, 18.1896, 18.4133, 18.6376, 18.8625, 19.0881, 19.3143, 19.5411, 19.7686, 19.9967, 20.2255, 20.4549, 20.6849, 20.9155, 21.1468, 21.3788, 21.6113, 21.8445, 22.0783, 22.3128, 22.5479, 22.7837, 23.02, 23.257, 23.4947, 23.7329, 23.9719, 24.2114, 24.4516, 24.6924, 24.9338, 25.1759, 25.4186, 25.662, 25.906, 26.1506, 26.3959, 26.6418, 26.8883, 27.1355, 27.3833, 27.6317, 27.8808, 28.1305, 28.3808, 28.6318, 28.8834, 29.1357, 29.3885, 29.6421, 29.8962, 30.151, 30.4064, 30.6625, 30.9192, 31.1765, 31.4344, 31.693, 31.9523, 32.2121, 32.4726, 32.7338, 32.9955, 33.2579, 33.521, 33.7847, 34.049, 34.3139, 34.5795, 34.8457, 35.1126, 35.3801, 35.6482, 35.9169, 36.1863, 36.4563, 36.727, 36.9983, 37.2702, 37.5428, 37.816, 38.0898, 38.3643, 38.6394, 38.9152, 39.1915, 39.4685, 39.7462, 40.0245, 40.3034, 40.5829, 40.8631, 41.1439, 41.4254, 41.7075, 41.9902, 42.2736, 42.5576, 42.8422, 43.1275, 43.4134, 43.6999, 43.9871, 44.2749, 44.5633, 44.8524, 45.1421, 45.4325]
};
// ==== Lookup/interpolation helper ====
function lookupSpeed(x, col_name) {
const speed = blackGam.speed_m_s;
const energy = blackGam[col_name];
if (x < speed[0] || x > speed[speed.length - 1]) return NaN;
let i = 0;
for (; i < speed.length - 1; i++) {
if (x >= speed[i] && x <= speed[i + 1]) break;
}
// Linear interpolation
return energy[i] + (energy[i+1] - energy[i]) * ((x - speed[i]) / (speed[i+1] - speed[i]));
}
// ==== Minetti 2002 quintic polynomial for (change in) cost ====
function delta_Cr(g) {
return (
155.4 * Math.pow(g, 5)
- 30.4 * Math.pow(g, 4)
- 43.3 * Math.pow(g, 3)
+ 46.3 * Math.pow(g, 2)
+ 19.5 * g
); // g as decimal grade
}
// ==== Find equivalent flat speed for given metabolic power ====
function getEquivFlatSpeed(W_kg) {
const speed = blackGam.speed_m_s;
const met_power = blackGam[‘energy_j_kg_s’];
if (W_kg < met_power[0] || W_kg > met_power[met_power.length - 1]) return NaN;
let i = 0;
for (; i < met_power.length - 1; i++) {
if (W_kg >= met_power[i] && W_kg <= met_power[i + 1]) break;
}
// Linear interpolation
return speed[i] + (speed[i+1] - speed[i]) * ((W_kg - met_power[i]) / (met_power[i+1] - met_power[i]));
}
// ==== Main stream code ====
grade_arr = icu.streams.grade_smooth; // percent, array
v_arr = icu.streams.velocity_smooth; // m/s, array
data = new Array(v_arr.length);
for (let i = 0; i < data.length; i++) {
let g = grade_arr[i] / 100; // decimal
let v = v_arr[i];
let Cr_flat = lookupSpeed(v, “energy_j_kg_m”); // table lookup
let deltaCr = delta_Cr(g); // Minetti addition
let totalCr = Cr_flat + deltaCr; // total J/kg/m
let P = totalCr * v; // W/kg (J/kg/s)
data[i] = getEquivFlatSpeed(P); // result: eq. flat speed (m/s)
}

1 Like

Try this:

// Black et al. data arrays
blackGam = {
"speed_m_s": [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2, 2.05, 2.1, 2.15, 2.2, 2.25, 2.3, 2.35, 2.4, 2.45, 2.5, 2.55, 2.6, 2.65, 2.7, 2.75, 2.8, 2.85, 2.9, 2.95, 3, 3.05, 3.1, 3.15, 3.2, 3.25, 3.3, 3.35, 3.4, 3.45, 3.5, 3.55, 3.6, 3.65, 3.7, 3.75, 3.8, 3.85, 3.9, 3.95, 4, 4.05, 4.1, 4.15, 4.2, 4.25, 4.3, 4.35, 4.4, 4.45, 4.5, 4.55, 4.6, 4.65, 4.7, 4.75, 4.8, 4.85, 4.9, 4.95, 5, 5.05, 5.1, 5.15, 5.2, 5.25, 5.3, 5.35, 5.4, 5.45, 5.5, 5.55, 5.6, 5.65, 5.7, 5.75, 5.8, 5.85, 5.9, 5.95, 6, 6.05, 6.1, 6.15, 6.2, 6.25, 6.3, 6.35, 6.4, 6.45, 6.5, 6.55, 6.6, 6.65, 6.7, 6.75, 6.8, 6.85, 6.9, 6.95, 7, 7.05, 7.1, 7.15, 7.2, 7.25, 7.3, 7.35, 7.4, 7.45, 7.5, 7.55, 7.6, 7.65, 7.7, 7.75, 7.8, 7.85, 7.9, 7.95, 8, 8.05, 8.1, 8.15, 8.2, 8.25, 8.3, 8.35, 8.4, 8.45, 8.5, 8.55, 8.6, 8.65, 8.7, 8.75, 8.8, 8.85, 8.9, 8.95, 9, 9.05, 9.1, 9.15, 9.2, 9.25, 9.3, 9.35, 9.4, 9.45, 9.5, 9.55, 9.6, 9.65, 9.7, 9.75, 9.8, 9.85, 9.9, 9.95, 10],
"energy_j_kg_m": [6.0976, 6.0592, 6.0208, 5.9824, 5.944, 5.9056, 5.8672, 5.8289, 5.7905, 5.7521, 5.7137, 5.6753, 5.6369, 5.5985, 5.5601, 5.5217, 5.4833, 5.4449, 5.4066, 5.3682, 5.3298, 5.2914, 5.253, 5.2146, 5.1762, 5.1378, 5.0994, 5.061, 5.0227, 4.9843, 4.9459, 4.9075, 4.8691, 4.8307, 4.7923, 4.7539, 4.7155, 4.6771, 4.6387, 4.6004, 4.562, 4.5236, 4.4852, 4.4468, 4.4084, 4.37, 4.3317, 4.2936, 4.2559, 4.2187, 4.1821, 4.1463, 4.1115, 4.0777, 4.0451, 4.0139, 3.9841, 3.956, 3.9297, 3.9053, 3.883, 3.8628, 3.845, 3.8294, 3.816, 3.8046, 3.795, 3.7872, 3.7811, 3.7764, 3.7732, 3.7713, 3.7704, 3.7707, 3.7718, 3.7737, 3.7763, 3.7794, 3.783, 3.7868, 3.791, 3.7955, 3.8002, 3.8051, 3.8103, 3.8157, 3.8213, 3.827, 3.8329, 3.8389, 3.845, 3.8512, 3.8575, 3.8638, 3.8701, 3.8765, 3.8828, 3.8892, 3.8955, 3.9019, 3.9082, 3.9146, 3.9209, 3.9273, 3.9336, 3.94, 3.9463, 3.9527, 3.959, 3.9654, 3.9717, 3.9781, 3.9844, 3.9908, 3.9971, 4.0035, 4.0098, 4.0162, 4.0225, 4.0289, 4.0352, 4.0416, 4.0479, 4.0543, 4.0606, 4.067, 4.0733, 4.0797, 4.086, 4.0924, 4.0987, 4.1051, 4.1114, 4.1178, 4.1241, 4.1305, 4.1368, 4.1432, 4.1495, 4.1559, 4.1622, 4.1686, 4.1749, 4.1813, 4.1876, 4.194, 4.2003, 4.2067, 4.213, 4.2194, 4.2257, 4.2321, 4.2384, 4.2448, 4.2511, 4.2575, 4.2638, 4.2702, 4.2765, 4.2829, 4.2892, 4.2956, 4.3019, 4.3083, 4.3146, 4.321, 4.3273, 4.3337, 4.34, 4.3464, 4.3527, 4.3591, 4.3654, 4.3718, 4.3781, 4.3845, 4.3908, 4.3972, 4.4035, 4.4099, 4.4162, 4.4226, 4.4289, 4.4353, 4.4416, 4.448, 4.4543, 4.4607, 4.467, 4.4734, 4.4797, 4.4861, 4.4924, 4.4988, 4.5051, 4.5115, 4.5178, 4.5242, 4.5305, 4.5369, 4.5432],
"energy_j_kg_s": [0, 0.303, 0.6021, 0.8974, 1.1888, 1.4764, 1.7602, 2.0401, 2.3162, 2.5884, 2.8568, 3.1214, 3.3821, 3.639, 3.8921, 4.1413, 4.3867, 4.6282, 4.8659, 5.0998, 5.3298, 5.556, 5.7783, 5.9968, 6.2115, 6.4223, 6.6293, 6.8324, 7.0317, 7.2272, 7.4188, 7.6066, 7.7905, 7.9707, 8.1469, 8.3194, 8.488, 8.6527, 8.8136, 8.9707, 9.1239, 9.2733, 9.4189, 9.5606, 9.6985, 9.8325, 9.9629, 10.09, 10.2142, 10.3358, 10.4553, 10.5731, 10.6898, 10.8058, 10.9217, 11.0381, 11.1556, 11.2747, 11.3962, 11.5207, 11.6489, 11.7817, 11.9196, 12.0628, 12.2112, 12.3648, 12.5235, 12.6872, 12.8557, 13.0287, 13.2062, 13.388, 13.5736, 13.763, 13.9557, 14.1515, 14.3499, 14.5508, 14.7536, 14.958, 15.1641, 15.3717, 15.5808, 15.7914, 16.0034, 16.2168, 16.4315, 16.6475, 16.8647, 17.0831, 17.3026, 17.523, 17.7444, 17.9666, 18.1896, 18.4133, 18.6376, 18.8625, 19.0881, 19.3143, 19.5411, 19.7686, 19.9967, 20.2255, 20.4549, 20.6849, 20.9155, 21.1468, 21.3788, 21.6113, 21.8445, 22.0783, 22.3128, 22.5479, 22.7837, 23.02, 23.257, 23.4947, 23.7329, 23.9719, 24.2114, 24.4516, 24.6924, 24.9338, 25.1759, 25.4186, 25.662, 25.906, 26.1506, 26.3959, 26.6418, 26.8883, 27.1355, 27.3833, 27.6317, 27.8808, 28.1305, 28.3808, 28.6318, 28.8834, 29.1357, 29.3885, 29.6421, 29.8962, 30.151, 30.4064, 30.6625, 30.9192, 31.1765, 31.4344, 31.693, 31.9523, 32.2121, 32.4726, 32.7338, 32.9955, 33.2579, 33.521, 33.7847, 34.049, 34.3139, 34.5795, 34.8457, 35.1126, 35.3801, 35.6482, 35.9169, 36.1863, 36.4563, 36.727, 36.9983, 37.2702, 37.5428, 37.816, 38.0898, 38.3643, 38.6394, 38.9152, 39.1915, 39.4685, 39.7462, 40.0245, 40.3034, 40.5829, 40.8631, 41.1439, 41.4254, 41.7075, 41.9902, 42.2736, 42.5576, 42.8422, 43.1275, 43.4134, 43.6999, 43.9871, 44.2749, 44.5633, 44.8524, 45.1421, 45.4325]
};
// ==== Lookup/interpolation helper ====
function lookupSpeed(x, col_name) {
const speed = blackGam.speed_m_s;
const energy = blackGam[col_name];
if (x < speed[0] || x > speed[speed.length - 1]) return NaN;
let i = 0;
for (; i < speed.length - 1; i++) {
if (x >= speed[i] && x <= speed[i + 1]) break;
}
// Linear interpolation
return energy[i] + (energy[i+1] - energy[i]) * ((x - speed[i]) / (speed[i+1] - speed[i]));
}
// ==== Minetti 2002 quintic polynomial for (change in) cost ====
function delta_Cr(g) {
return (
155.4 * Math.pow(g, 5)
- 30.4 * Math.pow(g, 4)
- 43.3 * Math.pow(g, 3)
+ 46.3 * Math.pow(g, 2)
+ 19.5 * g
); // g as decimal grade
}
// ==== Find equivalent flat speed for given metabolic power ====
function getEquivFlatSpeed(W_kg) {
const speed = blackGam.speed_m_s;
const met_power = blackGam["energy_j_kg_s"];
if (W_kg < met_power[0] || W_kg > met_power[met_power.length - 1]) return NaN;
let i = 0;
for (; i < met_power.length - 1; i++) {
if (W_kg >= met_power[i] && W_kg <= met_power[i + 1]) break;
}
// Linear interpolation
return speed[i] + (speed[i+1] - speed[i]) * ((W_kg - met_power[i]) / (met_power[i+1] - met_power[i]));
}
// ==== Main stream code ====
grade_arr = icu.streams.grade_smooth; // percent, array
v_arr = icu.streams.velocity_smooth; // m/s, array

for (let i = 0; i < data.length; i++) {
let g = grade_arr[i] / 100; // decimal
let v = v_arr[i];
let Cr_flat = lookupSpeed(v, "energy_j_kg_m"); // table lookup
let deltaCr = delta_Cr(g); // Minetti addition
let totalCr = Cr_flat + deltaCr; // total J/kg/m
let P = totalCr * v; // W/kg (J/kg/s)
data.setAt(i, getEquivFlatSpeed(P)); // result: eq. flat speed (m/s)
}

1 Like

A comparation:


Power from Stryd.

Thank you.

1 Like

Can you share the Custom stream which calculates that Alternative GAP?

It is R2Tom above custom stream code, i dont want to share it in my name.

I made it public as „John Davis GAP“

2 Likes

Thank you!

First impression is that it closely ressembles Coros Effort Pace. Which is also mentioned on the links provided by @pepe .

1 Like

Thank you for fixing my code, it now works, but I am getting some unexpected results

(not really sure how to share this activity)

Might need to apply a calcmovingavg to velocity smooth (over 18 to 20s) as the grade is averaged out over 50m based on coordinates and fixed altitude (according to this)
Or perhaps I need to replace the smooth_grade stream with a custom calculated grade stream. I may be getting odd results as my fit files take speed and distance from my stryd footpod, not GPS

Will experiment with these options when I have time in a few day’s time

I struggle to even get intervals generated GAP and this custom GAP on the same graph, as the first is a “pace” and the second is a “velocity” and when I change the units of the custom stream of R2Tom’s code to pace, it gives odd results

1 Like

Try


works for me.

Thanks Pepe,
I tried this and run into the same problem, around having the charts on the same axis.
Say I chose m/s as the units for this chart, then moved pace onto m/s, one of the curves would get flattened. The same happens if I chose “pace” as the units for this curve. Right now I just have them on separate axes, but it would be handy to spot when they are equal, if they were on the same axis.

I’ve tried to code in my (AI generated) non-smooth grade stream.
So far, I’ve found that on activities with no pauses, this custom GAP curve “lags” a small bit, but the spikes in this curve actually match the power curve much better than the default GAP. (Black is this custom curve, pink GAP, blue pace, purple power)

But on activities with pauses, this stream falls ahead of the “real” pace/power stream by a fair amount (up to 700m or 3.5mins) - note the spike. Must be the way the system interacts with the way I have set it up to calculate non-smooth grade
Aside from this issue, I am fairly happy with the way this GAP curve is performing

//Black et al. data arrays
blackGam = {
"speed_m_s": [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2, 2.05, 2.1, 2.15, 2.2, 2.25, 2.3, 2.35, 2.4, 2.45, 2.5, 2.55, 2.6, 2.65, 2.7, 2.75, 2.8, 2.85, 2.9, 2.95, 3, 3.05, 3.1, 3.15, 3.2, 3.25, 3.3, 3.35, 3.4, 3.45, 3.5, 3.55, 3.6, 3.65, 3.7, 3.75, 3.8, 3.85, 3.9, 3.95, 4, 4.05, 4.1, 4.15, 4.2, 4.25, 4.3, 4.35, 4.4, 4.45, 4.5, 4.55, 4.6, 4.65, 4.7, 4.75, 4.8, 4.85, 4.9, 4.95, 5, 5.05, 5.1, 5.15, 5.2, 5.25, 5.3, 5.35, 5.4, 5.45, 5.5, 5.55, 5.6, 5.65, 5.7, 5.75, 5.8, 5.85, 5.9, 5.95, 6, 6.05, 6.1, 6.15, 6.2, 6.25, 6.3, 6.35, 6.4, 6.45, 6.5, 6.55, 6.6, 6.65, 6.7, 6.75, 6.8, 6.85, 6.9, 6.95, 7, 7.05, 7.1, 7.15, 7.2, 7.25, 7.3, 7.35, 7.4, 7.45, 7.5, 7.55, 7.6, 7.65, 7.7, 7.75, 7.8, 7.85, 7.9, 7.95, 8, 8.05, 8.1, 8.15, 8.2, 8.25, 8.3, 8.35, 8.4, 8.45, 8.5, 8.55, 8.6, 8.65, 8.7, 8.75, 8.8, 8.85, 8.9, 8.95, 9, 9.05, 9.1, 9.15, 9.2, 9.25, 9.3, 9.35, 9.4, 9.45, 9.5, 9.55, 9.6, 9.65, 9.7, 9.75, 9.8, 9.85, 9.9, 9.95, 10],
"energy_j_kg_m": [6.0976, 6.0592, 6.0208, 5.9824, 5.944, 5.9056, 5.8672, 5.8289, 5.7905, 5.7521, 5.7137, 5.6753, 5.6369, 5.5985, 5.5601, 5.5217, 5.4833, 5.4449, 5.4066, 5.3682, 5.3298, 5.2914, 5.253, 5.2146, 5.1762, 5.1378, 5.0994, 5.061, 5.0227, 4.9843, 4.9459, 4.9075, 4.8691, 4.8307, 4.7923, 4.7539, 4.7155, 4.6771, 4.6387, 4.6004, 4.562, 4.5236, 4.4852, 4.4468, 4.4084, 4.37, 4.3317, 4.2936, 4.2559, 4.2187, 4.1821, 4.1463, 4.1115, 4.0777, 4.0451, 4.0139, 3.9841, 3.956, 3.9297, 3.9053, 3.883, 3.8628, 3.845, 3.8294, 3.816, 3.8046, 3.795, 3.7872, 3.7811, 3.7764, 3.7732, 3.7713, 3.7704, 3.7707, 3.7718, 3.7737, 3.7763, 3.7794, 3.783, 3.7868, 3.791, 3.7955, 3.8002, 3.8051, 3.8103, 3.8157, 3.8213, 3.827, 3.8329, 3.8389, 3.845, 3.8512, 3.8575, 3.8638, 3.8701, 3.8765, 3.8828, 3.8892, 3.8955, 3.9019, 3.9082, 3.9146, 3.9209, 3.9273, 3.9336, 3.94, 3.9463, 3.9527, 3.959, 3.9654, 3.9717, 3.9781, 3.9844, 3.9908, 3.9971, 4.0035, 4.0098, 4.0162, 4.0225, 4.0289, 4.0352, 4.0416, 4.0479, 4.0543, 4.0606, 4.067, 4.0733, 4.0797, 4.086, 4.0924, 4.0987, 4.1051, 4.1114, 4.1178, 4.1241, 4.1305, 4.1368, 4.1432, 4.1495, 4.1559, 4.1622, 4.1686, 4.1749, 4.1813, 4.1876, 4.194, 4.2003, 4.2067, 4.213, 4.2194, 4.2257, 4.2321, 4.2384, 4.2448, 4.2511, 4.2575, 4.2638, 4.2702, 4.2765, 4.2829, 4.2892, 4.2956, 4.3019, 4.3083, 4.3146, 4.321, 4.3273, 4.3337, 4.34, 4.3464, 4.3527, 4.3591, 4.3654, 4.3718, 4.3781, 4.3845, 4.3908, 4.3972, 4.4035, 4.4099, 4.4162, 4.4226, 4.4289, 4.4353, 4.4416, 4.448, 4.4543, 4.4607, 4.467, 4.4734, 4.4797, 4.4861, 4.4924, 4.4988, 4.5051, 4.5115, 4.5178, 4.5242, 4.5305, 4.5369, 4.5432],
"energy_j_kg_s": [0, 0.303, 0.6021, 0.8974, 1.1888, 1.4764, 1.7602, 2.0401, 2.3162, 2.5884, 2.8568, 3.1214, 3.3821, 3.639, 3.8921, 4.1413, 4.3867, 4.6282, 4.8659, 5.0998, 5.3298, 5.556, 5.7783, 5.9968, 6.2115, 6.4223, 6.6293, 6.8324, 7.0317, 7.2272, 7.4188, 7.6066, 7.7905, 7.9707, 8.1469, 8.3194, 8.488, 8.6527, 8.8136, 8.9707, 9.1239, 9.2733, 9.4189, 9.5606, 9.6985, 9.8325, 9.9629, 10.09, 10.2142, 10.3358, 10.4553, 10.5731, 10.6898, 10.8058, 10.9217, 11.0381, 11.1556, 11.2747, 11.3962, 11.5207, 11.6489, 11.7817, 11.9196, 12.0628, 12.2112, 12.3648, 12.5235, 12.6872, 12.8557, 13.0287, 13.2062, 13.388, 13.5736, 13.763, 13.9557, 14.1515, 14.3499, 14.5508, 14.7536, 14.958, 15.1641, 15.3717, 15.5808, 15.7914, 16.0034, 16.2168, 16.4315, 16.6475, 16.8647, 17.0831, 17.3026, 17.523, 17.7444, 17.9666, 18.1896, 18.4133, 18.6376, 18.8625, 19.0881, 19.3143, 19.5411, 19.7686, 19.9967, 20.2255, 20.4549, 20.6849, 20.9155, 21.1468, 21.3788, 21.6113, 21.8445, 22.0783, 22.3128, 22.5479, 22.7837, 23.02, 23.257, 23.4947, 23.7329, 23.9719, 24.2114, 24.4516, 24.6924, 24.9338, 25.1759, 25.4186, 25.662, 25.906, 26.1506, 26.3959, 26.6418, 26.8883, 27.1355, 27.3833, 27.6317, 27.8808, 28.1305, 28.3808, 28.6318, 28.8834, 29.1357, 29.3885, 29.6421, 29.8962, 30.151, 30.4064, 30.6625, 30.9192, 31.1765, 31.4344, 31.693, 31.9523, 32.2121, 32.4726, 32.7338, 32.9955, 33.2579, 33.521, 33.7847, 34.049, 34.3139, 34.5795, 34.8457, 35.1126, 35.3801, 35.6482, 35.9169, 36.1863, 36.4563, 36.727, 36.9983, 37.2702, 37.5428, 37.816, 38.0898, 38.3643, 38.6394, 38.9152, 39.1915, 39.4685, 39.7462, 40.0245, 40.3034, 40.5829, 40.8631, 41.1439, 41.4254, 41.7075, 41.9902, 42.2736, 42.5576, 42.8422, 43.1275, 43.4134, 43.6999, 43.9871, 44.2749, 44.5633, 44.8524, 45.1421, 45.4325]
};
// ==== Lookup/interpolation helper ====
function lookupSpeed(x, col_name) {
const speed = blackGam.speed_m_s;
const energy = blackGam[col_name];
if (x < speed[0] || x > speed[speed.length - 1]) return NaN;
let i = 0;
for (; i < speed.length - 1; i++) {
if (x >= speed[i] && x <= speed[i + 1]) break;
}
// Linear interpolation
return energy[i] + (energy[i+1] - energy[i]) * ((x - speed[i]) / (speed[i+1] - speed[i]));
}
// ==== Minetti 2002 quintic polynomial for (change in) cost ====
function delta_Cr(g) {
return (
155.4 * Math.pow(g, 5) - 30.4 * Math.pow(g, 4) - 43.3 * Math.pow(g, 3) + 46.3 * Math.pow(g, 2) + 19.5 * g
); // g as decimal grade
}
// ==== Find equivalent flat speed for given metabolic power ====
function getEquivFlatSpeed(W_kg) {
const speed = blackGam.speed_m_s;
const met_power = blackGam["energy_j_kg_s"];
if (W_kg < met_power[0] || W_kg > met_power[met_power.length - 1]) return NaN;
let i = 0;
for (; i < met_power.length - 1; i++) {
if (W_kg >= met_power[i] && W_kg <= met_power[i + 1]) break;
}
// Linear interpolation
return speed[i] + (speed[i+1] - speed[i]) * ((W_kg - met_power[i]) / (met_power[i+1] - met_power[i]));
}
// ==== Main stream code ====
alt_arr = icu.streams.altitude; // meters; ideally barometric or corrected
dist_arr = icu.streams.distance; // meters, cumulative distance
v_arr = icu.streams.velocity_smooth; // m/s, array
const window = 5; // Calculate moving average of grade over a specified window of points
for (let i = 0; i < data.length; i++) {
    let g = 0;
    if (i >= window) {
        let start_idx = i - window;
        let dist_diff = dist_arr[i] - dist_arr[start_idx];
        let alt_diff = alt_arr[i] - alt_arr[start_idx];
        g = dist_diff > 0 ? (alt_diff / dist_diff) : 0;
    }
let v = v_arr[i];
let Cr_flat = lookupSpeed(v, "energy_j_kg_m"); // table lookup
let deltaCr = delta_Cr(g); // Minetti addition
let totalCr = Cr_flat + deltaCr; // total J/kg/m
let P = totalCr * v; // W/kg (J/kg/s)
data.setAt(i, getEquivFlatSpeed(P)); // result: eq. flat speed (m/s)
}
1 Like

How do you get all the GAP graphs and stream superposed?

Theyare not ‘exactly’ superposed because both Custom streams (Coros Effort Pace and John Davis GAP) are plotted as speed in m/s but numerically displayed as pace. While Pace and GAP are treated and displayed as pace in min/km.
Pace display needs a cutoff because else you get invalid values by dividing by 0. That messes with the lowest speed plotting. The others are in speed and have a minimal value of 0 m/sec.
That’s why you need to plot them on different Y-axes.
For some reason, unknown to me, if you put both axes on a minimum value of ~ 0.9 m/s, they are very close. At least for my avg hiking speed…
Not sure if @david can figure out how to solve that.

2 Likes

Thanks!

Thats great Justin.
Can you create a ''Stryd" GAP Pace using Stryd Power / ECOR and then Black and Minetti’s maths?

I did a shortcut:

{
// Calculation for Stryd Equivalent GAP Pace
let weight = activity.StrydWeight // Or change to Stryd weight in kg
power= icu.streams.fixed_watts;
factor=0.98 // Change based on your needs

for (let i = 0; i < data.length; i++)
data[i] = power[i] ? power[i] / weight * factor:0
}

Great success!
I’ve experimented with my data and this gives me values which look correct when zoomed in on a graph.

  • I’ve set the window for calculating elevation at 8 (seconds), whereas the default intervals GAP was 50m (~18s when running at 6min/km pace)
  • I’ve set the calculation of gradient to be off d(altitude)/d(distance), rather than off GPS coordinates
  • I’ve set a moving average of 10s within the code itself in the last line, but for graphing purposes, I actually apply a further moving average of 15s on top.

I’ve shared it as a custom stream titled GAP (RW)
Thank you to everyone for your help, credit for this tool really goes to John Davis of Running Writings

Ironically, the best way for me to compare values with Intervals.icu default GAP was actually to clone the ga_velocity stream to a separate stream with

gap = icu.streams.ga_velocity
for (let i = 0; i < data.length; i++) {
data[i] = gap[i] ? gap[i] : 0
}

//Black et al. data arrays
blackGam = {
"speed_m_s": [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2, 2.05, 2.1, 2.15, 2.2, 2.25, 2.3, 2.35, 2.4, 2.45, 2.5, 2.55, 2.6, 2.65, 2.7, 2.75, 2.8, 2.85, 2.9, 2.95, 3, 3.05, 3.1, 3.15, 3.2, 3.25, 3.3, 3.35, 3.4, 3.45, 3.5, 3.55, 3.6, 3.65, 3.7, 3.75, 3.8, 3.85, 3.9, 3.95, 4, 4.05, 4.1, 4.15, 4.2, 4.25, 4.3, 4.35, 4.4, 4.45, 4.5, 4.55, 4.6, 4.65, 4.7, 4.75, 4.8, 4.85, 4.9, 4.95, 5, 5.05, 5.1, 5.15, 5.2, 5.25, 5.3, 5.35, 5.4, 5.45, 5.5, 5.55, 5.6, 5.65, 5.7, 5.75, 5.8, 5.85, 5.9, 5.95, 6, 6.05, 6.1, 6.15, 6.2, 6.25, 6.3, 6.35, 6.4, 6.45, 6.5, 6.55, 6.6, 6.65, 6.7, 6.75, 6.8, 6.85, 6.9, 6.95, 7, 7.05, 7.1, 7.15, 7.2, 7.25, 7.3, 7.35, 7.4, 7.45, 7.5, 7.55, 7.6, 7.65, 7.7, 7.75, 7.8, 7.85, 7.9, 7.95, 8, 8.05, 8.1, 8.15, 8.2, 8.25, 8.3, 8.35, 8.4, 8.45, 8.5, 8.55, 8.6, 8.65, 8.7, 8.75, 8.8, 8.85, 8.9, 8.95, 9, 9.05, 9.1, 9.15, 9.2, 9.25, 9.3, 9.35, 9.4, 9.45, 9.5, 9.55, 9.6, 9.65, 9.7, 9.75, 9.8, 9.85, 9.9, 9.95, 10],
"energy_j_kg_m": [6.0976, 6.0592, 6.0208, 5.9824, 5.944, 5.9056, 5.8672, 5.8289, 5.7905, 5.7521, 5.7137, 5.6753, 5.6369, 5.5985, 5.5601, 5.5217, 5.4833, 5.4449, 5.4066, 5.3682, 5.3298, 5.2914, 5.253, 5.2146, 5.1762, 5.1378, 5.0994, 5.061, 5.0227, 4.9843, 4.9459, 4.9075, 4.8691, 4.8307, 4.7923, 4.7539, 4.7155, 4.6771, 4.6387, 4.6004, 4.562, 4.5236, 4.4852, 4.4468, 4.4084, 4.37, 4.3317, 4.2936, 4.2559, 4.2187, 4.1821, 4.1463, 4.1115, 4.0777, 4.0451, 4.0139, 3.9841, 3.956, 3.9297, 3.9053, 3.883, 3.8628, 3.845, 3.8294, 3.816, 3.8046, 3.795, 3.7872, 3.7811, 3.7764, 3.7732, 3.7713, 3.7704, 3.7707, 3.7718, 3.7737, 3.7763, 3.7794, 3.783, 3.7868, 3.791, 3.7955, 3.8002, 3.8051, 3.8103, 3.8157, 3.8213, 3.827, 3.8329, 3.8389, 3.845, 3.8512, 3.8575, 3.8638, 3.8701, 3.8765, 3.8828, 3.8892, 3.8955, 3.9019, 3.9082, 3.9146, 3.9209, 3.9273, 3.9336, 3.94, 3.9463, 3.9527, 3.959, 3.9654, 3.9717, 3.9781, 3.9844, 3.9908, 3.9971, 4.0035, 4.0098, 4.0162, 4.0225, 4.0289, 4.0352, 4.0416, 4.0479, 4.0543, 4.0606, 4.067, 4.0733, 4.0797, 4.086, 4.0924, 4.0987, 4.1051, 4.1114, 4.1178, 4.1241, 4.1305, 4.1368, 4.1432, 4.1495, 4.1559, 4.1622, 4.1686, 4.1749, 4.1813, 4.1876, 4.194, 4.2003, 4.2067, 4.213, 4.2194, 4.2257, 4.2321, 4.2384, 4.2448, 4.2511, 4.2575, 4.2638, 4.2702, 4.2765, 4.2829, 4.2892, 4.2956, 4.3019, 4.3083, 4.3146, 4.321, 4.3273, 4.3337, 4.34, 4.3464, 4.3527, 4.3591, 4.3654, 4.3718, 4.3781, 4.3845, 4.3908, 4.3972, 4.4035, 4.4099, 4.4162, 4.4226, 4.4289, 4.4353, 4.4416, 4.448, 4.4543, 4.4607, 4.467, 4.4734, 4.4797, 4.4861, 4.4924, 4.4988, 4.5051, 4.5115, 4.5178, 4.5242, 4.5305, 4.5369, 4.5432],
"energy_j_kg_s": [0, 0.303, 0.6021, 0.8974, 1.1888, 1.4764, 1.7602, 2.0401, 2.3162, 2.5884, 2.8568, 3.1214, 3.3821, 3.639, 3.8921, 4.1413, 4.3867, 4.6282, 4.8659, 5.0998, 5.3298, 5.556, 5.7783, 5.9968, 6.2115, 6.4223, 6.6293, 6.8324, 7.0317, 7.2272, 7.4188, 7.6066, 7.7905, 7.9707, 8.1469, 8.3194, 8.488, 8.6527, 8.8136, 8.9707, 9.1239, 9.2733, 9.4189, 9.5606, 9.6985, 9.8325, 9.9629, 10.09, 10.2142, 10.3358, 10.4553, 10.5731, 10.6898, 10.8058, 10.9217, 11.0381, 11.1556, 11.2747, 11.3962, 11.5207, 11.6489, 11.7817, 11.9196, 12.0628, 12.2112, 12.3648, 12.5235, 12.6872, 12.8557, 13.0287, 13.2062, 13.388, 13.5736, 13.763, 13.9557, 14.1515, 14.3499, 14.5508, 14.7536, 14.958, 15.1641, 15.3717, 15.5808, 15.7914, 16.0034, 16.2168, 16.4315, 16.6475, 16.8647, 17.0831, 17.3026, 17.523, 17.7444, 17.9666, 18.1896, 18.4133, 18.6376, 18.8625, 19.0881, 19.3143, 19.5411, 19.7686, 19.9967, 20.2255, 20.4549, 20.6849, 20.9155, 21.1468, 21.3788, 21.6113, 21.8445, 22.0783, 22.3128, 22.5479, 22.7837, 23.02, 23.257, 23.4947, 23.7329, 23.9719, 24.2114, 24.4516, 24.6924, 24.9338, 25.1759, 25.4186, 25.662, 25.906, 26.1506, 26.3959, 26.6418, 26.8883, 27.1355, 27.3833, 27.6317, 27.8808, 28.1305, 28.3808, 28.6318, 28.8834, 29.1357, 29.3885, 29.6421, 29.8962, 30.151, 30.4064, 30.6625, 30.9192, 31.1765, 31.4344, 31.693, 31.9523, 32.2121, 32.4726, 32.7338, 32.9955, 33.2579, 33.521, 33.7847, 34.049, 34.3139, 34.5795, 34.8457, 35.1126, 35.3801, 35.6482, 35.9169, 36.1863, 36.4563, 36.727, 36.9983, 37.2702, 37.5428, 37.816, 38.0898, 38.3643, 38.6394, 38.9152, 39.1915, 39.4685, 39.7462, 40.0245, 40.3034, 40.5829, 40.8631, 41.1439, 41.4254, 41.7075, 41.9902, 42.2736, 42.5576, 42.8422, 43.1275, 43.4134, 43.6999, 43.9871, 44.2749, 44.5633, 44.8524, 45.1421, 45.4325]
};
// ==== Lookup/interpolation helper ====
function lookupSpeed(x, col_name) {
const speed = blackGam.speed_m_s;
const energy = blackGam[col_name];
if (x < speed[0] || x > speed[speed.length - 1]) return NaN;
let i = 0;
for (; i < speed.length - 1; i++) {
if (x >= speed[i] && x <= speed[i + 1]) break;
}
// Linear interpolation
return energy[i] + (energy[i+1] - energy[i]) * ((x - speed[i]) / (speed[i+1] - speed[i]));
}
// ==== Minetti 2002 quintic polynomial for (change in) cost ====
function delta_Cr(g) {
return (
155.4 * Math.pow(g, 5) - 30.4 * Math.pow(g, 4) - 43.3 * Math.pow(g, 3) + 46.3 * Math.pow(g, 2) + 19.5 * g
); // g as decimal grade
}
// ==== Find equivalent flat speed for given metabolic power ====
function getEquivFlatSpeed(W_kg) {
const speed = blackGam.speed_m_s;
const met_power = blackGam["energy_j_kg_s"];
if (W_kg < met_power[0] || W_kg > met_power[met_power.length - 1]) return NaN;
let i = 0;
for (; i < met_power.length - 1; i++) {
if (W_kg >= met_power[i] && W_kg <= met_power[i + 1]) break;
}
// Linear interpolation
return speed[i] + (speed[i+1] - speed[i]) * ((W_kg - met_power[i]) / (met_power[i+1] - met_power[i]));
}
// ==== Main stream code ====
time_arr = icu.streams.time; // seconds since start
alt_arr = icu.streams.altitude;
dist_arr = icu.streams.distance;
v_arr = icu.streams.velocity_smooth;
window = 8;
results = [];
for (let i = 0; i < time_arr.length; i++) {
    let g = 0;
    if (i >= window) {
        let start_idx = i - window;
        let dist_diff = dist_arr[i] - dist_arr[start_idx];
        let alt_diff = alt_arr[i] - alt_arr[start_idx];
        g = dist_diff > 0 ? (alt_diff / dist_diff) : 0;
    }
let v = v_arr[i];
let Cr_flat = lookupSpeed(v, "energy_j_kg_m"); // table lookup
let deltaCr = delta_Cr(g); // Minetti addition
let totalCr = Cr_flat + deltaCr; // total J/kg/m
let P = totalCr * v; // W/kg (J/kg/s)
results[i] = getEquivFlatSpeed(P);
}
for (let i = 0; i < results.length; i++) {
    data.setAt(time_arr[i], results[i]);
}
icu.stats.calcMovingAvg(data, 10);

Tried to get the newly shared one but the stream is not returning anything. No errors, but also no plot.

[quote=“Justin_Wang, post:10, topic:112239”]

for (let i = 0; i < results.length; i++) {
    data.setAt(time_arr[i], results[i]);

for (let i = 0; i < results.length; i++) {
data.setAt(time_arr[i], results[i]?results[i]:0);
}

And do not process fit file messages
image

Ah shivers, I popped that in to save computing for when i re-analysed activities in the future, but forgot I shared the stream with everyone