From f573aff39c804ced8a70adf94639b919e4dd96e7 Mon Sep 17 00:00:00 2001 From: artem Date: Sat, 12 Sep 2026 18:17:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workouts/components/NormalizedPower.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/pages/workouts/components/NormalizedPower.ts diff --git a/src/pages/workouts/components/NormalizedPower.ts b/src/pages/workouts/components/NormalizedPower.ts new file mode 100644 index 0000000..372b531 --- /dev/null +++ b/src/pages/workouts/components/NormalizedPower.ts @@ -0,0 +1,62 @@ +/** + * Computes Normalized Power (NP) over a slice of a workout, matching the + * canonical Strava/TrainingPeaks algorithm and tolerating uneven sample dt. + * + * Exponential 4th-power smoothing with a 30 s time constant; points with + * null power or non-finite timestamps are skipped, as are samples with + * non-positive dt. Returns null when fewer than 2 valid points exist. + * + * @param times timestamps (ISO strings or ms numbers), 1 Hz + * @param powers power in watts, null where the sensor has no data + * @param startIdx inclusive start index + * @param endIdx exclusive end index + */ +export function normalizedPower( + times: Array, + powers: Array, + startIdx: number, + endIdx: number, +): number | null { + const tau = 30; + const end = Math.min(endIdx, times.length, powers.length); + const start = Math.max(0, startIdx); + + // Collect valid points: finite timestamp + non-null power. + const t: number[] = []; + const p: number[] = []; + for (let i = start; i < end; i++) { + const raw = times[i]; + const ms = + typeof raw === "number" && Number.isFinite(raw) + ? raw + : Number(new Date(raw as string)); + const w = powers[i]; + if (!Number.isFinite(ms) || w === null || !Number.isFinite(w)) { + continue; + } + t.push(ms); + p.push(w); + } + + const n = p.length; + if (n < 2) { + return null; + } + + // Exponential smoothing of p^4; the last sample has no forward dt, so its + // 4th power is added to the sum as its own smoothing value. + let sm = Math.pow(p[0], 4); + let sum = sm; + let count = 1; + for (let i = 1; i < n; i++) { + const dt = (t[i] - t[i - 1]) / 1000; + if (dt <= 0) { + continue; + } + sm = sm + (Math.pow(p[i], 4) - sm) * (1 - Math.exp(-dt / tau)); + sum += sm; + count += 1; + } + + return Math.pow(sum / count, 0.25); +}