wavereader / ui /_stubs.py
lucashudsn's picture
agent fully implemented, cleaned ui merged data etc
a1f52bf
Raw History Blame Contribute Delete
13 kB
"""Minimal local stand-ins for Worker A/C modules (NOT the real core).
Workers A/C own ``wavereader/*`` on parallel branches; those modules do not
exist on this branch yet. This module provides deterministic fakes with the
same *shapes* the real tools will return, so the UI builds and renders
offline today.
MERGE REWIRING (orchestrator): ``ui/_compat.py`` already tries the real
``wavereader.*`` imports first — once Worker A/C merge, the stubs stop being
used with no UI changes. Delete this module only when ``wavereader/tools.py``
+ ``wavereader/climate.py`` + ``wavereader/seafloor.py`` are all merged.
"""
from __future__ import annotations
import hashlib
import math
from datetime import date, timedelta
SKILL_ORDER = ["beginner", "intermediate", "advanced", "expert", "pro-only"]
STUB_BREAKS: list[dict] = [
{
"id": "stub-bells-beach",
"name": "Bells Beach",
"state": "Victoria",
"region": "Surf Coast",
"description": "Famous right-hand reef break (stub record for UI development).",
"skillLevel": "intermediate",
"breakType": "reef",
"peakType": "right",
"location": {"country": "Australia", "state": "Victoria", "region": "Surf Coast",
"coordinates": {"lat": -38.3667, "lng": 144.2833}},
"idealSwell": {"direction": ["SW", "S"], "sizeRangeFt": {"min": 3, "max": 8}},
"idealWind": {"direction": ["N", "NW"], "type": "offshore"},
"idealTide": {"stage": ["mid", "high"]},
"bestSeason": ["autumn", "winter"],
"hazards": ["rocks", "crowd"],
"crowdFactor": "high",
},
{
"id": "stub-snapper-rocks",
"name": "Snapper Rocks",
"state": "Queensland",
"region": "Gold Coast",
"description": "Superbank sand point, long right (stub record).",
"skillLevel": "advanced",
"breakType": "sand",
"peakType": "right",
"location": {"country": "Australia", "state": "Queensland", "region": "Gold Coast",
"coordinates": {"lat": -28.1656, "lng": 153.5500}},
"idealSwell": {"direction": ["E", "SE"], "sizeRangeFt": {"min": 2, "max": 6}},
"idealWind": {"direction": ["SW", "S"], "type": "offshore"},
"idealTide": {"stage": ["mid"]},
"bestSeason": ["autumn", "winter"],
"hazards": ["crowd", "rocks"],
"crowdFactor": "high",
},
{
"id": "stub-noosa-heads",
"name": "Noosa Heads",
"state": "Queensland",
"region": "Sunshine Coast",
"description": "Gentle right points, beginner friendly (stub record).",
"skillLevel": "beginner",
"breakType": "point",
"peakType": "right",
"location": {"country": "Australia", "state": "Queensland", "region": "Sunshine Coast",
"coordinates": {"lat": -26.3833, "lng": 153.1000}},
"idealSwell": {"direction": ["N", "NE"], "sizeRangeFt": {"min": 1, "max": 4}},
"idealWind": {"direction": ["SW"], "type": "offshore"},
"idealTide": {"stage": ["mid", "high"]},
"bestSeason": ["summer", "autumn"],
"hazards": ["crowd"],
"crowdFactor": "high",
},
{
"id": "stub-the-pass",
"name": "The Pass",
"state": "New South Wales",
"region": "Byron",
"description": "Mellow sand point at Byron Bay (stub record).",
"skillLevel": "beginner",
"breakType": "sand",
"peakType": "right",
"location": {"country": "Australia", "state": "New South Wales", "region": "Byron",
"coordinates": {"lat": -28.6333, "lng": 153.6333}},
"idealSwell": {"direction": ["E", "NE"], "sizeRangeFt": {"min": 1, "max": 4}},
"idealWind": {"direction": ["SW", "W"], "type": "offshore"},
"idealTide": {"stage": ["mid"]},
"bestSeason": ["autumn"],
"hazards": ["crowd"],
"crowdFactor": "high",
},
{
"id": "stub-margaret-river",
"name": "Margaret River Main Break",
"state": "Western Australia",
"region": "South West",
"description": "Heavy reef, expert only (stub record).",
"skillLevel": "expert",
"breakType": "reef",
"peakType": "a-frame",
"location": {"country": "Australia", "state": "Western Australia",
"region": "South West",
"coordinates": {"lat": -33.9500, "lng": 114.9833}},
"idealSwell": {"direction": ["SW", "W"], "sizeRangeFt": {"min": 4, "max": 12}},
"idealWind": {"direction": ["E", "NE"], "type": "offshore"},
"idealTide": {"stage": ["mid"]},
"bestSeason": ["winter"],
"hazards": ["rocks", "shallow reef", "sharks"],
"crowdFactor": "medium",
},
{
"id": "stub-bondi",
"name": "Bondi Beach",
"state": "New South Wales",
"region": "Sydney",
"description": "City beach break, peaks up and down the beach (stub record).",
"skillLevel": "beginner",
"breakType": "beach",
"peakType": "a-frame",
"location": {"country": "Australia", "state": "New South Wales", "region": "Sydney",
"coordinates": {"lat": -33.8915, "lng": 151.2767}},
"idealSwell": {"direction": ["S", "SE"], "sizeRangeFt": {"min": 1, "max": 4}},
"idealWind": {"direction": ["W", "NW"], "type": "offshore"},
"idealTide": {"stage": ["low", "mid"]},
"bestSeason": ["summer", "autumn"],
"hazards": ["crowd", "rips"],
"crowdFactor": "high",
},
]
_DIRECTIONS_16 = [
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW",
]
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def _rng01(key: str) -> float:
h = hashlib.md5(key.encode("utf-8")).hexdigest()
return int(h[:8], 16) / 0xFFFFFFFF
def _ideal_mid(break_: dict) -> float:
size = ((break_ or {}).get("idealSwell") or {}).get("sizeRangeFt") or {}
try:
return (float(size.get("min", 1)) + float(size.get("max", 4))) / 2.0
except (TypeError, ValueError):
return 2.5
def stub_scored_hours(break_: dict, skill: str = "intermediate",
days: int = 3) -> list[dict]:
"""Deterministic fake scored hours (06:00–17:00 local, 12/day).
Shape matches the real ``score_week`` hour rows: ``time, score,
wave_height_m, wave_period_s, wind_speed_kt, wind_direction_deg``.
"""
name = (break_ or {}).get("name", "?")
mid = _ideal_mid(break_)
hours: list[dict] = []
today = date(2026, 9, 9)
for d in range(max(1, min(7, int(days)))):
day = today + timedelta(days=d + 1)
for h in range(6, 18):
key = f"{name}|{day.isoformat()}|{h}"
swell = mid + 1.1 * math.sin(d * 1.3 + h * 0.35) + (_rng01(key + "h") - 0.5) * 1.6
swell = max(0.3, round(swell, 1))
period = round(6 + 7 * _rng01(key + "p") + 0.8 * math.sin(h), 1)
wind = round(max(0.0, 8 + 14 * _rng01(key + "w") - 6 * math.sin(h * 0.5)), 1)
wdir = int(_rng01(key + "d") * 360) % 360
# Plausible v1-style score: size closeness, wind penalty, period bonus.
size_score = max(0.0, 10 - abs(swell - mid) * 2.2)
wind_score = max(0.0, 10 - max(0.0, wind - 5) * 0.7)
period_score = min(10.0, max(0.0, (period - 4) * 1.1))
score = round(min(10.0, max(0.0, 0.4 * size_score + 0.35 * wind_score + 0.25 * period_score)), 1)
hours.append(
{
"time": f"{day.isoformat()}T{h:02d}:00",
"score": score,
"wave_height_m": swell,
"wave_period_s": period,
"wind_speed_kt": wind,
"wind_direction_deg": float(wdir),
}
)
return hours
def _best(hours: list[dict]) -> dict | None:
return max(hours, key=lambda r: r.get("score", 0)) if hours else None
def _daily_best(hours: list[dict]) -> list[dict]:
best_by_date: dict[str, dict] = {}
for row in hours:
day = str(row.get("time", ""))[:10]
if day and (day not in best_by_date or row.get("score", 0) > best_by_date[day].get("score", 0)):
best_by_date[day] = row
return [
{"date": d, "time": r.get("time"), "score": r.get("score"),
"wave_height_m": r.get("wave_height_m"), "wave_period_s": r.get("wave_period_s"),
"wind_speed_kt": r.get("wind_speed_kt"), "wind_direction_deg": r.get("wind_direction_deg")}
for d, r in sorted(best_by_date.items())
]
def find_stub(name: str) -> dict | None:
"""Find a stub break by name (case-insensitive)."""
want = (name or "").strip().lower()
for b in STUB_BREAKS:
if b.get("name", "").strip().lower() == want:
return b
return None
def score_week(spot_name: str, region: str = "",
skill: str = "intermediate") -> dict:
"""Fake ``score_week`` tool payload: spot/skill/scored/daily/best."""
break_ = find_stub(spot_name) or dict(STUB_BREAKS[0])
if region:
break_ = dict(break_, region=region)
hours = stub_scored_hours(break_, skill=skill, days=3)
return {
"spot": {"name": break_.get("name"), "region": break_.get("region")},
"skill": skill,
"scored": hours,
"daily": _daily_best(hours),
"best": _best(hours),
}
def rank_region_week(region: str, skill: str = "intermediate") -> dict:
"""Fake ``rank_region_week`` payload: ``{"region", "rank": [rows]}``."""
cands = [b for b in STUB_BREAKS if b.get("region", "").lower() == region.lower()] or STUB_BREAKS[:4]
rows = []
for b in cands:
hours = stub_scored_hours(b, skill=skill, days=2)
best = _best(hours) or {}
rows.append(
{
"name": b.get("name"),
"region": b.get("region"),
"best_score": best.get("score"),
"best_time": best.get("time"),
}
)
rows.sort(key=lambda r: r.get("best_score") or 0, reverse=True)
return {"region": region, "skill": skill, "rank": rows}
def explain_score(spot_name: str, skill: str = "intermediate", time: str = "") -> dict:
"""Fake ``explain_score`` payload: score + v1-style component breakdown."""
payload = score_week(spot_name, skill=skill)
hours = payload["scored"]
row = next((h for h in hours if h.get("time") == time), None) or _best(hours) or {}
score = float(row.get("score") or 0)
return {
"spot": payload["spot"],
"skill": skill,
"time": row.get("time"),
"score": score,
"components": {
"size": round(min(10.0, score + 0.6), 1),
"direction": round(min(10.0, score + 0.2), 1),
"wind": round(max(0.0, score - 0.8), 1),
"period": round(min(10.0, score + 0.4), 1),
},
"note": "stub breakdown (weights 30/20/30/20) — real engine lands with Worker A.",
}
def climate_profile(break_: dict) -> dict:
"""Fake 5-year climate profile: 16-bin rose (%), best months, medians."""
name = (break_ or {}).get("name", "?")
raw = [_rng01(f"{name}|rose|{d}") + 0.15 for d in _DIRECTIONS_16]
total = sum(raw)
rose = {d: round(v / total * 100, 1) for d, v in zip(_DIRECTIONS_16, raw)}
months = sorted(_MONTHS, key=lambda m: _rng01(f"{name}|month|{m}"), reverse=True)[:3]
return {
"rose": rose,
"best_months": months,
"median_height_m": round(0.6 + 2.4 * _rng01(f"{name}|h"), 1),
"median_period_s": round(6 + 6 * _rng01(f"{name}|p"), 1),
}
def seafloor_grid(break_: dict, radius_km: float = 1.2, n: int = 9) -> dict:
"""Fake bathymetry grid: depth increasing seaward + noise."""
coords = ((break_ or {}).get("location") or {}).get("coordinates") or {}
lat = float(coords.get("lat", -33.9))
lng = float(coords.get("lng", 151.3))
name = (break_ or {}).get("name", "?")
half_lat = radius_km / 111.0
half_lng = radius_km / max(20.0, 111.0 * math.cos(math.radians(lat)))
lats = [round(lat + ((i / (n - 1)) * 2 - 1) * half_lat, 5) for i in range(n)]
lngs = [round(lng + ((j / (n - 1)) * 2 - 1) * half_lng, 5) for j in range(n)]
elev = []
for i in range(n):
for j in range(n):
# Deeper to the east (seaward for east-coast stubs) + dip + noise.
e = -2 - j * 2.2 - abs(i - n // 2) * 0.7 + (_rng01(f"{name}|sf|{i},{j}") - 0.5) * 3
if j == 0 and i in (0, n - 1):
e = abs(e) * 0.4 # land corners
elev.append(round(e, 1))
return {"lats": lats, "lngs": lngs, "elev": elev, "n": n,
"radius_km": radius_km, "dataset": "stub-gebco",
"center": {"lat": lat, "lng": lng}}