File size: 8,701 Bytes
430bfd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4abd7f
430bfd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51e44c2
 
 
 
 
 
 
 
430bfd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4abd7f
51e44c2
430bfd7
 
 
d4abd7f
 
 
 
 
1b634dd
 
 
 
d4abd7f
 
 
 
 
 
 
 
 
 
 
 
430bfd7
 
 
 
 
 
 
 
 
 
 
d4abd7f
 
430bfd7
 
 
 
 
 
 
d4abd7f
 
430bfd7
51e44c2
430bfd7
 
 
 
 
 
 
 
 
 
 
a6e0d00
 
430bfd7
 
 
 
 
 
51e44c2
430bfd7
 
 
 
 
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
"""Three native-cell slice controls and cancellable, saved-frame playback."""

import asyncio
import json
import os
from pathlib import Path
from threading import Lock
from time import monotonic

import gradio as gr
import numpy as np
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.colors import LinearSegmentedColormap, SymLogNorm
from matplotlib.figure import Figure

try:
    from .reader import PLANES, NativeDataset
except ImportError:
    from reader import PLANES, NativeDataset

PLOT_LOCK = Lock()
CMAP = LinearSegmentedColormap.from_list(
    "native-cyan", ["#06111d", "#0e415c", "#2199bd", "#a9e1df", "#fff3b0"]
)
CSS = """
.gradio-container {background:#06111d !important; color:#e9f1f5 !important; max-width:1400px !important}
.prose {color:#e9f1f5 !important} footer {display:none !important}
"""


def render_slice(dataset, frame, plane, coordinate, quantity, n):
    vectors, _ = dataset.sample_plane(frame, plane, coordinate, quantity, n)
    values = np.linalg.norm(vectors, axis=-1)
    vmax = max(dataset.manifest["color_max"][quantity], 1e-15)
    h, v, fixed = PLANES[plane]
    with PLOT_LOCK:
        figure = Figure(figsize=(4.4, 4.4), dpi=120, facecolor="#06111d")
        canvas = FigureCanvasAgg(figure)
        axes = figure.add_subplot(111, facecolor="#06111d")
        picture = axes.imshow(
            values.T,
            origin="lower",
            extent=(-1, 1, -1, 1),
            cmap=CMAP,
            norm=SymLogNorm(linthresh=vmax * 1e-3, vmin=0, vmax=vmax),
            interpolation="nearest",
        )
        axes.set(
            xlabel="xyz"[h],
            ylabel="xyz"[v],
            title=f"{plane[0]}–{plane[1]} · {'xyz'[fixed]} = {coordinate:.5f}",
        )
        axes.tick_params(colors="#9fb3c2")
        for label in (axes.xaxis.label, axes.yaxis.label, axes.title):
            label.set_color("#e9f1f5")
        colorbar = figure.colorbar(picture, ax=axes, fraction=0.045, pad=0.035)
        colorbar.ax.tick_params(colors="#9fb3c2")
        figure.tight_layout()
        canvas.draw()
        return np.asarray(canvas.buffer_rgba())[..., :3].copy()


def render_state(dataset, frame, x, y, z, quantity, n):
    if not np.isfinite(frame) or int(frame) != frame or int(n) != n:
        raise ValueError("Choose a saved frame and supported display resolution")
    frame, n = int(frame), int(n)
    images = [
        render_slice(dataset, frame, plane, coord, quantity, n)
        for plane, coord in zip(("yz", "xz", "xy"), (x, y, z))
    ]
    status = f"Frame {frame + 1}/{len(dataset.times)} · t = {dataset.times[frame]:.8f} · {quantity} · {n}² display samples per plane\n\nx = {x:.5f} · y = {y:.5f} · z = {z:.5f}\n\n{dataset.cache_status}"
    return (*images, status)


def make_app(dataset):
    with gr.Blocks(
        title="Native Navier–Stokes explorer", delete_cache=(3600, 3600)
    ) as app:
        gr.Markdown(
            "## Explore the saved simulation\nMove the three fixed-coordinate planes, then play through saved time. Velocity and forcing are in model units."
        )
        if not dataset.manifest["complete_history"]:
            gr.Markdown(
                f"**Prototype subset:** {len(dataset.times)} of {dataset.manifest['source_frames']} saved states. This is not the complete animation."
            )
        with gr.Row():
            quantity = gr.Radio(
                ["velocity", "force"], value="velocity", label="Quantity"
            )
            resolution = gr.Dropdown(
                [128, 256, 512, 1024], value=256, label="Display samples per axis"
            )
        step = min(min(level["spacing"]) for level in dataset.levels)
        with gr.Row():
            x = gr.Slider(-1, 1, value=0, step=step / 2, label="x · YZ slice")
            y = gr.Slider(-1, 1, value=0, step=step / 2, label="y · XZ slice")
            z = gr.Slider(-1, 1, value=0, step=step / 2, label="z · XY slice")
        with gr.Row():
            images = [
                gr.Image(
                    label=name,
                    interactive=False,
                    format="png",
                    type="numpy",
                    buttons=["download", "fullscreen"],
                    alt_text=f"Native {name} magnitude slice",
                )
                for name in ("YZ", "XZ", "XY")
            ]
        time = gr.Slider(
            0,
            len(dataset.times) - 1,
            value=0,
            step=1,
            label="t · saved frame",
            info="Actual simulation time is shown below. Saved times are not uniformly spaced.",
        )
        with gr.Row():
            previous = gr.Button("Previous frame")
            play = gr.Button("Play", variant="primary")
            pause = gr.Button("Pause")
            following = gr.Button("Next frame")
        status = gr.Markdown()
        gr.Markdown(
            "The viewer samples the finest native cell available at each location. Display resolution does not change solver resolution. No spatial or temporal interpolation, automatic peak tracking, or vector arrows. Playback targets five saved states per second and waits for data; it never skips a state.\n\nFinite manufactured-force experiment—not a singularity or convergence certificate."
        )
        inputs = [time, x, y, z, quantity, resolution]
        outputs = [*images, status]

        def render(*args):
            return render_state(dataset, *args)

        async def animate(frame, *args):
            start = 0 if int(frame) == len(dataset.times) - 1 else int(frame)
            for index in range(start, len(dataset.times)):
                started = monotonic()
                result = await asyncio.to_thread(render, index, *args)
                yield index, *result
                await asyncio.sleep(max(0, 0.2 - (monotonic() - started)))

        playback = play.click(
            animate,
            inputs,
            [time, *outputs],
            api_name=False,
            concurrency_id="native-playback",
            concurrency_limit=4,
            show_progress="hidden",
        )
        pause.click(fn=None, cancels=[playback], queue=False)
        # One ordered event for all controls: a slower old request must not
        # overwrite a newer quantity/coordinate choice. Playback uses a separate
        # bounded group so one long animation cannot monopolize manual controls.
        gr.on(
            triggers=[
                x.input,
                y.input,
                z.input,
                time.input,
                quantity.change,
                resolution.change,
            ],
            fn=render,
            inputs=inputs,
            outputs=outputs,
            cancels=[playback],
            trigger_mode="always_last",
            concurrency_id="native-manual",
            concurrency_limit=1,
            show_progress="hidden",
        )
        for button, increment in ((previous, -1), (following, 1)):

            def advance(frame, *args, amount=increment):
                index = max(0, min(len(dataset.times) - 1, int(frame) + amount))
                return index, *render(index, *args)

            button.click(
                advance,
                inputs,
                [time, *outputs],
                cancels=[playback],
                concurrency_id="native-manual",
                concurrency_limit=1,
                show_progress="hidden",
            )
        app.load(
            render,
            inputs,
            outputs,
            api_name="slice",
            concurrency_id="native-manual",
            concurrency_limit=1,
        )
    return app.queue(max_size=16, default_concurrency_limit=4)


if __name__ == "__main__":
    config_path = Path(__file__).with_name("dataset.json")
    config = json.loads(config_path.read_text()) if config_path.exists() else {}
    dataset = NativeDataset(
        folder=os.environ.get("NS_DATASET_LOCAL"),
        repo_id=os.environ.get("NS_DATASET_REPO", config.get("repo_id")),
        revision=os.environ.get("NS_DATASET_REVISION", config.get("revision")),
        prefix=os.environ.get("NS_DATASET_PREFIX", config.get("prefix", "")),
    )
    if os.environ.get("SPACE_ID"):
        dataset.warm_archive_cache()
    make_app(dataset).launch(
        server_name="0.0.0.0" if os.environ.get("SPACE_ID") else "127.0.0.1",
        server_port=int(
            os.environ.get("PORT", "7860" if os.environ.get("SPACE_ID") else "8877")
        ),
        share=False,
        max_threads=4,
        theme=gr.themes.Base(primary_hue="cyan", neutral_hue="slate"),
        css=CSS,
        footer_links=[],
        show_error=False,
    )