DEV Community

Bryan Clark
Bryan Clark

Posted on • Originally published at engineering.sailingnaturali.com

Tide predictions read higher than the tables? Suspect the datum

TL;DR — If a harmonic tide prediction disagrees with the official tide
tables by a constant offset at a station, the algorithm is fine; the two
heights are measured from different zeros. Canadian chart datum is not
LAT — at CHS stations it sits up to ~0.43 m above it, and the per-station
offset is published in the CHS station /metadata API.
Jump to the fix.

Worst-case height error against the official CHS tide tables, per station, before and after normalizing both sides to LAT. Sidney BC falls from 0.35 to 0.48 m down to 8 cm or less, Point Atkinson from 0.13 m to 5 cm, Halifax from 0.38 m to 13 cm.

Tide height numbers are meaningless without a stated vertical datum. Every
"3.2 m at 14:40" is really "3.2 m above some agreed zero", and there are
half a dozen zeros in circulation: mean sea level (MSL), mean lower low water
(MLLW), lowest astronomical tide (LAT), and each hydrographic office's own
chart datum. Compare two predictions that don't share a zero and you get a
bug report that looks exactly like broken code.

This is the story of one of those bug reports — against
signalk-tides, the SignalK
tide plugin — that turned out to be a datum mismatch, and of the
cross-checking that then turned up a genuinely broken station in the upstream
tide database.

Problem

signalk-tides v2 (2.0.0-beta.1,
after #78 went
Neaps-only) predicts tides offline from harmonic constituents — no NOAA,
no WorldTides, no API keys. It runs on our boat's SignalK server and answers
for any position:

curl "http://<signalk-host>:3000/signalk/v2/api/tides/extremes?latitude=48.649&longitude=-123.393"
Enter fullscreen mode Exit fullscreen mode

Cross-checking it against the official CHS predictions for Sidney, BC
(station 07260) before trusting it for anchoring math:

validated against official CHS predictions, 2026-06-12

Sidney BC (07260):          every high and low +0.35 to +0.48 m HIGH
                            timing within ~16 min
Point Atkinson BC (07795):  heights within 0.03–0.13 m
                            timing within ~6 min
Enter fullscreen mode Exit fullscreen mode

At Sidney, every single high and low read a consistent third-to-half metre
higher than the published tables — while Point Atkinson, checked the same
way on the same day, was nearly spot on. Timing was fine at both. The obvious
conclusion — the one any user would file
as an issue — is "the plugin over-reads by half a metre; the harmonic engine
is broken."

The same symptom was already sitting in the Neaps tracker as
openwatersio/neaps#223,
with a Halifax example:

// Neaps (TICON constituents), Halifax NS, 2026-02-20, datum: "MLLW"
{ t: 'Fri, 20 Feb 2026 01:38:10 GMT', level:  1.507 }
{ t: 'Fri, 20 Feb 2026 07:51:58 GMT', level: -0.068 }
{ t: 'Fri, 20 Feb 2026 13:51:17 GMT', level:  1.474 }
{ t: 'Fri, 20 Feb 2026 20:08:29 GMT', level: -0.136 }
Enter fullscreen mode Exit fullscreen mode
// CHS official wlp-hilo predictions, same station, same day
{ "eventDate": "2026-02-20T01:36:00Z", "value": 1.817 }
{ "eventDate": "2026-02-20T07:57:00Z", "value": 0.310 }
{ "eventDate": "2026-02-20T13:46:00Z", "value": 1.774 }
{ "eventDate": "2026-02-20T20:09:00Z", "value": 0.181 }
Enter fullscreen mode Exit fullscreen mode

Times agree to minutes. Levels are ~0.3 m apart — and uniformly apart.

Diagnosis

That uniformity is the whole diagnosis. Harmonic synthesis produces an
oscillation about the station's mean water level; getting from there to
"height above chart datum" is a per-station constant from a datum table, added
at the end. So:

  • A constituent error (bad amplitudes/phases) varies with the tide — big at springs, small at neaps, different at highs vs lows.
  • A datum error is DC — the same offset on every prediction, at every tide.

A constant offset means the sine waves are right and the zero is wrong.

Here the zeros genuinely differ. Neaps serves heights in the station's
chart_datum from the TICON-based tide database,
which labels Canadian stations "LAT" (lowest astronomical tide). CHS tide
tables are referenced to CHS chart datum. And the common assumption that
Canadian chart datum ≈ LAT turns out to be false at station level: CHS chart
datum sits above LAT by a per-station amount — 0.43 m at Sidney.

Vertical datum ladder at Sidney BC: CHS chart datum, the zero of the tide tables, sits 0.43 m above lowest astronomical tide, the zero of the harmonic record, so the same water surface is reported 0.43 m higher by the harmonic engine.

You don't have to take that on faith; CHS publishes every station's vertical
ladder in the IWLS /metadata endpoint:

# Sidney BC (station code 07260)
curl -s "https://api-iwls.dfo-mpo.gc.ca/api/v1/stations/5cebf1df3d0f4a073c4bbd26/metadata"
# resolve the heightTypeIds via:
curl -s "https://api-iwls.dfo-mpo.gc.ca/api/v1/height-types"
Enter fullscreen mode Exit fullscreen mode
// metadata "heights", values in metres relative to CHS chart datum
{ "code": "MWL",   "value":  2.09 }   // mean water level
{ "code": "LLWLT", "value": -0.22 }   // lower low water, large tide
{ "code": "LAT",   "value": -0.43 }   // lowest astronomical tide
{ "code": "HAT",   "value":  3.53 }
Enter fullscreen mode Exit fullscreen mode

LAT is 0.43 m below chart datum at Sidney — exactly the amount by which the
plugin's LAT-referenced predictions "over-read" against the chart-datum
tables. (Note LLWLT ≠ 0 either: even the textbook definition of Canadian
chart datum doesn't sit at the datum's actual zero here.)

What we tried (and why it failed)

Suspecting the constituents. The natural first read — offline harmonics
from a community database vs the hydrographic office, of course the
constituents are worse. But lining up TICON's own datum table against CHS's
published offsets shows the two agree almost exactly on where MSL sits above
LAT:

Station TICON MSL→LAT CHS metadata (MWL above CD + CD above LAT) agreement
Sidney BC 2.522 m 2.09 + 0.43 = 2.52 m ~2 mm
Point Atkinson BC 3.144 m 3.09 + 0.08 = 3.17 m ~3 cm
Halifax NS 1.152 m 1.03 + 0.09 = 1.12 m ~3 cm

The physics agreed to centimetres. It was bookkeeping.

Comparing "MLLW to MLLW". The Halifax example above asked Neaps for
datum: "MLLW" and still missed by 0.3 m — because CHS wlp-hilo values
aren't MLLW-referenced (they're chart datum), and TICON's MLLW is its own
computed statistic. Two datums with the same name from two sources are not
the same zero.

Trusting the chart_datum: "LAT" label. The station records say chart
datum is LAT, so diffing against the tables should be safe… except CHS chart
datum isn't LAT (see above). A label is a claim, not a measurement.

The fix

Normalize both sides to one explicit datum before comparing — or before doing
any arithmetic at all with someone else's heights:

// CHS /metadata "heights": LAT is relative to chart datum (negative)
const lat = heights.find((h) => h.code === "LAT")!.value; // Sidney: -0.43

// shift CHS chart-datum predictions onto LAT…
const chsAboveLAT = chsPrediction.value - lat;

// …and ask the harmonic side for the same zero explicitly
const { extremes } = station.getExtremesPrediction({ start, end, datum: "LAT" });
Enter fullscreen mode Exit fullscreen mode

Datum-aware, the "half a metre of error" collapses: Sidney residuals drop to
≤ ~8 cm, Point Atkinson ≤ ~5 cm, Halifax ~10–13 cm, timing within minutes
everywhere.

That normalization then became a standing benchmark, contributed upstream as
openwatersio/neaps#272: all
226 Canadian MEDS-sourced TICON stations diffed against live CHS predictions
(the TICON ids embed the CHS station code — sidney_bc-7260-can-meds
so the join is free). Across the 180 stations that line up, full-year
height MAE is p50 7.3 cm / p90 16.4 cm / p95 25.7 cm, timing ~23 min
(p95 of median |Δt|), re-run monthly in CI against fresh CHS predictions.

The bonus find: a station no datum can explain

The benchmark's per-station CSV kicked out one outlier that is not a datum
problem: cape_dor-240-can-meds (Cape d'Or, Bay of Fundy) carries a +3.5 m
constant bias
— and unlike the others, its harmonic record itself is
implausible. Filed upstream as
openwatersio/tide-database#93
(still open as of this writing):

TICON HAT−LAT:              18.58 m
CHS published HAT−LAT:      12.40 m   (same location)
one-year predicted range:   17.4 m    — would exceed the world-record
                                        range at Burntcoat Head
SA constituent:             2.08 m    — 4× the next-highest Canadian
                                        station; implausible for an
                                        open-marine site
Enter fullscreen mode Exit fullscreen mode

Dropping the suspect SA term only brings the predicted range down to 16.0 m,
so the whole constituent set reads as over-amplified, not one bad row. The
point isn't this station — it's that benchmarking against published ground
truth catches upstream data errors, not just your own bugs
. Nothing in the
code path was wrong; the input data was.

Why it matters / gotchas

  • Never trust a height without asking "above what?" Any pipeline that passes tide heights between systems should carry the datum with the number, and any comparison should normalize first.
  • The error signature tells you where to look. Constant offset across all tides → datum/bookkeeping. Error that scales with the tide → constituents. This one rule would have saved the original "water levels differ from the public tide predictors" investigation a lot of head-scratching.
  • The optimistic direction is the unsafe one. Charted depths are below chart datum. Add a LAT-referenced height to a charted depth where chart datum sits 0.4 m above LAT and you believe in 0.4 m of water that isn't there — exactly the wrong direction for under-keel clearance. Our agent tooling carries this as an explicit caveat and pads margins accordingly.
  • Per-station, not a constant. The CHS chart-datum-to-LAT offset is 0.43 m at Sidney, 0.08 m at Point Atkinson, 0.09 m at Halifax. There is no single fudge factor; the station /metadata is the source of truth.

Close

This came out of wiring signalk-tides v2 into the AI-ops stack we run on an
all-electric sailing catamaran — offline tide predictions the boat can answer
from with no internet, which is only useful if you've checked them against
the official tables first. The plugin, the
Neaps engine, and the
tide database are all open
source and better for the benchmarking.

Related: verifying forecasts against what a buoy is actually measuring is
the same habit applied to weather — see
why generic weather MCPs fail for marine navigation.

Top comments (0)