Great question. This is exactly the kind of metric that’s missing from most running platforms, and you’re right to focus on it. Downhill running is fundamentally different from flat or uphill running because of the eccentric load, and that’s what causes the real muscle damage.
The core problem:
When you run downhill, your quads and calves are working eccentrically - they’re contracting while lengthening to control your descent. This is what creates that deep soreness that lasts for days. Research shows that a single 30-minute downhill session can elevate muscle damage markers for 3-4 days. So quantifying this load is absolutely valuable for training.
What matters most:
-
Gradient - this is the primary driver. The steeper the downhill, the more braking force required. There’s a non-linear relationship here - a -10% grade is significantly worse than -5%, not just twice as bad.
-
Speed - faster descents increase the braking impulse. This is why you feel more beat up after bombing a descent versus controlling your speed.
-
Cadence - this is the one factor you can actively change. Higher cadence (more steps per minute) reduces the impact per step and lowers the braking demand.
-
Ground contact time - longer time on the ground generally means more braking load.
What I built: Impact Load Rate with Grade Component
The idea is simple - take @miguell ´s ILR Calc Custom Stream (which already estimates vertical impact forces from Garmin’s running dynamics data) and add a grade component that kicks in on downhill sections.
The formula works like this:
-
Original ILR runs as normal on all terrain
-
On downhill sections (negative grade), an additional grade component is calculated using GCT, gradient, speed, and cadence
-
The grade component is weighted (0.10) and added to the original ILR Calc
This gives you a single stream that accounts for both vertical impact forces AND the braking demand from downhills.
I called the new stream “ILR Calc with Grade”.
The values are in BW/s (body weight per second). The grade component only activates on downhill sections (negative grade), and the console output shows ILR and grade component breakdown.
The grade component uses a non-linear gradient factor (|grade|/10)^1.6 + 1 - so steeper slopes contribute much more to the load than milder ones. I’ve also added GCT ratio, speed factor, and cadence factor to capture the full braking picture.
Here’s the full script:
js
(function() {
try {
// ============================================================
// IMPACT LOAD RATE WITH GRADE COMPONENT
// ============================================================
// ILR + Downhill Eccentric Load (grade-based)
// ============================================================
console.log("IMPACT LOAD RATE WITH GRADE COMPONENT");
console.log("========================================");
// ============================================================
// CONFIG / CALIBRATION
// ============================================================
var STREAM_KEYS = {
speed: "velocity_smooth",
grade: "grade_smooth",
cadence: "cadence", // RPM from Garmin
vo: "GarminVO", // Vertical oscillation
gct: "GarminGCT",
time: "time"
};
//ILR params
var MIN_SPEED = 0.5, MAX_SPEED = 12;
var MIN_CAD = 70, MAX_CAD = 260; // SPM (RPM Ă— 2)
var MIN_VO = 8, MAX_VO = 220;
var MIN_GCT = 45, MAX_GCT = 550;
var CONTACT_REF = 260;
var VLOAD_OFFSET = 1.5;
var SPEED_INT_BASE = 0.7;
var SPEED_INT_PER_MS = 0.10;
var CALIBRATION_IL = 11.35;
// Grade component params
var BASELINE_GCT = 220;
var BASELINE_SPEED = 3.0;
var BASELINE_CADENCE_SPM = 180;
var GRADE_EXPONENT = 1.6;
var GRADE_WEIGHT = 0.10; // Weight of grade component
var ROUND_DECIMALS = 1;
// ============================================================
// SAFE STREAM FETCH
// ============================================================
function getStreamData(key) {
var s = streams.get(key);
return (s && Array.isArray(s.data)) ? s.data : null;
}
var vel = getStreamData(STREAM_KEYS.speed);
var grade = getStreamData(STREAM_KEYS.grade);
var cadence_rpm = getStreamData(STREAM_KEYS.cadence);
var vo = getStreamData(STREAM_KEYS.vo);
var gct = getStreamData(STREAM_KEYS.gct);
var timeArr = getStreamData(STREAM_KEYS.time);
var missing = [];
var required = ['speed', 'grade', 'cadence', 'vo', 'gct', 'time'];
for (var r = 0; r < required.length; r++) {
var key = required[r];
if (!getStreamData(STREAM_KEYS[key])) {
missing.push(STREAM_KEYS[key]);
}
}
if (missing.length) {
console.log("❌ MISSING STREAMS: " + missing.join(", "));
return [];
}
var n = Math.min(vel.length, grade.length, cadence_rpm.length, vo.length, gct.length, timeArr.length);
if (n === 0) return [];
console.log("Processing " + n + " data points");
console.log("Cadence: RPM Ă— 2 = SPM");
console.log("Grade Weight: " + GRADE_WEIGHT + "\n");
if (typeof data.clear === 'function') data.clear();
var pointsSet = 0;
var totalILR = 0;
var minILR = Infinity;
var maxILR = -Infinity;
var downhillPoints = 0;
var maxGradient = 0;
var totalGradient = 0;
var gradeImpact = 0;
var invalidReasons = {
missing: 0, speed: 0, grade: 0,
cadence: 0, vo: 0, gct: 0
};
for (var i = 0; i < n; i++) {
var speed = vel[i];
var grad = grade[i];
var cadRPM = cadence_rpm[i];
var vlo = vo[i];
var contact = gct[i];
var timestamp = timeArr[i];
if (speed == null || grad == null || cadRPM == null || vlo == null || contact == null || timestamp == null) {
invalidReasons.missing++;
continue;
}
var cadSPM = cadRPM * 2;
if (typeof speed !== 'number' || speed < MIN_SPEED || speed > MAX_SPEED) {
invalidReasons.speed++;
continue;
}
if (typeof cadSPM !== 'number' || cadSPM < MIN_CAD || cadSPM > MAX_CAD) {
invalidReasons.cadence++;
continue;
}
if (typeof vlo !== 'number' || vlo < MIN_VO || vlo > MAX_VO) {
invalidReasons.vo++;
continue;
}
if (typeof contact !== 'number' || contact < MIN_GCT || contact > MAX_GCT) {
invalidReasons.gct++;
continue;
}
// ============================================================
// CALCULATE ORIGINAL IMPACT LOAD RATE
// ============================================================
var impactsPerSecond = cadSPM / 60.0;
var verticalLoad = VLOAD_OFFSET + (vlo / 1000.0);
var speedIntensity = SPEED_INT_BASE + (speed * SPEED_INT_PER_MS);
var contactTimeFactor = CONTACT_REF / contact;
contactTimeFactor = Math.max(contactTimeFactor, 0.5);
contactTimeFactor = Math.min(contactTimeFactor, 5.5);
var impactRaw = impactsPerSecond * verticalLoad * speedIntensity * contactTimeFactor * CALIBRATION_IL;
// ============================================================
// CALCULATE GRADE COMPONENT (only for downhill)
// ============================================================
var gradeComponent = 0;
if (typeof grad === 'number' && grad < 0) {
var gctRatio = contact / BASELINE_GCT;
var gradeFactor = Math.pow(Math.abs(grad) / 10, GRADE_EXPONENT) + 1;
var speedFactor = speed / BASELINE_SPEED;
var cadenceFactor = BASELINE_CADENCE_SPM / cadSPM;
gradeComponent = gctRatio * gradeFactor * speedFactor * cadenceFactor * GRADE_WEIGHT;
downhillPoints++;
totalGradient += Math.abs(grad);
if (Math.abs(grad) > maxGradient) maxGradient = Math.abs(grad);
gradeImpact += gradeComponent;
}
// ============================================================
// COMBINE: ILR + Grade Component
// ============================================================
var ilrWithGrade = impactRaw + gradeComponent;
var ilrValue = Number(ilrWithGrade.toFixed(ROUND_DECIMALS));
if (typeof data.setAt === 'function') {
data.setAt(timestamp, ilrValue);
}
totalILR += ilrWithGrade;
if (ilrWithGrade < minILR) minILR = ilrWithGrade;
if (ilrWithGrade > maxILR) maxILR = ilrWithGrade;
pointsSet++;
}
var avgILR = pointsSet > 0 ? totalILR / pointsSet : 0;
var avgGradeImpact = downhillPoints > 0 ? gradeImpact / downhillPoints : 0;
var avgDownhillGrad = downhillPoints > 0 ? totalGradient / downhillPoints : 0;
var minOut = (minILR === Infinity) ? 0 : Number(minILR.toFixed(ROUND_DECIMALS));
var maxOut = (maxILR === -Infinity) ? 0 : Number(maxILR.toFixed(ROUND_DECIMALS));
if (typeof data.setType === 'function') data.setType("impact");
if (typeof data.setUnit === 'function') data.setUnit("BW/s");
if (typeof data.setName === 'function') data.setName("Impact Load Rate with Grade");
console.log("📊 ILR:");
console.log(" Average: " + avgILR.toFixed(ROUND_DECIMALS) + " BW/s");
console.log(" Range: " + minOut + " - " + maxOut + " BW/s");
console.log("");
console.log("⛰️ GRADE COMPONENT:");
console.log(" Downhill Points: " + downhillPoints);
console.log(" Average Grade: " + avgDownhillGrad.toFixed(1) + "%");
console.log(" Max Gradient: " + maxGradient.toFixed(1) + "%");
console.log(" Average Grade Impact: " + avgGradeImpact.toFixed(ROUND_DECIMALS) + " BW/s");
console.log("");
console.log("📊 VALIDATION:");
console.log(" Valid Points: " + pointsSet + "/" + n + " (" + ((pointsSet/n)*100).toFixed(1) + "%)");
console.log(" Rejected - Missing: " + invalidReasons.missing + ", Speed: " + invalidReasons.speed);
console.log(" Rejected - Cadence: " + invalidReasons.cadence + ", VO: " + invalidReasons.vo + ", GCT: " + invalidReasons.gct);
console.log("");
var level = "Optimal";
if (avgILR < 45) level = "Very Low";
else if (avgILR < 60) level = "Low";
else if (avgILR < 75) level = "Moderate";
else if (avgILR < 90) level = "High";
else level = "Very High";
console.log("Risk Level: " + level);
console.log("");
console.log("đź’ˇ FORMULA:");
console.log(" ILR with Grade = ILR + (Grade Component Ă— " + GRADE_WEIGHT + ")");
console.log(" Grade Component only applies on downhill sections");
console.log("");
console.log("âś… Stream created: 'Impact Load Rate with Grade'");
console.log(" Data points stored: " + pointsSet);
return [pointsSet, Number(avgILR.toFixed(ROUND_DECIMALS))];
} catch (e) {
console.log("❌ ERROR: " + (e && e.message ? e.message : e));
return [];
}
})();
What you’ll see:
-
Values in BW/s (body weight per second)
-
The grade component only activates on downhill sections (negative grade)
-
Console output shows ILR and grade component breakdown
I’ve tested this on several trail runs and the gradient differentiation is clear - mild downhills show moderate increases, steep sections show significant spikes.
Your proposed dedicated metric: ELI (Eccentric Load Index) - Pure Downhill Braking Demand
While the ILR Calc with Grade is great for overall load monitoring, Your proposed dedicated metric focuses exclusively on downhill braking demand - the Eccentric Load Index (ELI).
Why a separate metric?
The ILR Calc with Grade still includes impact forces from vertical oscillation, which can mask the pure braking signal. Eccentric Load Index (ELI) would strip away everything except the eccentric braking component, giving you a clean measure of what’s actually causing muscle damage on downhills.
For Eccentric Load Index (ELI) i would suggest also the use of the following streams:
Ground Contact Time, Ground Contact Time Percent and Ground Contact Time Balance