File size: 13,954 Bytes
0b1fdba
 
5d756e6
 
 
 
 
0b1fdba
5d756e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b1fdba
 
 
5d756e6
 
 
 
 
0b1fdba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b2e675
0b1fdba
 
 
 
 
 
 
 
 
 
8b2e675
0b1fdba
 
 
 
 
8b2e675
 
5d756e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b1fdba
5d756e6
 
 
 
 
 
0b1fdba
5d756e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b1fdba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
from fluid import * 
from gradio_litmodel3d import LitModel3D
import math, zipfile, tempfile
import numpy as np
import plotly.graph_objects as go
import gradio as gr
import cudaq

# Helper: snap grid size to nearest power of two within bounds
def _nearest_pow2(n: int, min_pow: int = 2**7, max_pow: int = 2**12) -> int:
    if n <= 0:
        return min_pow
    # nearest power of two
    p = 1 << (int(round(math.log2(max(n, 1)))))
    # clamp to bounds and adjust if out of range
    if p < min_pow:
        p = min_pow
    if p > max_pow:
        # choose the closer between max_pow and previous power
        p = max_pow
    return p

# New update function: also updates the slider value after snapping
def update_grid_and_qubit_info(grid_size):
    snapped = _nearest_pow2(int(grid_size))
    num_reg_qubits = int(math.log2(snapped))
    total_qubits = 2 * num_reg_qubits + 3

    x = np.array([128, 256, 512, 1024, 2048, 4096])
    y = np.log2(x).astype(int)
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='Qubits/Direction'))
    fig.add_trace(go.Scatter(x=[snapped], y=[num_reg_qubits], mode='markers', marker=dict(size=12, color='red'), name='Current Selection'))
    fig.update_layout(
        xaxis_title="Grid Size (Points/Direction)",
        yaxis_title="Qubits/Direction",
        width=400,
        height=300
    )

    total_qubits_display = f"Total Qubits: {total_qubits}"
    warn_pow2 = " (snapped to nearest power of two)" if snapped != int(grid_size) else ""
    warning = ("⚠️ Warning: Grid sizes > 1024 may exceed simulator/memory limits!" if snapped > 1024 else "") + warn_pow2
    recommended_time_steps = num_reg_qubits * 200
    recommended_display = f"Recommended time steps: {recommended_time_steps}"

    return gr.update(value=snapped), fig, total_qubits_display, warning, recommended_display

# Modified interface functions to take num_reg_qubits_input
def qlbm_gradio_interface(grid_size_input: int, time_steps_input: int, distribution_type_param: str, velocity_field_param: str, vx_param: float, vy_param: float, boundary_condition_param: str):
    snapped_grid = _nearest_pow2(int(grid_size_input))
    if snapped_grid != int(grid_size_input):
        gr.Warning(f"Grid size {grid_size_input} is not a power of two. Using {snapped_grid}.")
    num_reg_qubits_val = int(math.log2(snapped_grid))
    grid_size_val = snapped_grid
    time_steps_val = int(time_steps_input)
    vx_val = float(vx_param)
    vy_val = float(vy_param)

    print(f"Gradio Interface: Qubits/Direction={num_reg_qubits_val}, Grid Size={grid_size_val}, T={time_steps_val}, Distribution={distribution_type_param}, Velocity Field={velocity_field_param}, vx={vx_val}, vy={vy_val}, Boundary={boundary_condition_param}")

    plot_fig, plotly_json_frames = simulate_qlbm_and_animate( # Modified to unpack two return values
        num_reg_qubits=num_reg_qubits_val,
        T=time_steps_val,
        distribution_type=distribution_type_param,
        velocity_field=velocity_field_param,
        vx_input=vx_val,
        vy_input=vy_val,
        boundary_condition=boundary_condition_param
    )

    if plot_fig is None:
        gr.Warning("Simulation or plotting failed. Please check console for errors.")
        return None, None # Modified return
    return plot_fig, plotly_json_frames # Modified return

# New functions for downloading Plotly objects
def download_plot_data(plotly_json_frames):
    if not plotly_json_frames:
        gr.Warning("No data to download.")
        return None

    zip_file_path = tempfile.NamedTemporaryFile(suffix=".zip", delete=False).name
    with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zf:
        for i, json_str in enumerate(plotly_json_frames):
            zf.writestr(f"frame_{i}.json", json_str)
    return zip_file_path

# Modified update functions to take grid_size (which is num_reg_qubits_input now)
def update_qubit_info(grid_size):
    num_reg_qubits = int(math.log2(grid_size))
    total_qubits = 2 * num_reg_qubits + 3

    x = np.array([128, 256, 512, 1024, 2048, 4096])
    y = np.log2(x).astype(int)
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='Qubits/Direction'))
    fig.add_trace(go.Scatter(x=[grid_size], y=[num_reg_qubits], mode='markers', marker=dict(size=12, color='red'), name='Current Selection'))
    fig.update_layout(
        xaxis_title="Grid Size (Points/Direction)",
        yaxis_title="Qubits/Direction",
        width=400,
        height=300
    )

    total_qubits_display = f"Total Qubits: {total_qubits}"
    warning = "⚠️ Warning: Grid sizes > 1024 may exceed simulator/memory limits!" if grid_size > 1024 else ""
    recommended_time_steps = num_reg_qubits * 200
    recommended_display = f"Recommended time steps: {recommended_time_steps}"

    return fig, total_qubits_display, warning, recommended_display

# Modified example functions to set grid_size
def set_sinusoidal_example():
    return (
        gr.update(value=256),  # grid_size = 256 (corresponds to 8 qubits)
        gr.update(value=1600),  # time_steps
        gr.update(value="Sinusoidal"),  # distribution_type
        gr.update(value="Uniform"),  # velocity_field
        gr.update(value=0.2),  # vx
        gr.update(value=0.15),  # vy
        gr.update(value="Periodic"),  # boundary_condition
    )

def set_gaussian_example():
    return (
        gr.update(value=256),  # grid_size = 256 (corresponds to 8 qubits)
        gr.update(value=1600),  # time_steps
        gr.update(value="Gaussian"),  # distribution_type
        gr.update(value="Uniform"),  # velocity_field
        gr.update(value=0.2),  # vx
        gr.update(value=0.15),  # vy
        gr.update(value="Periodic"),  # boundary_condition
    )

# Gradio interface for Fluid Dynamics - 2D only
with gr.Blocks(theme=gr.themes.Soft(), title="QLBM Fluid Simulation (2D)") as demo:
    with gr.Tabs():
        with gr.TabItem("Introduction"):
            gr.Markdown(
                """
                # Quantum Lattice Boltzmann (QLBM)
                This app runs a 2D quantum-inspired Lattice Boltzmann Method using a D2Q5 model (5 discrete velocity directions) to evolve a scalar density field with uniform advection and periodic boundaries.

                What is simulated
                - D2Q5 advection–diffusion on a square grid (periodic BCs only).
                - Uniform velocity (Vx, Vy) applied to all cells.
                - State is evolved via CUDA-Q kernels; results are downsampled and visualized as 3D surfaces with a time slider.

                How it works (from the implementation)
                - Grid size N = 2^q (q = qubits per spatial dimension). Total qubits = 2*q + 3 direction qubits.
                - Per timestep: a Householder-based prep_op initializes direction amplitudes, then conditional shifts stream along x and y (lshift/rshift) controlled by direction bits, followed by an unprep.
                - Up to 40 uniform time samples between 0..T are saved for visualization.

                Visualization pipeline
                - Only the real component of the state is saved and plotted; the initialized field is normalized before evolution.
                - Data is saved as .npy in a temporary folder, downsampled by a factor of 2^5 (32). For very small grids, the factor is reduced to keep at least 1×1.
                - Plotly renders one surface per saved frame; a slider toggles visibility. You can download per-frame Plotly JSON for offline analysis.

                Parameters you control
                - Grid Size/Direction: N = 2^q. The UI snaps any input to the nearest power of two in [128, 4096]. Larger N increases memory and runtime.
                - Time Steps (T): total evolution steps; up to 40 frames are sampled in [0, T].
                - Initial Distribution: Sinusoidal or Gaussian.
                - Velocity Field: Uniform (set Vx, Vy).
                - Boundary Condition: Periodic.

                Practical notes
                - Recommended time steps ≈ q * 200.
                - N > 1024 may exceed available memory/time.
                - Requires a CUDA-capable GPU and CUDA-Q runtime; CPU fallback will be slow or may fail.

                Workflow
                1) Review example initial distributions.
                2) Choose grid size N and time steps T.
                3) Set Vx and Vy; keep Periodic BC.
                4) Run the simulation; use the slider to browse frames.
                5) Optionally download the plotted frames as JSON for offline analysis.
                """
            )

        with gr.TabItem("Simulation"):
            with gr.Row(): # Main row for top section
                with gr.Column(scale=1): # Column for left-side controls
                    gr.Markdown("## Initial Distribution Examples")
                    with gr.Row():
                        with gr.Column(scale=1):
                            example1 = LitModel3D("Placeholder_Images/sinusoidal.stl", label="Sinusoidal")
                            sinusoidal_btn_2d = gr.Button("Sinusoidal")
                        with gr.Column(scale=1):
                            example2 = LitModel3D("Placeholder_Images/gaussian.stl", label="Gaussian")
                            gaussian_btn_2d = gr.Button("Gaussian")

                    gr.Markdown("## Simulation Parameters")
                    num_reg_qubits_input_2d = gr.Slider(
                        minimum=2**7, maximum=2**12, # Grid size values
                        value=2**8, step=1, # use step=1 for broader Gradio compatibility
                        label="Grid Size/Direction"
                    )
                    time_steps_slider_2d = gr.Slider(minimum=0, maximum=4000, value=1600, step=10, label="Time Steps")
                    qubit_plot_2d = gr.Plot(label="Qubits vs. Grid Size")
                    total_qubits_display_2d = gr.Markdown("Total Qubits: 19")
                    warning_display_2d = gr.Markdown("")
                    recommended_time_steps_display_2d = gr.Markdown("Recommended time steps: 1600")

                with gr.Column(scale=2): # Column for the main plot
                    qlbm_interactive_plot_2d = gr.Plot(label="QLBM")
                    download_button_2d = gr.DownloadButton(label="Download Plot Data (JSON)", visible=False) # New download button
                    plotly_json_frames_state_2d = gr.State([]) # New state to store Plotly JSON frames

            with gr.Row(): # Row for bottom section
                with gr.Column(scale=1):
                    gr.Markdown("## Initialization (Uniform)")
                    selected_velocity_field_2d = gr.State("Uniform")
                    vx_slider_2d = gr.Slider(minimum=-0.3, maximum=0.3, value=0.2, step=0.01, label="V_x", visible=True)
                    vy_slider_2d = gr.Slider(minimum=-0.3, maximum=0.3, value=0.15, step=0.01, label="V_y", visible=True)
                    distribution_type_input_2d = gr.Radio(choices=["Gaussian", "Sinusoidal"], value="Sinusoidal", label="Initial Distribution Type")

                with gr.Column(scale=1):
                    gr.Markdown("## Boundary Conditions")
                    boundary_condition_input_2d = gr.Radio(choices=["Periodic"], value="Periodic", label="Boundary Condition")

            run_qlbm_btn_2d = gr.Button("Run Simulation", variant="primary") # Button below the sections

            qlbm_inputs_list_2d = [
                num_reg_qubits_input_2d, time_steps_slider_2d, distribution_type_input_2d,
                selected_velocity_field_2d, vx_slider_2d, vy_slider_2d, boundary_condition_input_2d
            ]
            run_qlbm_btn_2d.click(
                fn=qlbm_gradio_interface,
                inputs=qlbm_inputs_list_2d,
                outputs=[qlbm_interactive_plot_2d, plotly_json_frames_state_2d] # Modified output
            ).then(
                lambda: gr.update(visible=True), # Make download button visible after simulation
                outputs=[download_button_2d]
            )
            download_button_2d.click(
                fn=download_plot_data,
                inputs=[plotly_json_frames_state_2d],
                outputs=[download_button_2d]
            )
            num_reg_qubits_input_2d.change(
                fn=update_grid_and_qubit_info,
                inputs=num_reg_qubits_input_2d,
                outputs=[num_reg_qubits_input_2d, qubit_plot_2d, total_qubits_display_2d, warning_display_2d, recommended_time_steps_display_2d]
            )
            sinusoidal_btn_2d.click(
                fn=set_sinusoidal_example,
                inputs=[],
                outputs=[num_reg_qubits_input_2d, time_steps_slider_2d, distribution_type_input_2d, selected_velocity_field_2d, vx_slider_2d, vy_slider_2d, boundary_condition_input_2d]
            )
            gaussian_btn_2d.click(
                fn=set_gaussian_example,
                inputs=[],
                outputs=[num_reg_qubits_input_2d, time_steps_slider_2d, distribution_type_input_2d, selected_velocity_field_2d, vx_slider_2d, vy_slider_2d, boundary_condition_input_2d]
            )

if __name__ == "__main__":
    try:
        cudaq.set_target('nvidia', option='fp64')
        print(f"CUDA-Q Target successfully set to: {cudaq.get_target().name}")
    except Exception as e_target:
        print(f"Warning: Could not set CUDA-Q target to 'nvidia'. Error: {e_target}")
        print(f"Current CUDA-Q Target: {cudaq.get_target().name}. Performance may be affected.")

    # Force local serving; disable any implicit sharing/tunneling
    import os
    os.environ["GRADIO_SHARE"] = "0"
    os.environ["GRADIO_ANALYTICS_ENABLED"] = "0"
    os.environ["GRADIO_LAUNCH_BROWSER"] = "0"

    # Bind to all interfaces so the host can reach it; no share
    demo.launch(server_name="0.0.0.0", server_port=7860, share=False, inbrowser=False, show_error=True)