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%
2.6 KB · 82 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/models/test/fourier.test.ts6 * Purpose: Golden recovery tests — the Fourier fitter must recover a known synthetic Keeling-style signal7 */89import { describe, expect, it } from "vitest";10import { SEASONAL_EPOCH_MS, YEAR_SECONDS } from "@earth-now/counter";11import { fitFourier } from "../src/index";1213/** Synthetic Keeling curve: 400 ppm at epoch + 2.5 ppm/yr + 3 ppm annual cycle. */14function syntheticCo2(tMs: number): number {15  const tau = (tMs - SEASONAL_EPOCH_MS) / 1000;16  const omega = (2 * Math.PI) / YEAR_SECONDS;17  return 400 + (2.5 / YEAR_SECONDS) * tau + 3 * Math.cos(omega * tau - 0.7);18}1920function monthlySeries(fromYear: number, years: number): Array<{ time: string; value: number }> {21  const out: Array<{ time: string; value: number }> = [];22  for (let y = 0; y < years; y++) {23    for (let m = 0; m < 12; m++) {24      const t = Date.UTC(fromYear + y, m, 15);25      out.push({ time: new Date(t).toISOString(), value: syntheticCo2(t) });26    }27  }28  return out;29}3031describe("fitFourier — synthetic Keeling recovery", () => {32  const observations = monthlySeries(2020, 5);33  const fit = fitFourier({34    observations,35    harmonics: [36      { period: "year", order: 1 },37      { period: "year", order: 2 },38    ],39  });4041  it("recovers the linear trend (~2.5 ppm/yr)", () => {42    expect(fit.trendPerSecond * YEAR_SECONDS).toBeCloseTo(2.5, 2);43  });4445  it("reproduces the signal within 0.05 ppm at arbitrary instants", () => {46    for (const iso of [47      "2021-03-07T12:00:00Z",48      "2022-08-19T00:00:00Z",49      "2024-11-30T06:30:00Z",50      "2025-05-15T00:00:00Z",51    ]) {52      const t = Date.parse(iso);53      expect(fit.valueAt(t)).toBeCloseTo(syntheticCo2(t), 1);54      expect(Math.abs(fit.valueAt(t) - syntheticCo2(t))).toBeLessThan(0.05);55    }56  });5758  it("fits with near-zero residual on noiseless input", () => {59    expect(fit.rmse).toBeLessThan(0.02);60  });6162  it("converts the annual cycle to rate harmonics with the right amplitude", () => {63    // Value amplitude 3 ppm → rate amplitude 3·ω.64    const omega = (2 * Math.PI) / YEAR_SECONDS;65    const fundamental = fit.rateHarmonics.find((h) => h.order === 1);66    expect(fundamental).toBeDefined();67    expect(fundamental!.amplitude).toBeCloseTo(3 * omega, 6);68  });6970  it("rejects underdetermined fits", () => {71    expect(() =>72      fitFourier({73        observations: observations.slice(0, 3),74        harmonics: [75          { period: "year", order: 1 },76          { period: "year", order: 2 },77        ],78      }),79    ).toThrow(/need at least/);80  });81});82