Skip to content

Tidal Phase Classification

The tidal_phase tool does not just look up a value from NOAA. It runs a classification algorithm locally, using NOAA’s high/low predictions as input. This page explains how that algorithm works, why it exists, and what its outputs mean.

All of this logic lives in tidal.py — a pure-Python module with no I/O, no async, and no FastMCP dependencies. It takes datetimes and numbers in, and returns classification results out.

Tides follow a roughly sinusoidal cycle. At any given moment, the water is doing one of four things:

Flood

The tide is rising. Water moves shoreward, into estuaries and harbors. This is the period after a low tide and before the next high tide. Current flows inward.

Ebb

The tide is falling. Water moves seaward, draining out of estuaries and harbors. This is the period after a high tide and before the next low tide. Current flows outward.

Slack High

The tide has just reached its peak or is about to. Current is minimal — the water is essentially pausing before reversing direction. This is the transition from flood to ebb.

Slack Low

The tide has just reached its trough or is about to. Current is minimal. This is the transition from ebb to flood.

The classification algorithm maps any timestamp to one of these four phases, plus an "unknown" fallback when data is insufficient.

The algorithm takes two inputs: a timestamp (now) and a list of high/low events from NOAA’s hilo predictions. It produces a phase label, human-readable description, timing details, and a progress percentage.

  1. Parse hilo predictionsparse_hilo_predictions() converts raw NOAA response dicts (with string timestamps and string values) into typed records with Python datetime objects and float values. It filters out any entries missing a "type" field and sorts by time.

    # Input from NOAA
    {"t": "2026-02-21 04:30", "v": "4.521", "type": "H"}
    # After parsing
    {"dt": datetime(2026, 2, 21, 4, 30), "v": 4.521, "type": "H"}
  2. Find bracketing events — The algorithm scans the sorted event list to find two events: the most recent one before now (the previous event) and the first one after now (the next event). These two events bracket the current moment in the tidal cycle.

  3. Check slack windows — Before classifying ebb or flood, the algorithm checks whether now falls within a slack window around any high or low event. This check takes priority because slack periods are short and operationally important.

  4. Classify by bracketing event types — If not in a slack window, the phase is determined by what kind of events bracket the current time:

    • Previous = High, Next = Low: ebb (falling)
    • Previous = Low, Next = High: flood (rising)
  5. Calculate progress — Linear interpolation between the previous and next events gives a percentage indicating how far through the current phase the tide has progressed.

The slack window is defined by a single constant:

SLACK_WINDOW_MIN = 30 # minutes

If the current time is within 30 minutes of a high tide event (either just past it or approaching it), the phase is slack_high. Within 30 minutes of a low tide event, the phase is slack_low.

The slack check runs before the ebb/flood classification. This means a timestamp that is 20 minutes after a high tide will be classified as slack_high, not ebb, even though the water has technically started falling. This is intentional — from a practical standpoint (navigation, fishing, crabbing), the current near a tidal turning point is negligible regardless of which side of the peak you are on.

The algorithm checks slack conditions in this order:

  1. Within 30 min after a high tide -> slack_high
  2. Within 30 min before a high tide -> slack_high
  3. Within 30 min after a low tide -> slack_low
  4. Within 30 min before a low tide -> slack_low

If none of these match, classification falls through to the ebb/flood logic.

When both a previous and next event are known, the algorithm computes a progress percentage through the current phase using linear interpolation:

progress_pct = (elapsed / total) * 100

Where elapsed is the time since the previous event and total is the full duration between the previous and next events.

Example: High tide occurred at 06:00, next low tide is at 12:12 (6 hours 12 minutes apart). At 09:00, 3 hours have elapsed out of 6.2 total hours.

progress_pct = (3.0 / 6.2) * 100 = 48.4%

This tells you the tide is about halfway through its ebb phase. A progress of 0% means the phase just started; 100% means the next turning point is imminent.

The interpolate_predictions() function serves a different purpose from phase classification. It takes 6-minute interval predictions (not hilo events) and linearly interpolates the expected water level at any arbitrary timestamp.

The water_level_anomaly tool uses this to compare what the water level should be (predicted) against what it actually is (observed). The difference is the anomaly — caused by wind, barometric pressure, storm surge, or other non-tidal forces.

def interpolate_predictions(obs_dt, pred_times, pred_values) -> float | None:

The function finds the two prediction timestamps that bracket obs_dt, then does a linear interpolation between their values. If obs_dt falls outside the prediction window entirely, it returns None.

NOAA provides 6-minute predictions. Over a 6-minute interval, the tidal curve is nearly linear — the error from linear interpolation versus a true sinusoidal fit is on the order of millimeters. Since the anomaly detection threshold defaults to 0.5 feet, this precision is more than adequate.

The algorithm handles several boundary conditions:

SituationBehavior
No hilo data at allReturns "unknown" phase with null timing fields
Only a previous event (no future event in data)Infers phase from the last event type: after a high -> "ebb", after a low -> "flood"
Only a future event (no past event in data)Infers from the upcoming event type: before a high -> "flood", before a low -> "ebb"
Neither previous nor future eventReturns "unknown"
Exactly on a hilo event timestampThe event is classified as prev_event (the <= comparison), placing the timestamp within the slack window

The single-event fallback cases use qualifiers like “likely ebbing” or “likely flooding” in the description to signal reduced confidence. These situations arise when the prediction window is too narrow — for instance, fetching only 2 hours of hilo data when tidal cycles run 6+ hours.

The classify_tidal_phase() function returns a dict with these keys:

{
"phase": "ebb", # ebb | flood | slack_high | slack_low | unknown
"description": "Tide is falling ...", # Human-readable explanation
"previous": { # Most recent past hilo event
"type": "high",
"time": "2026-02-21 06:00",
"level_ft": 4.521,
},
"next": { # Next upcoming hilo event
"type": "low",
"time": "2026-02-21 12:12",
"level_ft": -0.134,
},
"minutes_since_previous": 180, # Minutes since previous event
"minutes_to_next": 192, # Minutes until next event
"progress_pct": 48.4, # Percentage through current phase
}

The SmartPot tidal_phase tool wraps this output with station info, the current UTC timestamp, and the latest observed water level reading, giving a complete picture of tidal conditions at a location.