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+ const knownOutliers = [
25+ '9455920' ,
26+ '9455963'
27+ // ...
28+ ]
29+
30+ describe ( 'NOAA benchmarks' , ( ) => {
31+ noaa . forEach ( ( station ) => {
32+ it (
33+ station . source . id ,
34+ async ( ) => {
35+ const { harmonics, levels } = await getStation ( station . source . id )
36+ const tideStation = tidePrediction ( harmonics . HarmonicConstituents )
37+
38+ // No predictions available
39+ if ( ! levels . predictions ) return
40+
41+ let count = 0
42+ let sumError = 0
43+ let sumAbsError = 0
44+ let sumSqError = 0
45+
46+ levels . predictions . forEach ( ( prediction : { t : string ; v : string } ) => {
47+ const expected = parseFloat ( prediction . v )
48+
49+ const { level : actual } = tideStation . getWaterLevelAtTime ( {
50+ time : new Date ( prediction . t )
51+ } )
52+
53+ const error = actual - expected
54+
55+ count += 1
56+ sumError += error
57+ sumAbsError += Math . abs ( error )
58+ sumSqError += error * error
59+ } )
60+
61+ const mae = sumAbsError / count
62+ const rmse = Math . sqrt ( sumSqError / count )
63+ const bias = sumError / count
64+
65+ if ( knownOutliers . includes ( station . source . id ) ) {
66+ expect (
67+ mae ,
68+ `Remove ${ station . source . id } from known outliers`
69+ ) . toBeGreaterThan ( 0.15 )
70+ expect ( mae ) . toBeLessThan ( 0.3 )
71+ } else {
72+ expect ( mae ) . toBeLessThanOrEqual ( 0.15 )
73+ }
974
10- const makeRequest = async ( url : string ) => {
75+ stats . push ( { station : station . source . id , count, mae, rmse, bias } )
76+ } ,
77+ 20000
78+ )
79+ } )
80+
81+ afterAll ( async ( ) => {
82+ // Write stats to file for later analysis
83+ const summary = createWriteStream ( 'noaa-benchmarks.csv' )
84+ summary . write ( 'station,count,mae_m,rmse_m,bias_m\n' )
85+ stats . forEach ( ( { station, count, mae, rmse, bias } ) => {
86+ summary . write (
87+ [ station , count , mae . toFixed ( 4 ) , rmse . toFixed ( 4 ) , bias . toFixed ( 4 ) ] . join (
88+ ','
89+ ) + '\n'
90+ )
91+ } )
92+
93+ // Baseline expectations based on current performance. The goal should be to move these toward zero over time.
94+ const maeValues = stats . map ( ( s ) => s . mae ) . sort ( ( a , b ) => a - b )
95+ const medianMAE = maeValues [ Math . floor ( stats . length / 2 ) ]
96+ const p90MAE = maeValues [ Math . floor ( stats . length * 0.9 ) ]
97+ const p95MAE = maeValues [ Math . floor ( stats . length * 0.95 ) ]
98+
99+ expect ( medianMAE ) . toBeLessThan ( 0.03 ) // 3 cm
100+ expect ( p90MAE ) . toBeLessThan ( 0.06 ) // 6 cm
101+ expect ( p95MAE ) . toBeLessThan ( 0.08 ) // 8 cm
102+ expect ( stats . length ) . toBeGreaterThanOrEqual ( 1187 ) // Ensure enough stations were tested
103+
104+ // Ensure only a small number of stations have very high errors
105+ expect ( stats . filter ( ( { mae } ) => mae > 0.1 ) . length ) . toBeLessThan ( 10 )
106+ } )
107+ } )
108+
109+ async function makeRequest ( url : string ) {
11110 const res = await fetch ( url )
12111 if ( ! res . ok ) throw new Error ( `Request failed with status ${ res . status } ` )
13112 return res . json ( )
14113}
15114
16- const getStation = async ( station : string ) => {
115+ async function getStation ( station : string ) {
17116 const filePath = `./.test-cache/${ station } .json`
18117
19118 try {
@@ -36,18 +135,3 @@ const getStation = async (station: string) => {
36135 return data
37136 }
38137}
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