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 · 97 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/counter/src/counter-model.ts6 * Purpose: The project's central contract — CounterModel and RateFunction types shared by server, client, widget and badge7 */89/**10 * A single Fourier harmonic of the instantaneous rate.11 *12 * The rate contribution at time t is:13 *   amplitude * cos(2π * order * τ / period + phase)14 * where τ is the number of seconds between t and SEASONAL_EPOCH (2000-01-01T00:00:00Z),15 * so phases are absolute calendar phases, identical for every client.16 */17export interface Harmonic {18  /** Base period of the cycle. "year" = mean Gregorian year, "week" = 604 800 s, "day" = 86 400 s. */19  period: "year" | "week" | "day";20  /** Harmonic order (1 = fundamental, 2 = half-period, ...). Must be >= 1. */21  order: number;22  /** Amplitude, in the metric's base unit per second. */23  amplitude: number;24  /** Phase offset in radians at the seasonal epoch. */25  phase: number;26}2728export type RateFunction =29  /** Constant rate: v(t) = anchorValue + perSecond * (t - anchorTime). */30  | { kind: "linear"; perSecond: number }31  /**32   * Piecewise-constant rate. Segments must be sorted by `from` ascending and the33   * first segment must start at or before anchorTime. Used e.g. for Jan-1-UTC resets.34   */35  | { kind: "piecewise"; segments: Array<{ from: string; perSecond: number }> }36  /**37   * Seasonal rate: base + Fourier harmonics. v(t) is the analytic integral of the38   * rate from anchorTime, so evaluation is exact and identical on every runtime.39   */40  | { kind: "seasonal"; base: number; harmonics: Harmonic[] }41  /**42   * Monotone-spline (PCHIP) interpolation of (time, value) knots — value knots,43   * not rate knots. Outside the knot range the value extrapolates linearly with44   * the endpoint derivative. anchorValue must equal the spline value at anchorTime.45   */46  | { kind: "spline"; knots: Array<[time: string, value: number]> };4748export interface DisplayHints {49  /** Fraction digits to render. */50  decimals: number;51  /** Hard cap on significant digits — never display more precision than the model justifies. */52  sigFigs?: number;53  /** Display unit label (SI in the data layer; conversions live here, never in the pipeline). */54  unit: string;55  /** Display conversion factor applied before formatting (e.g. 1e-9 for t → Gt). Default 1. */56  scale?: number;57}5859export interface CounterModel {60  /** e.g. "co2_emissions_ytd" — key in packages/registry. */61  metricId: string;62  /** Value at the anchor point. */63  anchorValue: number;64  /** ISO 8601 UTC of the anchor point. */65  anchorTime: string;66  /** How the value evolves from the anchor. */67  rateFn: RateFunction;68  /** 90 % CI of the value at anchorTime (widens with model-specific rules client-side). */69  uncertainty?: { low: number; high: number };70  /** Date of the last REAL observation this model is anchored on. */71  observedAt: string;72  /** Key in the source registry. */73  sourceId: string;74  /** e.g. "seasonal-spline-v2" — bump on ANY output-changing modification. */75  modelVersion: string;76  displayHints: DisplayHints;77}7879/** Epoch used to phase seasonal harmonics: 2000-01-01T00:00:00Z, in ms. */80export const SEASONAL_EPOCH_MS = Date.UTC(2000, 0, 1);8182/** Mean Gregorian year, in seconds. Shared by fitting (packages/models) and evaluation. */83export const YEAR_SECONDS = 365.2425 * 86_400;8485/** One UTC day, in seconds. */86export const DAY_SECONDS = 86_400;8788/** One week, in seconds. The seasonal epoch (2000-01-01) is a Saturday — weekly phases are relative to Saturday 00:00 UTC. */89export const WEEK_SECONDS = 604_800;9091/** Parse a strict ISO 8601 UTC timestamp to ms since epoch; throws on invalid input. */92export function parseIsoUtc(iso: string): number {93  const ms = Date.parse(iso);94  if (Number.isNaN(ms)) throw new Error(`Invalid ISO 8601 timestamp: ${iso}`);95  return ms;96}97