/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: packages/models/test/fourier.test.ts * Purpose: Golden recovery tests — the Fourier fitter must recover a known synthetic Keeling-style signal */ import { describe, expect, it } from "vitest"; import { SEASONAL_EPOCH_MS, YEAR_SECONDS } from "@earth-now/counter"; import { fitFourier } from "../src/index"; /** Synthetic Keeling curve: 400 ppm at epoch + 2.5 ppm/yr + 3 ppm annual cycle. */ function syntheticCo2(tMs: number): number { const tau = (tMs - SEASONAL_EPOCH_MS) / 1000; const omega = (2 * Math.PI) / YEAR_SECONDS; return 400 + (2.5 / YEAR_SECONDS) * tau + 3 * Math.cos(omega * tau - 0.7); } function monthlySeries(fromYear: number, years: number): Array<{ time: string; value: number }> { const out: Array<{ time: string; value: number }> = []; for (let y = 0; y < years; y++) { for (let m = 0; m < 12; m++) { const t = Date.UTC(fromYear + y, m, 15); out.push({ time: new Date(t).toISOString(), value: syntheticCo2(t) }); } } return out; } describe("fitFourier — synthetic Keeling recovery", () => { const observations = monthlySeries(2020, 5); const fit = fitFourier({ observations, harmonics: [ { period: "year", order: 1 }, { period: "year", order: 2 }, ], }); it("recovers the linear trend (~2.5 ppm/yr)", () => { expect(fit.trendPerSecond * YEAR_SECONDS).toBeCloseTo(2.5, 2); }); it("reproduces the signal within 0.05 ppm at arbitrary instants", () => { for (const iso of [ "2021-03-07T12:00:00Z", "2022-08-19T00:00:00Z", "2024-11-30T06:30:00Z", "2025-05-15T00:00:00Z", ]) { const t = Date.parse(iso); expect(fit.valueAt(t)).toBeCloseTo(syntheticCo2(t), 1); expect(Math.abs(fit.valueAt(t) - syntheticCo2(t))).toBeLessThan(0.05); } }); it("fits with near-zero residual on noiseless input", () => { expect(fit.rmse).toBeLessThan(0.02); }); it("converts the annual cycle to rate harmonics with the right amplitude", () => { // Value amplitude 3 ppm → rate amplitude 3·ω. const omega = (2 * Math.PI) / YEAR_SECONDS; const fundamental = fit.rateHarmonics.find((h) => h.order === 1); expect(fundamental).toBeDefined(); expect(fundamental!.amplitude).toBeCloseTo(3 * omega, 6); }); it("rejects underdetermined fits", () => { expect(() => fitFourier({ observations: observations.slice(0, 3), harmonics: [ { period: "year", order: 1 }, { period: "year", order: 2 }, ], }), ).toThrow(/need at least/); }); });