Skip to content

Commit 3ef68e8

Browse files
committed
Move benchmarks out of tests and run as a separate action
1 parent f9dae37 commit 3ef68e8

5 files changed

Lines changed: 145 additions & 132 deletions

File tree

.github/workflows/test.yml

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,8 @@ jobs:
2222
- name: Install modules
2323
run: npm install
2424

25-
- name: Cache test data
26-
id: test-cache
27-
uses: actions/cache@v5
28-
with:
29-
path: .test-cache
30-
key: ${{ runner.os }}-test-cache
31-
3225
- name: Test build
33-
run: npm run build:all
26+
run: npm run build
3427

3528
- name: Test
3629
run: npm run coverage
@@ -41,8 +34,29 @@ jobs:
4134
fail_ci_if_error: true
4235
files: ./coverage/coverage-final.json
4336

44-
- name: Upload NOAA summary
37+
benchmarks:
38+
runs-on: ubuntu-latest
39+
steps:
40+
- uses: actions/checkout@v6
41+
42+
- name: Install modules
43+
run: npm install
44+
45+
- name: Build
46+
run: npm run build
47+
48+
- name: Cache benchmark data
49+
id: benchmark-cache
50+
uses: actions/cache@v5
51+
with:
52+
path: .test-cache
53+
key: ${{ runner.os }}-benchmark-cache
54+
55+
- name: Run benchmarks
56+
run: npm run benchmarks
57+
58+
- name: Upload benchmark results
4559
uses: actions/upload-artifact@v6
4660
with:
47-
name: noaa-benchmarks
48-
path: noaa-benchmarks.csv
61+
name: benchmarks
62+
path: benchmarks/*.csv

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ junit.xml
88
dist
99
*.tsbuildinfo
1010
package-lock.json
11+
benchmarks/*.csv

benchmarks/noaa.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { expect } from 'vitest'
2+
import fs from 'fs/promises'
3+
import tidePredictor from '@neaps/tide-predictor'
4+
import db from '@neaps/tide-database'
5+
import { createWriteStream } from 'fs'
6+
import { join } from 'path'
7+
8+
const __dirname = new URL('.', import.meta.url).pathname
9+
10+
const stations = db.filter(
11+
(station) =>
12+
// TODO: Update this to test subordinate stations too.
13+
// Need to switch from `getWaterLevelAtTime` to `getExtremesPrediction` and compare time/level.
14+
station.type === 'reference' &&
15+
station.source.source_url.includes('noaa.gov')
16+
)
17+
18+
// Create a directory for test cache
19+
await fs.mkdir('./.test-cache', { recursive: true })
20+
21+
interface Stat {
22+
station: string
23+
count: number
24+
mae: number
25+
rmse: number
26+
bias: number
27+
}
28+
29+
const stats: Stat[] = []
30+
31+
console.log(`Testing tide predictions against ${stations.length} NOAA stations`)
32+
33+
for (const station of stations) {
34+
// Catch error and return no levels if failing to fetch data. There is a test later to ensure enough stations are tested.
35+
const { levels } = await getStation(station.source.id).catch(() => ({
36+
levels: {}
37+
}))
38+
const tideStation = tidePredictor(station.harmonic_constituents, {
39+
phaseKey: 'phase_UTC'
40+
})
41+
42+
// No predictions available
43+
if (!levels.predictions) continue
44+
45+
let count = 0
46+
let sumError = 0
47+
let sumAbsError = 0
48+
let sumSqError = 0
49+
50+
levels.predictions.forEach((prediction: { t: string; v: string }) => {
51+
const expected = parseFloat(prediction.v)
52+
53+
const { level: actual } = tideStation.getWaterLevelAtTime({
54+
time: new Date(prediction.t)
55+
})
56+
57+
const error = actual - expected
58+
59+
count += 1
60+
sumError += error
61+
sumAbsError += Math.abs(error)
62+
sumSqError += error * error
63+
})
64+
65+
const mae = sumAbsError / count
66+
const rmse = Math.sqrt(sumSqError / count)
67+
const bias = sumError / count
68+
69+
stats.push({ station: station.source.id, count, mae, rmse, bias })
70+
process.stdout.write('.')
71+
}
72+
73+
// Write stats to file for later analysis
74+
const summary = createWriteStream(join(__dirname, 'noaa.csv'))
75+
summary.write('station,count,mae_m,rmse_m,bias_m\n')
76+
stats.forEach(({ station, count, mae, rmse, bias }) => {
77+
summary.write(
78+
[station, count, mae.toFixed(4), rmse.toFixed(4), bias.toFixed(4)].join(
79+
','
80+
) + '\n'
81+
)
82+
})
83+
summary.end()
84+
85+
// Baseline expectations based on current performance. The goal should be to move these toward zero over time.
86+
const maeValues = stats.map((s) => s.mae).sort((a, b) => a - b)
87+
const medianMAE = maeValues[Math.floor(stats.length / 2)]
88+
const p90MAE = maeValues[Math.floor(stats.length * 0.9)]
89+
const p95MAE = maeValues[Math.floor(stats.length * 0.95)]
90+
91+
expect(medianMAE, 'MAE p50').toBeLessThan(0.03) // 3 cm
92+
expect(p90MAE, 'MAE p90').toBeLessThan(0.06) // 6 cm
93+
expect(p95MAE, 'MAE p95').toBeLessThan(0.08) // 8 cm
94+
expect(stats.length, 'Total stations').toBeGreaterThanOrEqual(1100) // Ensure enough stations were tested
95+
96+
async function makeRequest(url: string) {
97+
const res = await fetch(url)
98+
if (!res.ok) throw new Error(`Request failed with status ${res.status}`)
99+
return res.json()
100+
}
101+
102+
async function getStation(station: string) {
103+
const filePath = `./.test-cache/${station}.json`
104+
105+
try {
106+
return await fs.readFile(filePath, 'utf-8').then((data) => JSON.parse(data))
107+
} catch {
108+
const levels = await makeRequest(
109+
`https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=recent&station=${station}&product=predictions&datum=MTL&time_zone=gmt&units=metric&format=json`
110+
)
111+
112+
const data = { levels }
113+
await fs.writeFile(filePath, JSON.stringify(data))
114+
return data
115+
}
116+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
"scripts": {
55
"test": "vitest",
66
"coverage": "vitest run --coverage",
7-
"build:all": "npm run build --workspaces --if-present",
7+
"build": "npm run build --workspaces --if-present",
88
"lint": "eslint && prettier --check .",
9-
"format": "prettier --write ."
9+
"format": "prettier --write .",
10+
"benchmarks": "node benchmarks/noaa.ts"
1011
},
1112
"devDependencies": {
1213
"@eslint/js": "^9.39.2",

packages/tide-predictor/test/noaa.test.ts

Lines changed: 0 additions & 119 deletions
This file was deleted.

0 commit comments

Comments
 (0)