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%
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