/** A unitless teaching illustration, not a model fitted to biological data. */ export type RecoveryParameters = { amplitude: number; speed: number; repeated: boolean; interval: number; }; export const recoveryDefaults: RecoveryParameters = { amplitude: 55, speed: 55, repeated: false, interval: 38, }; export const recoveryPresets: Record = { single: { ...recoveryDefaults }, slow: { ...recoveryDefaults, speed: 20 }, repeated: { ...recoveryDefaults, repeated: true, interval: 26 }, }; const clamp = (value: number, min: number, max: number, fallback: number) => Math.min(max, Math.max(min, Number.isFinite(value) ? value : fallback)); export function normalizeRecovery(p: RecoveryParameters): RecoveryParameters { return { amplitude: clamp(p.amplitude, 15, 85, 55), speed: clamp(p.speed, 10, 90, 55), repeated: Boolean(p.repeated), interval: clamp(p.interval, 20, 55, 38), }; } export function recoveryResponse( time: number, raw: RecoveryParameters, ): number { const p = normalizeRecovery(raw); const response = (since: number) => since <= 0 ? 0 : (p.amplitude / 100) * (1 - Math.exp(-since / 2)) * Math.exp(-since / (5 + (100 - p.speed) * 0.22)); return ( response(time - 12) + (p.repeated ? response(time - 12 - p.interval) : 0) ); } // Fixed axes prevent rescaling from disguising changes in amplitude. export function recoveryTrajectory(p: RecoveryParameters): string { return Array.from({ length: 241 }, (_, i) => { const time = (i / 240) * 100; return `${i === 0 ? "M" : "L"}${(40 + time * 6.2).toFixed(2)} ${(244 - recoveryResponse(time, p) * 165).toFixed(2)}`; }).join(" "); }