Appearance
Tides
sextant/tides gives the boat a tide page: for every port near the current position, what the water is doing now, when the next high and low water are, how big the tide is today, and a curve to plot.
It publishes all the stations it found, not one, sorted nearest first. The screen picks — the crew usually wants the port they are heading for, not the one they happen to be closest to.
Read this before using the numbers
Two properties of this data set are not negotiable and change how the numbers must be read.
1. The heights are not referenced to a chart datum. Every station's height is a harmonic sum with no constant term, so it oscillates about zero, and roughly half the time it is negative. A published height of −1.8 m does not mean 1.8 m below the charted sounding; it means 1.8 m below the local mean water level. The database has a datum column and it is empty for every one of the 8117 stations. There is no offset anywhere in the daemon that would let you convert.
So: do not add a published height to a charted depth. For under-keel clearance, use the official tide table for the port.
2. What is trustworthy is everything datum-independent — the timing of high and low water, the range between them, and the tidal coefficient. Those are what the tide page is genuinely good for, and they are what MUXEN validates against official predictions.
What a station carries
| Field | Unit | Notes |
|---|---|---|
name | — | port name from the source station list |
country | ISO alpha-3 | optional; 129 of the 8117 stations have none |
latitude, longitude | degrees | the station's own position |
distanceNm | nautical miles | great-circle distance from the boat |
height | metres | the level now, about local mean water — see above |
nextHighTide | ISO-8601 UTC | optional |
nextHighTideHeight | metres | optional, present with nextHighTide |
nextLowTide | ISO-8601 UTC | optional |
nextLowTideHeight | metres | optional, present with nextLowTide |
tidalCoefficient | integer | optional |
curve | array of 216 points | always present |
Unlike the sun and moon reports, the tide times are published only as ISO-8601 strings; there is no …Timestamp sibling here.
json
{
"data": {
"stations": [
{
"name": "Brest",
"country": "FRA",
"latitude": 48.3833,
"longitude": -4.495,
"distanceNm": 2.3,
"height": 1.42,
"nextHighTide": "2026-08-16T16:42:00Z",
"nextHighTideHeight": 2.84,
"nextLowTide": "2026-08-16T22:58:00Z",
"nextLowTideHeight": -2.31,
"tidalCoefficient": 95,
"curve": [
{ "t": "2026-08-16T02:30:00Z", "h": -0.87 },
{ "t": "2026-08-16T02:40:00Z", "h": -0.62 }
]
}
]
},
"metadata": { "rxDate": "2026-08-16T14:30:00Z", "expireAfterSec": 3600 }
}Which stations are published
The daemon queries an R-tree spatial index for every station inside a bounding box, then filters exactly by great-circle distance against --tide-radius-nm (default 50.0). Stations are returned sorted by distanceNm, nearest first.
The bounding box is built with the cosine of the latitude applied to the longitude half-width, so a 50 NM radius really is 50 NM east-west at 60° north and not 25. Near the poles the longitude sweep is widened to the full circle and the exact distance filter does the work. A box that would straddle the antimeridian is likewise widened rather than split.
How many stations you get depends entirely on the coast. Mid-ocean returns none. A fjord coast returns a great many: the test suite pins 83 stations within 50 NM of Bergen. Each one carries a 216-point curve, so the payload grows linearly with the count — a few hundred kilobytes is normal on a busy coast, and that is the reason --tide-radius-nm exists.
If no station is inside the radius, nothing is published at all — not an empty array. The previous retained payload therefore stays on the broker, which on a long offshore passage means the tide page keeps showing the last coast you left. The log line, with --verbose-tides, is:
Tides: No tide stations found within 50 NMheight and the curve
height is the harmonic sum evaluated at the moment of the recompute.
curve is the same sum evaluated at 216 points: from −12 h to +24 h around that moment, every 10 minutes. Each point is { "t": "<ISO-8601 UTC>", "h": <metres> }, with t an absolute timestamp so a plot needs no arithmetic. The window is deliberately asymmetric — enough history to show the shape you have just come through, and a full day ahead to plan against.
The curve is what makes the report expensive: 216 evaluations of a harmonic sum — up to 34 constituents, 25 on average — per station, for every station in radius. Hence the separate throttle.
Recompute throttle
The tides report has its own minimum interval, --tide-recompute-min, default 30 minutes, checked inside the report itself. The central scheduler still fires on drift or on --recompute-interval-min (default 15), but the tides report ignores triggers that arrive too soon after the last computation.
The practical effects:
- With the defaults, the tide page updates every 30 minutes, while the sun and moon pages update every 15.
heighton the page can be up to half an hour old. On a 6 m range that is a meaningful amount of water.- A boat oscillating across a station-set boundary — anchored near the edge of the radius, say — does not churn the payload.
The tidal coefficient
tidalCoefficient is the French convention for "how big is today's tide", and it is the number a French almanac prints:
| Coefficient | Tide |
|---|---|
| ~20 | smallest neap |
| 45 | mean neap |
| 100 | mean spring |
| ~120 | large spring |
Values normally fall between about 20 and 120; the daemon clamps to 0…200 so an extreme station cannot emit nonsense.
It is computed as
coefficient = (nextHighTideHeight − nextLowTideHeight) / meanSpringRange × 100clamped to the range 0…200, where the mean spring range is 2 × (A_M2 + A_S2) from the station's own harmonic constants. Because it is a ratio of ranges, it is datum-independent and therefore one of the trustworthy numbers.
It is absent when it cannot be formed honestly:
- when either the next high or the next low was not found — a range built from one real extremum and one missing one is not a tidal range;
- when the station has no usable mean spring range, which is what a diurnal-dominant or micro-tidal port looks like. Two of the 8117 stations have no mean spring range recorded at all.
Finding the extremes
High and low water are found by scanning the computed curve for turning points, with a prominence filter: an extremum is accepted only if it stands out from its neighbourhood by more than the greater of 10 % of the curve's full range and 0.05 m.
That filter exists because shallow-water overtides — M4, MS4, M6 — put small wobbles on the curve near slack water. Without it, a double high water at a place like Southampton, or ordinary harmonic noise in a micro-tidal Mediterranean port, produces a "high tide" 20 minutes after the real one and 3 cm lower.
Consequences to expect:
- In a genuinely micro-tidal port the whole curve can fail the prominence floor, and both
nextHighTideandnextLowTideare absent. Thecurveis still published and still correct. - The search covers only the 36-hour window of the curve, so a very long interval between extremes can leave one of them unfound.
Accuracy
The harmonic constants are extracted from FES2022b, a global ocean tide model distributed by AVISO/CNES, interpolated at each port's coordinates. That is the essential caveat: they are a global model sampled at a port, not the port's own observed harmonic constants from a hydrographic office.
MUXEN validates the result against official predictions — SHOM for French ports, NOAA for American ones — for Brest, Saint-Nazaire, La Rochelle, Saint-Malo, Lorient, Canet, Palermo, Fethiye, Bermuda and Fort Lauderdale, on a common reference date. The tolerances those tests enforce are the honest statement of accuracy:
| Quantity | Tolerance against the official prediction |
|---|---|
| Time of high water | within 60 minutes |
| Time of low water | within 60 minutes |
| Tidal range | within 0.50 m |
| Tidal coefficient | within 15 |
Absolute height is not validated at all, and cannot be, because of the datum.
Sixty minutes is a wide tolerance, and it is a test bound rather than a typical error — but it is the number MUXEN stands behind. For anything where the timing of the tide is a safety matter — a bar, a sill, a lock, a tidal gate — use the official tide table for that port.
The other structural limitation is that FES2022b is an ocean model. It represents the open-sea tide well and does not resolve estuaries, rivers, narrow inlets or the shallow-water distortion that dominates inside them. A station at the head of an estuary is being given the tide of the sea outside it.
When the report does not appear
| Symptom | Cause |
|---|---|
Tides: Database not found: <path> at startup, no sextant/tides ever | muxen-sextant-database is not installed, or --tides-db-path is wrong. The report is not created at all; the rest of the daemon runs normally. |
| Startup line present, but no topic | no station within --tide-radius-nm, or no position yet |
| Topic present but stale | the 30-minute throttle, or the retained payload from a previous run |
The database is opened read-only on every computation and closed again; the daemon never writes to it.
Its schema and the harmonic arithmetic are documented in internal/tide-harmonics.md.
