Spaces:
Running
Running
File size: 8,646 Bytes
93d288c a1f52bf 93d288c a1f52bf 93d288c a1f52bf 93d288c a1f52bf 93d288c a1f52bf 93d288c a1f52bf 93d288c a1f52bf 93d288c a1f52bf 93d288c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | """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)
|