File size: 5,215 Bytes
a1f52bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Offline tests for ui.charts.seafloor (animated swell wiring).

Synthetic grids only — no network. The animation test guards the Gradio
integration: gr.Plot renders with ``Plotly.react(gd, data, layout,
config)`` and drops ``fig.frames``, so the swell frames must ride inside
the ▶ button's ``args[0]`` (raw frame-object list plotly.js animates).
"""

import json
import math

from ui.charts.seafloor import _FRAMES, build_surface_fig


def _reef_grid(n=10, center=(-38.37, 144.28)):
    """GEBCO-shaped grid: deep offshore, a reef peak breaching near (7, 3)."""
    lats = [round(center[0] + (i - n / 2) * 0.008, 5) for i in range(n)]
    lngs = [round(center[1] + (j - n / 2) * 0.008, 5) for j in range(n)]
    elev = [round(-25.0 + 4.0 * math.hypot(i - 7, j - 3), 1)
            for i in range(n) for j in range(n)]
    return {"lats": lats, "lngs": lngs, "elev": elev, "n": n,
            "center": {"lat": center[0], "lng": center[1]}}


def test_surface_animates_frames_via_button_args():
    """▶ frames ride in updatemenus args — never in (client-dropped) fig.frames."""
    doc = json.loads(build_surface_fig(
        _reef_grid(), "Bells Beach",
        wave={"height_m": 1.8, "period_s": 11.0}).to_json())

    assert "frames" not in doc  # dead payload — gr.Plot drops it client-side
    button = doc["layout"]["updatemenus"][0]["buttons"][0]
    assert button["method"] == "animate" and "swell" in button["label"]
    frames_arg, opts = button["args"]
    assert len(frames_arg) == _FRAMES
    assert opts["frame"]["redraw"] is True and opts["frame"]["duration"] > 0
    for frame in frames_arg:
        # z-only updates for trace 1 (the swell Surface); NaN ships as null
        assert frame["traces"] == [1]
        (z,) = frame["data"]
        assert set(z) == {"z"} and len(z["z"]) == 41
        assert any(v is None for row in z["z"] for v in row)  # broken-zone holes
    # the phases must actually differ or the "animation" would be a still
    z0 = frames_arg[0]["data"][0]["z"]
    z_mid = frames_arg[_FRAMES // 2]["data"][0]["z"]
    assert any(abs((a or 0) - (b or 0)) > 0.01
               for row_a, row_b in zip(z0, z_mid)
               for a, b in zip(row_a, row_b))
    # initial swell surface present as trace 1, matching frame 0
    assert doc["data"][1]["name"] == "swell"
    assert doc["data"][1]["z"] == z0


def test_surface_without_wave_has_no_button():
    doc = json.loads(build_surface_fig(_reef_grid(), "Bells").to_json())
    assert "updatemenus" not in doc["layout"]
    assert len(doc["data"]) == 1 and doc["data"][0]["name"] == "seafloor"


def _flat_grid(n=10, center=(-38.37, 144.28)):
    """Uniform deep water — no shoaling, no broken zone: z *is* the phase field."""
    lats = [round(center[0] + (i - n / 2) * 0.008, 5) for i in range(n)]
    lngs = [round(center[1] + (j - n / 2) * 0.008, 5) for j in range(n)]
    elev = [-25.0] * (n * n)
    return {"lats": lats, "lngs": lngs, "elev": elev, "n": n,
            "center": {"lat": center[0], "lng": center[1]}}


def _first_frame_z(direction_deg):
    doc = json.loads(build_surface_fig(
        _flat_grid(), "Bells",
        wave={"height_m": 1.8, "period_s": 11.0, "direction_deg": direction_deg}
    ).to_json())
    return doc["data"][1]["z"]


def test_surface_direction_rotates_the_swell():
    """direction_deg steers travel: from-E crests run N–S, from-N crests run E–W."""
    z_from_east = _first_frame_z(90.0)   # travels west → varies along lng only
    z_from_north = _first_frame_z(0.0)   # travels south → varies along lat only

    # from-east: rows are identical (field depends on lng only) but vary along the row
    assert all(a == b for row_a, row_b in zip(z_from_east, z_from_east[1:])
               for a, b in zip(row_a, row_b))
    assert any(a != b for a, b in zip(z_from_east[0], z_from_east[0][1:]))
    # from-north: every row is flat (constant along lng) and rows differ
    assert all(v == row[0] for row in z_from_north for v in row)
    assert any(row_a[0] != row_b[0]
               for row_a, row_b in zip(z_from_north, z_from_north[1:]))
    # no direction given: legacy default (travels west, varies along lng)
    z_default = _first_frame_z(None)
    assert all(a == b for row_a, row_b in zip(z_default, z_default[1:])
               for a, b in zip(row_a, row_b))
    # and the two steerings genuinely differ from each other
    assert any((a or 0) != (b or 0)
               for row_a, row_b in zip(z_from_east, z_from_north)
               for a, b in zip(row_a, row_b))


def test_surface_button_label_shows_swell_direction():
    doc = json.loads(build_surface_fig(
        _reef_grid(), "Bells Beach",
        wave={"height_m": 1.8, "period_s": 11.0, "direction_deg": 213}).to_json())
    label = doc["layout"]["updatemenus"][0]["buttons"][0]["label"]
    assert "swell" in label and "213°" in label
    # missing direction still animates, with no "@ …°" suffix
    doc = json.loads(build_surface_fig(
        _reef_grid(), "Bells Beach",
        wave={"height_m": 1.8, "period_s": 11.0, "direction_deg": None}).to_json())
    label = doc["layout"]["updatemenus"][0]["buttons"][0]["label"]
    assert "swell" in label and "°" not in label