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