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.5 KB · 63 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/models/test/filters.test.ts6 * Purpose: Holt-Winters and Kalman tests — nowcasting recovery and observation/forecast blending behavior7 */89import { describe, expect, it } from "vitest";10import { blendWithForecast, fitHoltWinters, kalmanFilter } from "../src/index";1112describe("fitHoltWinters", () => {13  it("recovers a clean level+trend+season series and forecasts it", () => {14    // 4 seasons of monthly data: level 100, trend +1/step, season = 10·sin.15    const m = 12;16    const series: number[] = [];17    for (let i = 0; i < 4 * m; i++) {18      series.push(100 + i + 10 * Math.sin((2 * Math.PI * (i % m)) / m));19    }20    const fit = fitHoltWinters(series, { alpha: 0.5, beta: 0.1, gamma: 0.3, seasonLength: m });21    const truthNext = (h: number) =>22      100 + (series.length + h - 1) + 10 * Math.sin((2 * Math.PI * ((series.length + h - 1) % m)) / m);23    // Exponential smoothing carries a small lag on trending series — accept < 1.524    // on a signal of amplitude 10 with level ~150.25    for (const h of [1, 3, 6]) {26      expect(Math.abs(fit.forecast(h) - truthNext(h))).toBeLessThan(1.5);27    }28  });2930  it("rejects too-short series and bad params", () => {31    expect(() => fitHoltWinters([1, 2, 3], { alpha: 0.5, beta: 0.1, gamma: 0.1, seasonLength: 12 })).toThrow();32    expect(() =>33      fitHoltWinters(new Array(30).fill(1), { alpha: 1.5, beta: 0.1, gamma: 0.1, seasonLength: 12 }),34    ).toThrow();35  });36});3738describe("kalmanFilter + blendWithForecast", () => {39  it("converges to a constant signal", () => {40    const noisy = [10.4, 9.7, 10.1, 10.3, 9.9, 10.0, 10.2, 9.8, 10.0, 10.1];41    const steps = kalmanFilter(noisy, {42      processVariance: 0.001,43      measurementVariance: 0.25,44      initialValue: 0,45      initialVariance: 100,46    });47    const last = steps[steps.length - 1]!;48    expect(last.value).toBeCloseTo(10, 0);49    expect(last.variance).toBeLessThan(0.1);50  });5152  it("blend tracks the observation near it and the forecast far out", () => {53    const lastState = { value: 100, variance: 1 };54    const forecast = { value: 110, variance: 4 };55    const near = blendWithForecast(lastState, 1, 0.01, forecast);56    const far = blendWithForecast(lastState, 10_000, 0.01, forecast);57    // Near: mostly the observation; far: mostly the forecast.58    expect(near.value).toBeLessThan(103);59    expect(far.value).toBeGreaterThan(106);60    expect(near.variance).toBeLessThan(far.variance);61  });62});63