wavereader / tests /test_climate.py
lucashudsn's picture
agent fully implemented, cleaned ui merged data etc
a1f52bf
Raw History Blame Contribute Delete
8.65 kB
"""Offline tests for wavereader.climate (Worker B slice). No network."""
import json
import sys
from pathlib import Path
# Bootstrap: repo root on sys.path so `wavereader.climate` imports without
# requiring Worker A's `wavereader/__init__.py` (absent on v2-climate branch).
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import pytest
from wavereader.climate import (
COMPASS16,
audit_break,
break_climate,
build_profile,
climate_rose_fig,
compute_monthly_stats,
compute_rose,
direction_rose,
direction_to_bin,
dominant_directions,
filter_existing_slugs,
ideal_window_label,
load_climate,
monthly_medians,
monthly_window_pct,
month_to_season,
rose_top_share,
slugify,
summarize,
top_months_by_window,
top_rose_bins,
)
FIXTURE = Path(__file__).parent / "fixtures" / "climate_bells.json"
BELLS_BREAK = {
"id": "Bells Beach | Victoria | Surf Coast",
"name": "Bells Beach",
"idealSwell": {"direction": ["SW", "S", "SSW"], "sizeRangeFt": {"min": 4, "max": 12}},
"bestSeason": ["autumn", "winter"],
}
@pytest.fixture()
def bells_climate():
return json.loads(FIXTURE.read_text(encoding="utf-8"))
def test_direction_to_bin_edges():
assert direction_to_bin(0) == "N"
assert direction_to_bin(360) == "N"
assert direction_to_bin(22.5) == "NNE"
assert direction_to_bin(225) == "SW"
assert direction_to_bin(359) == "N"
def test_compute_rose_known_distribution():
rose = compute_rose([90.0] * 50 + [180.0] * 25 + [None, None])
assert rose["E"] == pytest.approx(66.67, abs=0.01)
assert rose["S"] == pytest.approx(33.33, abs=0.01)
assert sum(rose.values()) == pytest.approx(100.0, abs=0.1)
assert set(rose) == set(COMPASS16)
def test_compute_rose_empty_and_nulls():
assert all(v == 0.0 for v in compute_rose([]).values())
assert all(v == 0.0 for v in compute_rose([None, None]).values())
assert sum(compute_rose([None, 45.0]).values()) == pytest.approx(100.0)
def test_direction_rose_accessor(bells_climate):
rose = direction_rose(bells_climate)
assert set(rose) == set(COMPASS16)
assert sum(rose.values()) == pytest.approx(100.0, abs=0.5)
assert top_rose_bins(bells_climate, 3) == ["SW", "S", "SSW"]
def test_monthly_medians_accessor(bells_climate):
med = monthly_medians(bells_climate)
assert set(med) == set(range(1, 13))
assert med[6]["median_swell_height_m"] == pytest.approx(2.5)
assert med[1]["median_swell_period_s"] == pytest.approx(10.5)
def test_monthly_stats_grouping_and_window():
dates = ["2021-01-05", "2021-01-20", "2021-06-10", "bad-date"]
# window 1.0m..2.0m
stats = compute_monthly_stats(dates, [1.5, 5.0, 1.2], [10.0, 11.0, 12.0], 1.0, 2.0)
assert stats[1]["median_swell_height_m"] == pytest.approx(3.25)
assert stats[1]["days_in_ideal_window"] == 1
assert stats[6]["days_in_ideal_window"] == 1
assert stats[6]["n_days"] == 1
assert stats[2]["n_days"] == 0
assert stats[2]["median_swell_height_m"] is None
def test_top_months_by_window(bells_climate):
assert top_months_by_window(bells_climate, 3) == [6, 7, 5]
def test_monthly_window_pct(bells_climate):
pct = monthly_window_pct(bells_climate)
assert set(pct) == set(range(1, 13))
assert pct[10] == pytest.approx(22.6, abs=0.01) # 35/155, rounded to 0.1
assert pct[6] == pytest.approx(53.3, abs=0.01) # 80/150
empty = {"monthly": {"3": {"days_in_ideal_window": 0, "n_days": 0}}}
assert monthly_window_pct(empty)[3] is None
def test_dominant_directions_and_top_share(bells_climate):
assert dominant_directions(bells_climate, 2) == [("SW", 28.0), ("S", 22.0)]
assert rose_top_share(bells_climate, 2) == pytest.approx(50.0)
assert rose_top_share({"direction_rose_pct": {}}) == 0.0
def test_ideal_window_label(bells_climate):
assert ideal_window_label(bells_climate) == "4–12 ft"
assert ideal_window_label({"ideal_window_ft": {"min": None, "max": None}}) == ""
assert ideal_window_label({}) == ""
def test_summarize_digest(bells_climate):
s = summarize(bells_climate)
assert s["ideal_window_ft"] == "4–12 ft"
assert s["dominant_directions"] == [["SW", 28.0], ["S", 22.0]]
assert s["top_directions_share_pct"] == pytest.approx(50.0)
# by share of in-window days: 6 (53.3%) > 7 (48.4%) > 5 (45.2%)
assert s["best_months_by_share"] == [6, 7, 5]
assert set(s["monthly_window_pct"]) == set(range(1, 13))
assert s["monthly_window_pct"][6] == pytest.approx(53.3, abs=0.1)
json.dumps(s) # must stay JSON-serializable for the agent tool surface
def test_month_to_season():
assert month_to_season(1) == "summer"
assert month_to_season(4) == "autumn"
assert month_to_season(7) == "winter"
assert month_to_season(10) == "spring"
with pytest.raises(ValueError):
month_to_season(13)
def test_audit_agreement_bells(bells_climate):
# Dataset SW/S/SSW == rose top-3; autumn/winter covers top months 6,7,5.
assert audit_break(BELLS_BREAK, bells_climate) == []
def test_audit_direction_disagreement(bells_climate):
bad = {**BELLS_BREAK, "idealSwell": {"direction": ["N", "NE"]}}
findings = audit_break(bad, bells_climate)
by_field = {f["field"]: f for f in findings}
assert by_field["idealSwell.direction"]["severity"] == "high"
assert by_field["idealSwell.direction"]["dataset_value"] == ["N", "NE"]
assert by_field["idealSwell.direction"]["climate_value"] == ["SW", "S", "SSW"]
# season still agrees -> no season finding
assert "bestSeason" not in by_field
def test_audit_season_disagreement(bells_climate):
bad = {**BELLS_BREAK, "bestSeason": ["summer"]}
findings = audit_break(bad, bells_climate)
by_field = {f["field"]: f for f in findings}
assert by_field["bestSeason"]["severity"] == "medium"
assert by_field["bestSeason"]["dataset_value"] == ["summer"]
assert by_field["bestSeason"]["climate_value"] == [6, 7, 5]
assert "idealSwell.direction" not in by_field
def test_audit_no_data_returns_empty():
empty = {"n_valid_direction": 0, "direction_rose_pct": {}, "monthly": {}}
assert audit_break(BELLS_BREAK, empty) == []
def test_slugify():
assert slugify("Bells Beach | Victoria | Surf Coast") == "bells-beach-victoria-surf-coast"
assert slugify("Avoca Beach | New South Wales | Central Coast") == (
"avoca-beach-new-south-wales-central-coast"
)
def test_load_climate_fixture(bells_climate):
loaded = load_climate("climate_bells", Path(__file__).parent / "fixtures")
assert loaded == bells_climate
with pytest.raises(FileNotFoundError):
load_climate("nope", Path(__file__).parent / "fixtures")
def test_break_climate_prefers_embedded(bells_climate, tmp_path):
break_ = dict(BELLS_BREAK, state="Victoria", region="Surf Coast", climate=bells_climate)
# embedded copy wins even when the fallback dir holds nothing
assert break_climate(break_, tmp_path) == bells_climate
def test_break_climate_file_fallback(bells_climate, tmp_path):
import shutil
shutil.copy(FIXTURE, tmp_path / "bells-beach-victoria-surf-coast.json")
break_ = {k: v for k, v in BELLS_BREAK.items() if k != "id"}
break_["state"] = "Victoria"
break_["region"] = "Surf Coast"
assert break_climate(break_, tmp_path) == bells_climate
with pytest.raises(FileNotFoundError):
break_climate({"name": "Nowhere", "state": "X", "region": "Y"}, tmp_path)
def test_filter_existing_slugs_resumable(tmp_path):
(tmp_path / "a.json").write_text("{}", encoding="utf-8")
assert filter_existing_slugs(["a", "b"], tmp_path) == ["b"]
assert filter_existing_slugs(["a", "b"], tmp_path, overwrite=True) == ["a", "b"]
def test_build_profile_roundtrip():
dates = ["2021-01-01", "2021-01-02", "2021-07-01"]
prof = build_profile(
slug="x", break_id="X", name="X", latitude=-38.5, longitude=144.3,
start="2021-01-01", end="2021-12-31", dates=dates,
heights_m=[1.5, None, 2.0], directions_deg=[225.0, 225.0, None],
periods_s=[12.0, 11.0, 13.0], window_min_ft=4, window_max_ft=12,
)
assert prof["n_days"] == 3
assert prof["n_valid_direction"] == 2
assert prof["direction_rose_pct"]["SW"] == pytest.approx(100.0)
assert prof["monthly"]["1"]["n_days"] == 2
def test_climate_rose_fig(bells_climate):
fig = climate_rose_fig(bells_climate, "Bells Beach")
assert len(fig.data) == 1
assert len(fig.data[0].r) == 16
assert list(fig.data[0].theta) == COMPASS16
assert max(fig.data[0].r) == pytest.approx(28.0)