Applied the changes and got it working.
Apart from being more ‘spiky’, it very closely mirrors Coros Effort Pace!
Alright I’ve implemented the backup 0, and widened the windows, both to 20, and now the stream is about as “spiky” as the OG GAP stream
//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 || Array(time_arr.length).fill(0);
dist_arr = icu.streams.distance;
v_arr = icu.streams.velocity_smooth;
window = 20;
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) || 0;
}
for (let i = 0; i < results.length; i++) {
data.setAt(time_arr[i], results[i] ? results[i] : 0);
}
icu.stats.calcMovingAvg(data, 20);
data.type = “velocity”;
data. Unit = “m/s”;
And a Stryd Power Equivalent Pace based on Justin code above
// ==== EQUIVALENT FLAT SPEED FROM POWER ====
// Converts running power to equivalent flat speed using Black et al. model
// For Intervals.icu - Paste this into Activity Custom Stream
// Configuration - Adjust these for your needs
var CONFIG = {
EFFICIENCY: 0.27, // Running efficiency (20-25%), please adjust
GRADE_WINDOW: 15, // Grade calculation smoothing
SMOOTHING_WINDOW: 10, // Final speed smoothing
DEFAULT_WEIGHT: 75, // Your weight in kg
};
// Running economy data (Black et al.)
var ECONOMY_DATA = {
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_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]
};
// Binary search for efficient lookup
function binarySearch(arr, x) {
var low = 0, high = arr.length - 1;
while (low <= high) {
var mid = Math.floor((low + high) / 2);
if (arr[mid] === x) return mid;
arr[mid] < x ? low = mid + 1 : high = mid - 1;
}
return Math.max(0, Math.min(arr.length - 2, low - 1));
}
// Calculate grade from altitude and distance
function calculateGrade(altitudes, distances, currentIndex, window) {
if (currentIndex < window || !altitudes || !distances) return 0;
var startIdx = currentIndex - window;
var distDiff = distances[currentIndex] - distances[startIdx];
var altDiff = altitudes[currentIndex] - altitudes[startIdx];
return Math.abs(distDiff) < 0.1 ? 0 : altDiff / distDiff;
}
// Smooth array with moving average
function smoothArray(values, windowSize) {
if (!values || values.length === 0) return values;
var smoothed = new Array(values.length);
var halfWindow = Math.floor(windowSize / 2);
for (var i = 0; i < values.length; i++) {
var start = Math.max(0, i - halfWindow);
var end = Math.min(values.length, i + halfWindow + 1);
var sum = 0, count = 0;
for (var j = start; j < end; j++) {
if (values[j] > 0.1) { sum += values[j]; count++; }
}
smoothed[i] = count > 0 ? sum / count : values[i];
}
return smoothed;
}
// Convert metabolic power to equivalent flat speed
function getEquivalentFlatSpeed(metabolicPower) {
var speeds = ECONOMY_DATA.speed_m_s;
var powers = ECONOMY_DATA.energy_j_kg_s;
if (metabolicPower <= powers[0]) return speeds[0];
if (metabolicPower >= powers[powers.length - 1]) return speeds[speeds.length - 1];
var i = binarySearch(powers, metabolicPower);
return speeds[i] + (speeds[i + 1] - speeds[i]) *
((metabolicPower - powers[i]) / (powers[i + 1] - powers[i]));
}
// Calculate metabolic power from measured power
function calculateMetabolicPower(measuredPower, weightKg, grade) {
if (measuredPower < 10 || weightKg <= 0) return 0;
var mechanicalPowerWkg = measuredPower / weightKg;
var baseMetabolicPower = mechanicalPowerWkg / CONFIG.EFFICIENCY;
var gradeFactor = 1.0 + (Math.min(0.15, Math.abs(grade) * 0.08));
return baseMetabolicPower * gradeFactor;
}
// Estimate weight if not provided
function estimateWeight(velocity, power) {
if (!power || !velocity || velocity <= 0) return CONFIG.DEFAULT_WEIGHT;
var typicalCr = 4.0;
var estimatedWeight = power / (typicalCr * velocity * CONFIG.EFFICIENCY);
return Math.max(50, Math.min(100, estimatedWeight));
}
// Main Equivalent Flat Speed calculation
function calculateEFS() {
if (!icu || !icu.streams) return null;
var streams = icu.streams;
var timeArr = streams.time || [];
var altArr = streams.altitude || Array(timeArr.length).fill(0);
var distArr = streams.distance || [];
var velocityArr = streams.velocity_smooth || [];
var powerArr = streams.fixed_watts || [];
var weightKg = (activity && activity.StrydWeight) ? activity.StrydWeight : CONFIG.DEFAULT_WEIGHT;
if (!weightKg || weightKg <= 0) {
weightKg = estimateWeight(velocityArr[0] || 3.0, powerArr[0] || 300);
}
var results = new Array(timeArr.length).fill(0);
var hasElevationData = streams.altitude && streams.distance;
for (var i = 0; i < timeArr.length; i++) {
if (!powerArr[i] || powerArr[i] < 10 || !velocityArr[i] || velocityArr[i] < 0.1) continue;
try {
var grade = hasElevationData ? calculateGrade(altArr, distArr, i, CONFIG.GRADE_WINDOW) : 0;
var metabolicPower = calculateMetabolicPower(powerArr[i], weightKg, grade);
results[i] = getEquivalentFlatSpeed(metabolicPower);
} catch (error) {
// Skip calculation errors
}
}
var smoothedResults = smoothArray(results, CONFIG.SMOOTHING_WINDOW);
// Output to Intervals.icu data stream
if (typeof data !== 'undefined') {
for (var i = 0; i < timeArr.length; i++) {
if (smoothedResults[i] > 0.1) {
data.setAt(timeArr[i], smoothedResults[i]);
}
}
data.type = "velocity";
data.unit = "m/s";
data.name = "Equivalent Flat Speed";
}
return smoothedResults;
}
// Execute when loaded in Intervals.icu
if (typeof icu !== 'undefined') {
calculateEFS();
}
@david is there any way we could add this to the list of GAP Models? I can see in run settings the function of changing between different models has been programmed in, but right now there’s only Strava GAP available
// StrydEqPwr GAP Activity Custom Stream
// Equivalent Flat Speed from Power (Intervals.icu)
// Fixed & optimized version
var CONFIG = {
EFFICIENCY: 0.23, // Running efficiency (adjust if needed)
GRADE_WINDOW: 15, // samples for grade calc (uses distance window)
SMOOTHING_WINDOW: 10, // moving-average smoothing window (samples)
DEFAULT_WEIGHT: 75 // default weight (kg), preferably use Custom Field StrydWeight
};
// Running economy data (Black et al.)
var ECONOMY_DATA = {
speed_m_s: [0, 0.05, 0.1, /* ... truncated for brevity ... */ 10],
energy_j_kg_s: [0, 0.303, 0.6021, /* ... truncated for brevity ... */ 45.4325]
};
// --- Utility functions ---
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
// Binary search returning index i such that arr[i] <= x < arr[i+1]
function binarySearchLower(arr, x) {
var lo = 0, hi = arr.length - 1;
if (x <= arr[0]) return 0;
if (x >= arr[hi]) return hi - 1;
while (lo + 1 < hi) {
var mid = (lo + hi) >>> 1;
if (arr[mid] <= x) lo = mid; else hi = mid;
}
return lo;
}
// Linear interpolation between known arrays
function interp(xArr, yArr, x) {
var i = binarySearchLower(xArr, x);
var x0 = xArr[i], x1 = xArr[i + 1];
var y0 = yArr[i], y1 = yArr[i + 1];
if (x1 === x0) return y0;
var t = (x - x0) / (x1 - x0);
return y0 + t * (y1 - y0);
}
// O(n) moving average (handles edges by shorter window)
function movingAverage(values, windowSize) {
if (!values || values.length === 0) return values;
var n = values.length;
var w = Math.max(1, Math.floor(windowSize));
var half = Math.floor(w / 2);
var out = new Array(n);
var sum = 0, count = 0;
// initialize first window
for (var i = 0; i < Math.min(n, half + 1); i++) {
if (isFinite(values[i])) { sum += values[i]; count++; }
}
for (var i = 0; i < n; i++) {
var start = Math.max(0, i - half);
var end = Math.min(n - 1, i + half);
// build sum incrementally if small window; simple approach for clarity:
var s = 0, c = 0;
for (var j = start; j <= end; j++) {
var v = values[j];
if (isFinite(v)) { s += v; c++; }
}
out[i] = c > 0 ? s / c : 0;
}
return out;
}
// --- Domain functions ---
function getEquivalentFlatSpeed(metabolicPower) {
var speeds = ECONOMY_DATA.speed_m_s;
var powers = ECONOMY_DATA.energy_j_kg_s;
if (!Array.isArray(speeds) || !Array.isArray(powers) || speeds.length < 2) return 0;
if (!isFinite(metabolicPower) || metabolicPower <= powers[0]) return speeds[0];
if (metabolicPower >= powers[powers.length - 1]) return speeds[speeds.length - 1];
return interp(powers, speeds, metabolicPower);
}
// measuredPower in watts (total), weightKg in kg, grade = rise/run (signed)
function calculateMetabolicPower(measuredPower, weightKg, grade) {
if (!isFinite(measuredPower) || measuredPower < 0 || !isFinite(weightKg) || weightKg <= 0) return 0;
var mechanicalWperKg = measuredPower / weightKg; // W/kg
var baseMetabolic = mechanicalWperKg / Math.max(0.01, CONFIG.EFFICIENCY);
// grade adjustment: uphill increases metabolic cost, downhill slightly reduces it.
// Use small linear factor capped to avoid excessive influence.
var gradeFactor = 1 + clamp(grade * 0.08, -0.12, 0.15); // tuned caps
return baseMetabolic * gradeFactor;
}
// Estimate weight if missing: use simple inverse relation (keeps within plausible bounds)
function estimateWeight(velocity, power) {
if (!isFinite(power) || power <= 0 || !isFinite(velocity) || velocity <= 0) return CONFIG.DEFAULT_WEIGHT;
// power ≈ weight * CR * velocity * efficiency => weight ≈ power / (CR * v * eff)
var typicalCr = 4.0; // N/kg (approx rolling/movement cost per m/s) — adjustable
var w = power / (typicalCr * velocity * Math.max(0.01, CONFIG.EFFICIENCY));
return clamp(Math.round(w), 50, 110);
}
// Calculate signed grade using elevation/distance window (preserves sign)
function calculateGrade(altitudes, distances, idx, windowSamples) {
if (!Array.isArray(altitudes) || !Array.isArray(distances) || idx < 0) return 0;
var start = Math.max(0, idx - windowSamples);
var dDelta = distances[idx] - distances[start];
if (!isFinite(dDelta) || Math.abs(dDelta) < 0.5) return 0; // require >= 0.5 m to avoid noise
var altDelta = altitudes[idx] - altitudes[start];
return altDelta / dDelta;
}
// --- Main calculation ---
function calculateEFS() {
if (typeof icu === 'undefined' || !icu.streams) return null;
var streams = icu.streams;
var timeArr = streams.time || [];
var altArr = streams.altitude || [];
var distArr = streams.distance || [];
var velArr = streams.velocity_smooth || streams.velocity || [];
var powerArr = streams.fixed_watts || streams.power || [];
var n = timeArr.length || 0;
if (n === 0) return [];
// Ensure arrays length = n
function ensureLen(arr) { if (!arr || arr.length !== n) return new Array(n).fill(0); return arr; }
altArr = ensureLen(altArr);
distArr = ensureLen(distArr);
velArr = ensureLen(velArr);
powerArr = ensureLen(powerArr);
// Determine weight
var weightKg = (typeof activity !== 'undefined' && activity.StrydWeight) ? activity.StrydWeight : CONFIG.DEFAULT_WEIGHT;
if (!isFinite(weightKg) || weightKg <= 0) {
// try a quick estimate from first valid samples
var firstIdx = 0;
while (firstIdx < n && (!isFinite(powerArr[firstIdx]) || powerArr[firstIdx] < 10 || !isFinite(velArr[firstIdx]) || velArr[firstIdx] <= 0)) firstIdx++;
weightKg = (firstIdx < n) ? estimateWeight(velArr[firstIdx] || 3.0, powerArr[firstIdx] || 300) : CONFIG.DEFAULT_WEIGHT;
}
var results = new Array(n).fill(0);
var hasElevationData = altArr.some(isFinite) && distArr.some(isFinite);
for (var i = 0; i < n; i++) {
var p = powerArr[i];
var v = velArr[i];
if (!isFinite(p) || p < 10 || !isFinite(v) || v < 0.1) continue;
var grade = 0;
if (hasElevationData) {
grade = calculateGrade(altArr, distArr, i, CONFIG.GRADE_WINDOW);
}
var metabolic = calculateMetabolicPower(p, weightKg, grade);
if (metabolic <= 0) continue;
results[i] = getEquivalentFlatSpeed(metabolic);
}
// Smooth results
var smoothed = movingAverage(results, CONFIG.SMOOTHING_WINDOW);
// Output to Intervals.icu data object if present
if (typeof data !== 'undefined' && Array.isArray(timeArr)) {
for (var k = 0; k < n; k++) {
var val = smoothed[k];
if (isFinite(val) && val > 0.01) data.setAt(timeArr[k], val);
}
data.type = "velocity";
data.unit = "m/s";
data.name = "Equivalent Flat Speed";
}
return smoothed;
}
// Execute when loaded in Intervals.icu
if (typeof icu !== 'undefined') {
calculateEFS();
}
Unfortunately I could not get this code to give me meaningful data
no matter what variables I changed, it would spit out a pace which was 1min/km faster than it should’ve been
I’m also curious about the line // Execute when loaded in Intervals.icu
if (typeof icu !== ‘undefined’) {
calculateEFS();
}
I’ve noticed a few custom streams and/or activity fields don’t populate without an additional “analyse”
but i couldn’t quite get it to work
I’m now using @R2Tom 's script and @Justin_Wang 's idea with a cloned GAP stream to get a very nice comparison of Coros Effort, Intervals GAP and John Davis:



