Skip to content

Heuristics

The internals of the score, the jellyfish and whale heuristics, the water verdict and the other pure rules behind a recommendation. What they cannot tell a swimmer, in words meant for one, is Limitations; this page is the mechanics, for changing them. Everything here is a pure function in core, with no effect type, pinned by the spec named in each section.

The score

Swimability.score starts each hour at 100, adds every deduction, and clamps to 0–100. A water veto forces 0 whatever the rest says. Each deduction carries a Note, a code plus arguments that the CLI and the map word themselves (MIP-0054); a good hour has no notes.

Signal Deduction Note
Waves ≥ 1.5 m −40 rough_seas
Waves ≥ 0.6 m −15 choppy
Wind ≥ 30 km/h −25 strong_wind
Wind ≥ 15 km/h −10 breezy
Sea < 20 °C −20 cold_water
Sea > 27 °C −5 warm_water
Rain chance ≥ 60 % −10 rain_likely
Jellyfish High / Moderate −25 / −10 jellyfish_elevated / jellyfish_some
Night (is_day false) −60 dark
Water: IMPRÓPRIA and PRÓPRIA points both fresh −20 water_mixed
Water: fresh IMPRÓPRIA, no PRÓPRIA score 0 (veto) water_unfit
Water: every sample older than 45 days 0 water_stale
No wave, wind or sea-temperature value −5 each no_wave_data, no_wind_data, no_sea_temp_data

Missing rain or daylight data costs nothing. The wind bands are also exposed as Swimability.windLevel, so the map shows the same word without re-deriving a threshold (MIP-0009). Equal scores break by hourPreference, the distance from 10:00. Whale likelihood never enters the score. Pinned by SwimabilitySpec.

Jellyfish risk

No jellyfish-bloom API exists, so jellyfishRisk counts four ecological correlates:

Signal Threshold
Warm sea sea temperature ≥ 24 °C
Weak wind wind ≤ 15 km/h
Calm sea waves ≤ 0.6 m
Weak current current ≤ 2 km/h

Three or four true is High, two Moderate, otherwise Low. A missing value counts as false, so missing data lowers the risk. Three of the four are also what makes a pleasant swim, so a calm day is often flagged; SwimabilitySpec pins that rather than hides it. The thresholds are hand-tuned, not fitted to any data.

Whale sighting likelihood

whaleSightingLikelihood is Low outside July–November (the humpback migration along the Brazilian coast) and at night. Otherwise it counts two visibility signals, wind ≤ 20 km/h and waves ≤ 1.0 m, looser than the swim thresholds because you only need to spot a blow: both is High, one Moderate. Recommender also keeps each day's whale peak, the daylight hour with the highest likelihood, which the detailed block and the board show.

The water verdict

Swimability.waterVerdict turns a beach's matched sampling points into a deduction, a veto or neither (MIP-0001 §6). It runs once per beach per day, not per hour. A sample is fresh when it is at most 45 days old (WaterQuality.MaxSampleAgeDays: IMA samples monthly off-season), counted from the agency's own sample date. The agency's PRÓPRIA/IMPRÓPRIA is used as published (CONAMA 274/2000), never re-derived from the enterococci counts. Pinned by WaterVerdictSpec.

Kroki

No data, no match or the provider none leave the score alone: absence of data is not evidence of pollution. A point whose condition is missing or unrecognised is Unknown and counts as neither.

Matching points to beaches

WaterQualityMatcher.assign gives each agency point to at most one OSM beach. First by name: accents and case folded, a leading "Praia do/da/de…" and anything in parentheses dropped, and one name may be a word prefix of the other ("Campeche" matches "Campeche Sul"). An unmatched point then goes to the nearest beach within 2.5 km, unless its name starts with an inland-water word (Lagoa, Lago, Canal, Rio, Foz, Represa, Barragem): Lagoa da Conceição's point 72 sits 1.3 km from Praia da Joaquina's centre and would otherwise be attached to it. Pinned by WaterQualityMatcherSpec.

Tides

Tides.extrema reads high and low water off Open-Meteo's hourly sea_level_height_msl: a local peak or trough is a candidate. Kept turns alternate; a same-type candidate replaces the kept one when more extreme, and an opposite-type one less than 0.1 m away from the last kept turn is dropped as sampling noise. There is no tide-table API or harmonic model, so a turn is only as precise as the hour. Pinned by TidesSpec.

Sea lore

SeaLore.pick chooses one entry of core/src/main/resources/sea_lore.json (eight, each with a source URL; an entry without one is dropped on load). Entries filter by region (global everywhere, BR-S on the south and south-east Brazilian coast, a bounding box) and by month, then one is chosen by a seed of the date and the beach name: the same beach on the same day gives the same paragraph. It is appended verbatim and never passes through a model, so the reviewer does not see it (a deliberate deviation from MIP-0001 §5.4). Pinned by SeaLoreSpec.

Calibration: not wired yet

The thresholds above are constants. SightingStore (--report-sighting) and VisionClient (--analyze-photo) exist to collect real reports, but nothing reads them back into a threshold or weight. Doing so, or feeding reports into marola-ml's prompt trainset, is future work; MIP-0007's forecasting is one candidate for the calibration.