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