SPB Git

spb/earth-now Public License

earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.

TypeScript 93% Shell 2.3% SQL 1.4% JavaScript 1.3% Dockerfile 1.2% CSS 0.8%
3.8 KB · 123 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/counter/src/pchip.ts6 * Purpose: Monotone cubic (PCHIP, Fritsch–Carlson) evaluation — never overshoots, shared by client, server and badge7 */89/**10 * Compute PCHIP endpoint/interior slopes for strictly increasing xs.11 * Fritsch–Carlson: interior slopes are a weighted harmonic mean of adjacent12 * secants, zero at local extrema — this is what guarantees no overshoot13 * (a natural cubic spline could send a counter backwards; PCHIP cannot).14 */15export function pchipSlopes(xs: readonly number[], ys: readonly number[]): number[] {16  const n = xs.length;17  if (n !== ys.length) throw new Error("pchip: xs and ys length mismatch");18  if (n === 0) throw new Error("pchip: need at least one knot");19  if (n === 1) return [0];2021  const h: number[] = new Array(n - 1);22  const delta: number[] = new Array(n - 1);23  for (let i = 0; i < n - 1; i++) {24    const dx = xs[i + 1]! - xs[i]!;25    if (dx <= 0) throw new Error("pchip: knots must be strictly increasing in time");26    h[i] = dx;27    delta[i] = (ys[i + 1]! - ys[i]!) / dx;28  }29  if (n === 2) return [delta[0]!, delta[0]!];3031  const m: number[] = new Array(n).fill(0);32  for (let i = 1; i < n - 1; i++) {33    const d0 = delta[i - 1]!;34    const d1 = delta[i]!;35    if (d0 * d1 <= 0) {36      m[i] = 0;37    } else {38      const w1 = 2 * h[i]! + h[i - 1]!;39      const w2 = h[i]! + 2 * h[i - 1]!;40      m[i] = (w1 + w2) / (w1 / d0 + w2 / d1);41    }42  }4344  m[0] = endpointSlope(h[0]!, h[1]!, delta[0]!, delta[1]!);45  m[n - 1] = endpointSlope(h[n - 2]!, h[n - 3]!, delta[n - 2]!, delta[n - 3]!);46  return m;47}4849/** One-sided three-point endpoint slope with the standard monotonicity clamps. */50function endpointSlope(h0: number, h1: number, d0: number, d1: number): number {51  let m = ((2 * h0 + h1) * d0 - h0 * d1) / (h0 + h1);52  if (m * d0 <= 0) m = 0;53  else if (d0 * d1 < 0 && Math.abs(m) > 3 * Math.abs(d0)) m = 3 * d0;54  return m;55}5657/**58 * Evaluate the PCHIP interpolant at x. Outside the knot range the value59 * extrapolates linearly with the endpoint derivative (bounded, no polynomial blowup).60 */61export function pchipEvaluate(62  xs: readonly number[],63  ys: readonly number[],64  slopes: readonly number[],65  x: number,66): number {67  const n = xs.length;68  if (n === 1) return ys[0]!;69  if (x <= xs[0]!) return ys[0]! + slopes[0]! * (x - xs[0]!);70  if (x >= xs[n - 1]!) return ys[n - 1]! + slopes[n - 1]! * (x - xs[n - 1]!);7172  // Binary search for the segment containing x.73  let lo = 0;74  let hi = n - 1;75  while (hi - lo > 1) {76    const mid = (lo + hi) >> 1;77    if (xs[mid]! <= x) lo = mid;78    else hi = mid;79  }8081  const h = xs[lo + 1]! - xs[lo]!;82  const t = (x - xs[lo]!) / h;83  const t2 = t * t;84  const t3 = t2 * t;85  const h00 = 2 * t3 - 3 * t2 + 1;86  const h10 = t3 - 2 * t2 + t;87  const h01 = -2 * t3 + 3 * t2;88  const h11 = t3 - t2;89  return (90    h00 * ys[lo]! + h10 * h * slopes[lo]! + h01 * ys[lo + 1]! + h11 * h * slopes[lo + 1]!91  );92}9394/** First derivative of the PCHIP interpolant at x (endpoint slope outside the range). */95export function pchipDerivative(96  xs: readonly number[],97  ys: readonly number[],98  slopes: readonly number[],99  x: number,100): number {101  const n = xs.length;102  if (n === 1) return 0;103  if (x <= xs[0]!) return slopes[0]!;104  if (x >= xs[n - 1]!) return slopes[n - 1]!;105106  let lo = 0;107  let hi = n - 1;108  while (hi - lo > 1) {109    const mid = (lo + hi) >> 1;110    if (xs[mid]! <= x) lo = mid;111    else hi = mid;112  }113114  const h = xs[lo + 1]! - xs[lo]!;115  const t = (x - xs[lo]!) / h;116  const t2 = t * t;117  const dh00 = (6 * t2 - 6 * t) / h;118  const dh10 = 3 * t2 - 4 * t + 1;119  const dh01 = (-6 * t2 + 6 * t) / h;120  const dh11 = 3 * t2 - 2 * t;121  return dh00 * ys[lo]! + dh10 * slopes[lo]! + dh01 * ys[lo + 1]! + dh11 * slopes[lo + 1]!;122}123