1- import { describe , it , expect } from 'vitest'
1+ import { describe , it , expect , afterAll } from 'vitest'
22import fs from 'fs/promises'
33import tidePrediction from '../src/index.js'
4+ import db from '@neaps/tide-stations'
5+ import { createWriteStream } from 'fs'
6+
7+ const noaa = db . filter ( ( station ) =>
8+ station . source . source_url . includes ( 'noaa.gov' )
9+ )
410
511// Create a directory for test cache
612await fs . mkdir ( './.test-cache' , { recursive : true } )
713
8- const stations = [ '9413450' , '9411340' , '2695535' , '8761724' , '8410140' ]
14+ interface Stat {
15+ station : string
16+ count : number
17+ mae : number
18+ rmse : number
19+ bias : number
20+ }
21+
22+ const stats : Stat [ ] = [ ]
23+
24+ describe ( 'NOAA benchmarks' , ( ) => {
25+ noaa . forEach ( ( station ) => {
26+ it (
27+ station . source . id ,
28+ async ( ) => {
29+ const { harmonics, levels } = await getStation ( station . source . id )
30+ const tideStation = tidePrediction ( harmonics . HarmonicConstituents )
31+
32+ // No predictions available
33+ if ( ! levels . predictions ) return
34+
35+ let count = 0
36+ let sumError = 0
37+ let sumAbsError = 0
38+ let sumSqError = 0
39+
40+ levels . predictions . forEach ( ( prediction : { t : string ; v : string } ) => {
41+ const expected = parseFloat ( prediction . v )
42+
43+ const { level : actual } = tideStation . getWaterLevelAtTime ( {
44+ time : new Date ( prediction . t )
45+ } )
46+
47+ const error = actual - expected
48+
49+ count += 1
50+ sumError += error
51+ sumAbsError += Math . abs ( error )
52+ sumSqError += error * error
53+ } )
54+
55+ const mae = sumAbsError / count
56+ const rmse = Math . sqrt ( sumSqError / count )
57+ const bias = sumError / count
958
10- const makeRequest = async ( url : string ) => {
59+ stats . push ( { station : station . source . id , count, mae, rmse, bias } )
60+ } ,
61+ 20000
62+ )
63+ } )
64+
65+ afterAll ( async ( ) => {
66+ // Write stats to file for later analysis
67+ const summary = createWriteStream ( 'noaa-benchmarks.csv' )
68+ summary . write ( 'station,count,mae_m,rmse_m,bias_m\n' )
69+ stats . forEach ( ( { station, count, mae, rmse, bias } ) => {
70+ summary . write (
71+ [ station , count , mae . toFixed ( 4 ) , rmse . toFixed ( 4 ) , bias . toFixed ( 4 ) ] . join (
72+ ','
73+ ) + '\n'
74+ )
75+ } )
76+
77+ // Baseline expectations based on current performance. The goal should be to move these toward zero over time.
78+ const maeValues = stats . map ( ( s ) => s . mae ) . sort ( ( a , b ) => a - b )
79+ const medianMAE = maeValues [ Math . floor ( stats . length / 2 ) ]
80+ const p90MAE = maeValues [ Math . floor ( stats . length * 0.9 ) ]
81+ const p95MAE = maeValues [ Math . floor ( stats . length * 0.95 ) ]
82+
83+ expect ( medianMAE ) . toBeLessThan ( 0.03 ) // 3 cm
84+ expect ( p90MAE ) . toBeLessThan ( 0.06 ) // 6 cm
85+ expect ( p95MAE ) . toBeLessThan ( 0.08 ) // 8 cm
86+ expect ( stats . length ) . toBeGreaterThanOrEqual ( 1178 ) // Ensure enough stations were tested
87+ } )
88+ } )
89+
90+ async function makeRequest ( url : string ) {
1191 const res = await fetch ( url )
1292 if ( ! res . ok ) throw new Error ( `Request failed with status ${ res . status } ` )
1393 return res . json ( )
1494}
1595
16- const getStation = async ( station : string ) => {
96+ async function getStation ( station : string ) {
1797 const filePath = `./.test-cache/${ station } .json`
1898
1999 try {
@@ -36,18 +116,3 @@ const getStation = async (station: string) => {
36116 return data
37117 }
38118}
39-
40- describe ( 'Results compare to NOAA' , ( ) => {
41- stations . forEach ( ( station ) => {
42- it ( `it compares with station ${ station } ` , async ( ) => {
43- const { harmonics, levels } = await getStation ( station )
44- const tideStation = tidePrediction ( harmonics . HarmonicConstituents )
45- levels . predictions . forEach ( ( prediction : { t : string ; v : string } ) => {
46- const neapsPrediction = tideStation . getWaterLevelAtTime ( {
47- time : new Date ( prediction . t )
48- } )
49- expect ( neapsPrediction . level ) . toBeCloseTo ( parseFloat ( prediction . v ) , 0 )
50- } )
51- } , 20000 )
52- } )
53- } )
0 commit comments