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%
4.1 KB · 130 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/counter/src/value.ts6 * Purpose: Pure evaluation of a CounterModel — value(model, t) and rateAt(model, t), bit-identical client/server7 */89import {10  type CounterModel,11  type Harmonic,12  type RateFunction,13  DAY_SECONDS,14  SEASONAL_EPOCH_MS,15  WEEK_SECONDS,16  YEAR_SECONDS,17  parseIsoUtc,18} from "./counter-model.js";19import { pchipDerivative, pchipEvaluate, pchipSlopes } from "./pchip.js";2021/** Parsed spline data is cached per rateFn object identity — evaluation stays pure. */22const splineCache = new WeakMap<23  object,24  { xs: number[]; ys: number[]; slopes: number[] }25>();2627function splineData(rateFn: Extract<RateFunction, { kind: "spline" }>): {28  xs: number[];29  ys: number[];30  slopes: number[];31} {32  const cached = splineCache.get(rateFn);33  if (cached) return cached;34  const xs = rateFn.knots.map(([t]) => parseIsoUtc(t));35  const ys = rateFn.knots.map(([, v]) => v);36  const data = { xs, ys, slopes: pchipSlopes(xs, ys) };37  splineCache.set(rateFn, data);38  return data;39}4041function harmonicOmega(h: Harmonic): number {42  const period =43    h.period === "year" ? YEAR_SECONDS : h.period === "week" ? WEEK_SECONDS : DAY_SECONDS;44  return (2 * Math.PI * h.order) / period;45}4647/** ∫ rate dt of one harmonic between two instants (seconds since the seasonal epoch). */48function harmonicIntegral(h: Harmonic, tau0: number, tau1: number): number {49  const omega = harmonicOmega(h);50  return (h.amplitude / omega) * (Math.sin(omega * tau1 + h.phase) - Math.sin(omega * tau0 + h.phase));51}5253function harmonicRate(h: Harmonic, tau: number): number {54  return h.amplitude * Math.cos(harmonicOmega(h) * tau + h.phase);55}5657/**58 * Value of the counter at time t (ms since Unix epoch, UTC).59 * Time is always a parameter — this package never reads the system clock.60 */61export function counterValue(model: CounterModel, tMs: number): number {62  const anchorMs = parseIsoUtc(model.anchorTime);63  const rateFn = model.rateFn;64  switch (rateFn.kind) {65    case "linear":66      return model.anchorValue + (rateFn.perSecond * (tMs - anchorMs)) / 1000;6768    case "piecewise": {69      // Signed integral of the piecewise-constant rate from anchorTime to t.70      const froms = rateFn.segments.map((s) => parseIsoUtc(s.from));71      let acc = model.anchorValue;72      const [a, b] = anchorMs <= tMs ? [anchorMs, tMs] : [tMs, anchorMs];73      let integral = 0;74      for (let i = 0; i < rateFn.segments.length; i++) {75        const segStart = froms[i]!;76        const segEnd = i + 1 < froms.length ? froms[i + 1]! : Infinity;77        const lo = Math.max(a, segStart);78        const hi = Math.min(b, segEnd);79        if (hi > lo) integral += (rateFn.segments[i]!.perSecond * (hi - lo)) / 1000;80      }81      acc += anchorMs <= tMs ? integral : -integral;82      return acc;83    }8485    case "seasonal": {86      const tau0 = (anchorMs - SEASONAL_EPOCH_MS) / 1000;87      const tau1 = (tMs - SEASONAL_EPOCH_MS) / 1000;88      let v = model.anchorValue + rateFn.base * (tau1 - tau0);89      for (const h of rateFn.harmonics) v += harmonicIntegral(h, tau0, tau1);90      return v;91    }9293    case "spline": {94      const { xs, ys, slopes } = splineData(rateFn);95      return pchipEvaluate(xs, ys, slopes, tMs);96    }97  }98}99100/** Instantaneous rate (base unit per second) at time t. */101export function rateAt(model: CounterModel, tMs: number): number {102  const rateFn = model.rateFn;103  switch (rateFn.kind) {104    case "linear":105      return rateFn.perSecond;106107    case "piecewise": {108      let rate = rateFn.segments[0]?.perSecond ?? 0;109      for (const seg of rateFn.segments) {110        if (parseIsoUtc(seg.from) <= tMs) rate = seg.perSecond;111        else break;112      }113      return rate;114    }115116    case "seasonal": {117      const tau = (tMs - SEASONAL_EPOCH_MS) / 1000;118      let r = rateFn.base;119      for (const h of rateFn.harmonics) r += harmonicRate(h, tau);120      return r;121    }122123    case "spline": {124      const { xs, ys, slopes } = splineData(rateFn);125      // pchip derivative is per ms of x-axis; convert to per second.126      return pchipDerivative(xs, ys, slopes, tMs) * 1000;127    }128  }129}130