harishaseebat92 commited on
Commit
bfbcc6e
·
verified ·
1 Parent(s): 98575a1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1120 -497
app.py CHANGED
@@ -1,514 +1,1137 @@
1
- import os
2
  import math
 
3
  import tempfile
 
4
  import gradio as gr
 
5
  import cudaq
 
6
  import numpy as np
 
7
  import cupy as cp
 
8
  from pathlib import Path
 
9
  import plotly.graph_objects as go
 
10
  import plotly.io as pio
11
- import imageio # Keep for potential future use, but GIF generation removed
 
 
12
  from scipy.spatial import Delaunay
13
 
 
 
14
  # Set Plotly engine for image export
 
15
  try:
16
- pio.kaleido.scope.mathjax = None
 
 
17
  except AttributeError:
18
- pass
19
-
20
- def simulate_qlbm_and_animate(num_reg_qubits: int, T: int, distribution_type: str, ux_input: float, uy_input: float, velocity_field_type: str):
21
- """
22
- Simulates a 2D advection-diffusion problem using a Quantum Lattice Boltzmann Method (QLBM)
23
- and generates an interactive Plotly figure with a slider for selected time steps.
24
- """
25
- # GIF related variables removed as GIF generation is no longer needed
26
- # video_length = T
27
- # simulation_fps = 0.1
28
- # frames = int(simulation_fps * video_length)
29
- # if frames == 0:
30
- # frames = 1
31
-
32
- num_anc = 3
33
- num_qubits_total = 2 * num_reg_qubits + num_anc
34
- current_N = 2**num_reg_qubits
35
- N_tot_state_vector = 2**num_qubits_total
36
- num_ranks = 1
37
- rank = 0
38
- N_sub_per_rank = int(N_tot_state_vector // num_ranks)
39
-
40
- # timesteps_per_frame logic removed as it was for GIF
41
- # timesteps_per_frame = 1
42
- # if frames < T and frames > 0:
43
- # timesteps_per_frame = int(T / frames)
44
- # if timesteps_per_frame == 0:
45
- # timesteps_per_frame = 1
46
-
47
- # Initial state setup
48
- if distribution_type == "Sine Wave (Original)":
49
- selected_initial_state_function_raw = lambda x, y, N_val_func: \
50
- np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
51
- np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
52
- elif distribution_type == "Gaussian":
53
- selected_initial_state_function_raw = lambda x, y, N_val_func: \
54
- np.exp(-((x - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2) +
55
- (y - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2))) * 1.8 + 0.2
56
- elif distribution_type == "Random":
57
- selected_initial_state_function_raw = lambda x, y, N_val_func: \
58
- np.random.rand(N_val_func, N_val_func) * 1.5 + 0.2 if isinstance(x, int) else \
59
- np.random.rand(x.shape, x.shape[3]) * 1.5 + 0.2
60
- else:
61
- print(f"Warning: Unknown distribution type '{distribution_type}'. Defaulting to Sine Wave.")
62
- selected_initial_state_function_raw = lambda x, y, N_val_func: \
63
- np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
64
- np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
65
-
66
- initial_state_func_eval = lambda x_coords, y_coords: \
67
- selected_initial_state_function_raw(x_coords, y_coords, current_N) * \
68
- (y_coords < current_N).astype(int)
69
-
70
- with tempfile.TemporaryDirectory() as tmp_npy_dir:
71
- intermediate_folder_path = Path(tmp_npy_dir)
72
-
73
- cudaq.set_target('nvidia', option='fp64')
74
-
75
- @cudaq.kernel
76
- def alloc_kernel(num_qubits_alloc: int):
77
- qubits = cudaq.qvector(num_qubits_alloc)
78
-
79
- from cupy.cuda.memory import MemoryPointer, UnownedMemory
80
-
81
- def to_cupy_array(state):
82
- tensor = state.getTensor()
83
- pDevice = tensor.data()
84
- sizeByte = tensor.get_num_elements() * tensor.get_element_size()
85
- mem = UnownedMemory(pDevice, sizeByte, owner=state)
86
- memptr_obj = MemoryPointer(mem, 0)
87
- cupy_array_val = cp.ndarray(tensor.get_num_elements(),
88
- dtype=cp.complex128,
89
- memptr=memptr_obj)
90
- return cupy_array_val
91
-
92
- class QLBMAdvecDiffD2Q5_new:
93
- def __init__(self, ux=0.2, uy=0.15) -> None:
94
- self.dim = 2
95
- self.ndir = 5
96
- self.nq_dir = math.ceil(np.log2(self.ndir))
97
- self.dirs =
98
- for dir_int in range(self.ndir):
99
- dir_bin = f"{dir_int:b}".zfill(self.nq_dir)
100
- self.dirs.append(dir_bin)
101
- self.e_unitvec = np.array([0, 1, -1, 1, -1])
102
- self.wts = np.array([2/6, 1/6, 1/6, 1/6, 1/6])
103
- self.cs = 1 / np.sqrt(3)
104
- self.ux = ux
105
- self.uy = uy
106
- self.u = np.array([0, self.ux, self.ux, self.uy, self.uy])
107
- self.wtcoeffs = np.multiply(self.wts, 1 + self.e_unitvec * self.u / self.cs**2)
108
- self.create_circuit()
109
-
110
- def create_circuit(self):
111
- v = np.pad(self.wtcoeffs, (0, 2**num_anc - self.ndir))
112
- v = v**0.5
113
- v += 1 # Original line was v += 1, not v += 1
114
- v = v / np.linalg.norm(v)
115
- U_prep = 2 * np.outer(v, v) - np.eye(len(v))
116
- cudaq.register_operation("prep_op", U_prep)
117
-
118
- def collisionOp(dirs_list):
119
- dirs_i_list_val =
120
- for dir_str in dirs_list:
121
- dirs_i = [(int(c)) for c in dir_str]
122
- dirs_i_list_val += dirs_i[::-1]
123
- return dirs_i_list_val
124
-
125
- self.dirs_i_list = collisionOp(self.dirs)
126
-
127
- @cudaq.kernel
128
- def rshift(q: cudaq.qview, n: int):
129
- for i in range(n):
130
- if i == n - 1:
131
- x(q[n - 1 - i])
132
- elif i == n - 2:
133
- x.ctrl(q[n - 1 - (i + 1)], q[n - 1 - i])
134
- else:
135
- x.ctrl(q[0:n - 1 - i], q[n - 1 - i])
136
-
137
- @cudaq.kernel
138
- def lshift(q: cudaq.qview, n: int):
139
- for i in range(n):
140
- if i == 0:
141
- x(q) # Corrected from x(q)
142
- elif i == 1:
143
- x.ctrl(q, q[3])
144
- else:
145
- x.ctrl(q[0:i], q[i])
146
-
147
- @cudaq.kernel
148
- def d2q5_tstep(q: cudaq.qview, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
149
- qx = q[0:nqx]
150
- qy = q[nqx:nqx + nqy]
151
- qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
152
-
153
- idx_lqx = 2
154
- b_list = dirs_i_val[idx_lqx * nq_dir_val:(idx_lqx + 1) * nq_dir_val]
155
- for j in range(nq_dir_val):
156
- if b_list[j] == 0: x(qdir[j])
157
- cudaq.control(lshift, qdir, qx, nqx)
158
- for j in range(nq_dir_val):
159
- if b_list[j] == 0: x(qdir[j])
160
-
161
- idx_rqx = 1
162
- b_list = dirs_i_val[idx_rqx * nq_dir_val:(idx_rqx + 1) * nq_dir_val]
163
- for j in range(nq_dir_val):
164
- if b_list[j] == 0: x(qdir[j])
165
- cudaq.control(rshift, qdir, qx, nqx)
166
- for j in range(nq_dir_val):
167
- if b_list[j] == 0: x(qdir[j])
168
-
169
- idx_lqy = 4
170
- b_list = dirs_i_val[idx_lqy * nq_dir_val:(idx_lqy + 1) * nq_dir_val]
171
- for j in range(nq_dir_val):
172
- if b_list[j] == 0: x(qdir[j])
173
- cudaq.control(lshift, qdir, qy, nqy)
174
- for j in range(nq_dir_val):
175
- if b_list[j] == 0: x(qdir[j])
176
-
177
- idx_rqy = 3
178
- b_list = dirs_i_val[idx_rqy * nq_dir_val:(idx_rqy + 1) * nq_dir_val]
179
- for j in range(nq_dir_val):
180
- if b_list[j] == 0: x(qdir[j])
181
- cudaq.control(rshift, qdir, qy, nqy)
182
- for j in range(nq_dir_val):
183
- if b_list[j] == 0: x(qdir[j])
184
-
185
- @cudaq.kernel
186
- def d2q5_tstep_wrapper(state_arg: cudaq.State, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
187
- q = cudaq.qvector(state_arg)
188
- qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
189
- prep_op(qdir[4], qdir[3], qdir) # Corrected from qdir
190
- d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
191
- prep_op(qdir[4], qdir[3], qdir) # Corrected from qdir
192
-
193
- @cudaq.kernel
194
- def d2q5_tstep_wrapper_hadamard(vec_arg: list[complex], nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
195
- q = cudaq.qvector(vec_arg)
196
- qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
197
- qy = q[nqx:nqx + nqy]
198
- prep_op(qdir[4], qdir[3], qdir) # Corrected from qdir
199
- d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
200
- prep_op(qdir[4], qdir[3], qdir) # Corrected from qdir
201
- for i in range(nqy):
202
- h(qy[i])
203
-
204
- def run_timestep_func(vec_arg, hadamard=False):
205
- if hadamard:
206
- result = cudaq.get_state(d2q5_tstep_wrapper_hadamard, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
207
- else:
208
- result = cudaq.get_state(d2q5_tstep_wrapper, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
209
- num_nonzero_ranks = num_ranks / (2**num_anc)
210
- rank_slice_cupy = to_cupy_array(result)
211
- if rank >= num_nonzero_ranks and num_nonzero_ranks > 0:
212
- sub_sv_zeros = np.zeros(N_sub_per_rank, dtype=np.complex128)
213
- cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_zeros.ctypes.data, sub_sv_zeros.nbytes, cp.cuda.runtime.memcpyHostToDevice)
214
- if rank == 0 and num_nonzero_ranks < 1 and N_sub_per_rank > 0:
215
- limit_idx = int(N_tot_state_vector / (2**num_anc))
216
- if limit_idx < rank_slice_cupy.size:
217
- rank_slice_cupy[limit_idx:] = 0
218
- return result
219
- self.run_timestep = run_timestep_func
220
-
221
- def write_state(self, state_to_write, t_step_str_val):
222
- rank_slice_cupy = to_cupy_array(state_to_write)
223
- num_nonzero_ranks = num_ranks / (2**num_anc)
224
- if rank < num_nonzero_ranks or (rank == 0 and num_nonzero_ranks <= 0):
225
- save_path = intermediate_folder_path / f"{t_step_str_val}_{rank}.npy"
226
- with open(save_path, 'wb') as f:
227
- arr_to_save = None
228
- data_limit = N_sub_per_rank
229
- if num_nonzero_ranks < 1 and rank == 0:
230
- data_limit = int(N_tot_state_vector / (2**num_anc))
231
- if data_limit > 0:
232
- relevant_part_cupy = cp.real(rank_slice_cupy[:data_limit])
233
- else:
234
- relevant_part_cupy = cp.array(, dtype=cp.float64)
235
- if relevant_part_cupy.size >= current_N * current_N:
236
- arr_flat = relevant_part_cupy[:current_N * current_N]
237
- if downsampling_factor > 1 and current_N > 0:
238
- arr_reshaped = arr_flat.reshape((current_N, current_N))
239
- arr_downsampled = arr_reshaped[::downsampling_factor, ::downsampling_factor]
240
- arr_to_save = arr_downsampled.flatten()
241
- else:
242
- arr_to_save = arr_flat
243
- elif relevant_part_cupy.size > 0:
244
- if downsampling_factor > 1:
245
- arr_to_save = relevant_part_cupy[::downsampling_factor]
246
- else:
247
- arr_to_save = relevant_part_cupy
248
- if arr_to_save is not None and arr_to_save.size > 0:
249
- np.save(f, arr_to_save.get() if isinstance(arr_to_save, cp.ndarray) else arr_to_save)
250
-
251
- def run_evolution(self, initial_state_arg, total_timesteps, observable=False, timesteps_to_save=None):
252
- current_state_val = initial_state_arg
253
- for t_iter in range(total_timesteps):
254
- next_state_val = None
255
- if t_iter == total_timesteps - 1 and observable:
256
- next_state_val = self.run_timestep(current_state_val, True)
257
- self.write_state(next_state_val, str(t_iter)) # Save final state
258
- else:
259
- next_state_val = self.run_timestep(current_state_val)
260
- # Save data only for specific intervals for the slider
261
- if timesteps_to_save and t_iter in timesteps_to_save:
262
- self.write_state(next_state_val, str(t_iter))
263
- if rank == 0 and t_iter % 10 == 0: # Print progress less frequently
264
- print(f"Timestep: {t_iter}/{total_timesteps}")
265
- cp.get_default_memory_pool().free_all_blocks()
266
- current_state_val = next_state_val
267
- if rank == 0:
268
- print(f"Timestep: {total_timesteps}/{total_timesteps} (Evolution complete)")
269
- cp.get_default_memory_pool().free_all_blocks()
270
- self.final_state = current_state_val
271
-
272
- downsampling_factor = 2**5
273
- if current_N == 0:
274
- print("Error: current_N is zero. num_reg_qubits likely too small.")
275
- return None, None
276
- if current_N < downsampling_factor:
277
- downsampling_factor = current_N if current_N > 0 else 1
278
-
279
- qlbm_obj = QLBMAdvecDiffD2Q5_new(ux=ux_input, uy=uy_input)
280
- initial_state_val = cudaq.get_state(alloc_kernel, num_qubits_total)
281
-
282
- xv_init = np.arange(current_N)
283
- yv_init = np.arange(current_N)
284
- initial_grid_2d_X, initial_grid_2d_Y = np.meshgrid(xv_init, yv_init)
285
-
286
- if distribution_type == "Random":
287
- initial_grid_2d = selected_initial_state_function_raw(current_N, current_N, current_N)
288
- else:
289
- initial_grid_2d = initial_state_func_eval(initial_grid_2d_X, initial_grid_2d_Y)
290
-
291
- sub_sv_init_flat = initial_grid_2d.flatten().astype(np.complex128)
292
- full_initial_sv_host = np.zeros(N_sub_per_rank, dtype=np.complex128)
293
- num_computational_states = current_N * current_N
294
-
295
- if len(sub_sv_init_flat) == num_computational_states:
296
- if num_computational_states <= N_sub_per_rank:
297
- full_initial_sv_host[:num_computational_states] = sub_sv_init_flat
298
- else:
299
- print(f"Error: Grid data {num_computational_states} > N_sub_per_rank {N_sub_per_rank}")
300
- return None, None
301
- else:
302
- print(f"Warning: Initial state size {len(sub_sv_init_flat)}!= expected {num_computational_states}")
303
- fill_len = min(len(sub_sv_init_flat), num_computational_states, N_sub_per_rank)
304
- full_initial_sv_host[:fill_len] = sub_sv_init_flat[:fill_len]
305
-
306
- rank_slice_init = to_cupy_array(initial_state_val)
307
- print(f'Rank {rank}: Initializing state with {distribution_type} (ux={ux_input}, uy={uy_input})...')
308
- cp.cuda.runtime.memcpy(rank_slice_init.data.ptr, full_initial_sv_host.ctypes.data, full_initial_sv_host.nbytes, cp.cuda.runtime.memcpyHostToDevice)
309
- print(f'Rank {rank}: Initial state copied. Size: {len(sub_sv_init_flat)}. N_sub_per_rank: {N_sub_per_rank}')
310
-
311
- # Explicitly save initial state (t=0)
312
- qlbm_obj.write_state(initial_state_val, "0")
313
-
314
- print("Starting QLBM evolution...")
315
- # Define specific timesteps to save for the slider
316
- timesteps_for_slider = # T-1 is the last t_iter
317
- qlbm_obj.run_evolution(initial_state_val, T, timesteps_to_save=timesteps_for_slider)
318
- print("QLBM evolution complete.")
319
-
320
- print("Generating plots with Plotly...")
321
- downsampled_N = current_N // downsampling_factor
322
- if downsampled_N == 0 and current_N > 0:
323
- downsampled_N = 1
324
- elif current_N == 0:
325
- print("Error: current_N is zero before Plotly stage.")
326
- return None, None
327
-
328
- # Load data for specific time steps for interactive plot
329
- # These correspond to the filenames saved: 0, T//4, 3*T//4, T-1
330
- time_steps_to_load =
331
- data_frames =
332
- actual_timesteps_loaded =
333
- for t in time_steps_to_load:
334
- file_path = intermediate_folder_path / f"{t}_{rank}.npy"
335
- if file_path.exists():
336
- sol_loaded = np.load(file_path)
337
- if sol_loaded.size == downsampled_N * downsampled_N:
338
- Z_data = np.reshape(sol_loaded, (downsampled_N, downsampled_N))
339
- data_frames.append(Z_data)
340
- actual_timesteps_loaded.append(t)
341
- else:
342
- print(f"Warning: File {file_path} size {sol_loaded.size}!= expected {downsampled_N*downsampled_N}. Skipping.")
343
- else:
344
- print(f"Warning: File {file_path} not found. Skipping.")
345
-
346
- if not data_frames:
347
- print("Error: No data frames loaded for interactive plot.")
348
- return None, None
349
-
350
- x_coords_plot = np.linspace(-10, 10, downsampled_N)
351
- y_coords_plot = np.linspace(-10, 10, downsampled_N)
352
-
353
- # Calculate global min/max for consistent scaling
354
- z_min = min([np.min(Z) for Z in data_frames])
355
- z_max = max([np.max(Z) for Z in data_frames])
356
- if z_max == z_min:
357
- z_max += 1e-9
358
-
359
- # Create interactive Plotly figure with slider
360
- fig = go.Figure()
361
-
362
- for i, Z in enumerate(data_frames):
363
- fig.add_trace(
364
- go.Surface(
365
- z=Z, x=x_coords_plot, y=y_coords_plot,
366
- colorscale='Viridis',
367
- cmin=z_min, cmax=z_max,
368
- name=f'Time: {actual_timesteps_loaded[i]}',
369
- showscale=(i == 0) # Show color scale only for the first trace
370
- )
371
- )
372
-
373
- steps =
374
- for i in range(len(data_frames)):
375
- step = dict(
376
- method="update",
377
- args=[{"visible": [False] * len(data_frames)}],
378
- label=f"Time: {actual_timesteps_loaded[i]}"
379
- )
380
- step["args"]["visible"][i] = True
381
- steps.append(step)
382
-
383
- sliders =
384
-
385
- fig.update_layout(
386
- title='QLBM Simulation - Density Evolution',
387
- scene=dict(
388
- xaxis_title='X',
389
- yaxis_title='Y',
390
- zaxis_title='Density',
391
- xaxis=dict(range=[x_coords_plot, x_coords_plot[-1]]),
392
- yaxis=dict(range=[y_coords_plot, y_coords_plot[-1]]),
393
- zaxis=dict(range=[z_min, z_max]),
394
- ),
395
- sliders=sliders,
396
- width=1000, # Increased width
397
- height=900 # Increased height
398
- )
399
-
400
- # GIF generation logic removed as per request
401
- #... (removed all GIF related code)...
402
-
403
- return fig # Return only the interactive Plotly figure
404
 
405
- # Gradio Interface Definition
406
- def qlbm_gradio_interface(num_reg_qubits_input: int, timescale_input: int, distribution_type_param: str, ux_param: float, uy_param: float, velocity_field_type_param: str):
407
- num_reg_qubits_val = int(num_reg_qubits_input)
408
- timescale_val = int(timescale_input)
409
- ux_val = float(ux_param)
410
- uy_val = float(uy_param)
411
-
412
- print(f"Gradio Interface: num_reg_qubits={num_reg_qubits_val}, T={timescale_val}, Distribution={distribution_type_param}, ux={ux_val}, uy={uy_val}, VelocityFieldType={velocity_field_type_param}")
413
-
414
- plot_fig = simulate_qlbm_and_animate( # Only expecting plot_fig now
415
- num_reg_qubits=num_reg_qubits_val,
416
- T=timescale_val,
417
- distribution_type=distribution_type_param,
418
- ux_input=ux_val,
419
- uy_input=uy_val,
420
- velocity_field_type=velocity_field_type_param # Pass the new dummy parameter
421
- )
422
-
423
- if plot_fig is None:
424
- gr.Warning("Simulation or plotting failed. Please check console for errors.")
425
- return None
426
- return plot_fig # Return only the interactive Plotly figure
427
 
428
- with gr.Blocks(theme=gr.themes.Soft(), title="QLBM Simulation with Plotly") as qlbm_demo:
429
- gr.Markdown(
430
- """
431
- # ⚛️ Quantum Lattice Boltzmann Method (QLBM) Simulator (Plotly Animation)
432
- Welcome to the Quantum Lattice Boltzmann Method (QLBM) simulator! This version uses Plotly for 3D animation and interactive plots.
433
-
434
- **How this Simulator Works:**
435
- This simulator implements a D2Q5 model on a quantum computer simulator (CUDA-Q).
436
- - Control grid size (via Number of Register Qubits: $N=2^{\text{num_reg_qubits}}$).
437
- - Set total simulation time (Timescale T).
438
- - Choose initial distribution.
439
- - Set advection velocities `ux` and `uy`.
440
- The simulation generates an interactive Plotly figure with a slider for selected time steps.
441
-
442
- **Note:** Higher qubit counts and longer timescales are computationally intensive. Advection velocities should be small (e.g., < 0.3).
443
- The Plotly figure allows interactive exploration of specific time steps.
444
- """
445
- )
446
- with gr.Row():
447
- with gr.Column(scale=1):
448
- gr.Markdown("## Simulation Parameters")
449
- num_reg_qubits_slider = gr.Slider(
450
- minimum=2, maximum=10, value=8, step=1,
451
- label="Number of Register Qubits (num_reg_qubits)",
452
- info="Grid N = 2^num_reg_qubits. Max 10 (Note: >8 slow; >9 may hit simulator/memory limits on free tiers)."
453
- )
454
- timescale_slider = gr.Slider(
455
- minimum=0, maximum=2000, value=100, step=10,
456
- label="Timescale (T)", info="Total number of timesteps. Max 2000."
457
- )
458
-
459
- # Group 1: Initial Conditions
460
- with gr.Accordion("Initial Conditions", open=True):
461
- distribution_options =
462
- distribution_type_input = gr.Radio(
463
- choices=distribution_options, value="Sine Wave (Original)",
464
- label="Initial Distribution Type", info="Select the initial pattern of the substance."
465
- )
466
-
467
- # Group 2: Velocity Fields
468
- with gr.Accordion("Velocity Fields", open=True):
469
- velocity_field_options = # Dummy options
470
- velocity_field_type_input = gr.Radio(
471
- choices=velocity_field_options, value="Uniform",
472
- label="Velocity Field Type", info="Select the type of background velocity field."
473
- )
474
- ux_slider = gr.Slider(
475
- minimum=-0.4, maximum=0.4, value=0.2, step=0.01,
476
- label="Advection Velocity ux", info="x-component of background advection."
477
- )
478
- uy_slider = gr.Slider(
479
- minimum=-0.4, maximum=0.4, value=0.15, step=0.01,
480
- label="Advection Velocity uy", info="y-component of background advection."
481
- )
482
-
483
- run_qlbm_btn = gr.Button("Run QLBM Simulation", variant="primary")
484
-
485
- with gr.Column(scale=2):
486
- # Removed gr.Image for GIF
487
- # qlbm_plot_output = gr.Image(label="QLBM Simulation Animation (GIF)", type="filepath", height=900)
488
- qlbm_interactive_plot = gr.Plot(label="Interactive Density Plot with Slider")
489
-
490
- qlbm_inputs_list = [num_reg_qubits_slider, timescale_slider, distribution_type_input, ux_slider, uy_slider, velocity_field_type_input]
491
- run_qlbm_btn.click(
492
- fn=qlbm_gradio_interface,
493
- inputs=qlbm_inputs_list,
494
- outputs=[qlbm_interactive_plot] # Only interactive plot
495
- )
496
- gr.Examples(
497
- examples=,
498
- [6, 50, "Gaussian", 0.1, 0.05, "Uniform"],
499
- ,
500
- inputs=qlbm_inputs_list,
501
- outputs=[qlbm_interactive_plot], # Only interactive plot
502
- fn=qlbm_gradio_interface,
503
- cache_examples=False
504
- )
505
 
506
- if __name__ == "__main__":
507
- try:
508
- cudaq.set_target('nvidia', option='fp64')
509
- print(f"CUDA-Q Target successfully set to: {cudaq.get_target().name}")
510
- except Exception as e_target:
511
- print(f"Warning: Could not set CUDA-Q target to 'nvidia'. Error: {e_target}")
512
- print(f"Current CUDA-Q Target: {cudaq.get_target().name}. Performance may be affected.")
513
-
514
- qlbm_demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import math
2
+
3
  import tempfile
4
+
5
  import gradio as gr
6
+
7
  import cudaq
8
+
9
  import numpy as np
10
+
11
  import cupy as cp
12
+
13
  from pathlib import Path
14
+
15
  import plotly.graph_objects as go
16
+
17
  import plotly.io as pio
18
+
19
+ import imageio
20
+
21
  from scipy.spatial import Delaunay
22
 
23
+
24
+
25
  # Set Plotly engine for image export
26
+
27
  try:
28
+
29
+     pio.kaleido.scope.mathjax = None
30
+
31
  except AttributeError:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+     pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+
37
+ def simulate_qlbm_and_animate(num_reg_qubits: int, T: int, distribution_type: str, ux_input: float, uy_input: float):
38
+
39
+     """
40
+
41
+     Simulates a 2D advection-diffusion problem using a Quantum Lattice Boltzmann Method (QLBM)
42
+
43
+     and generates a GIF animation and an interactive Plotly figure with a slider for selected time steps.
44
+
45
+     """
46
+
47
+     video_length = T
48
+
49
+     simulation_fps = 0.1
50
+
51
+     frames = int(simulation_fps * video_length)
52
+
53
+     if frames == 0:
54
+
55
+         frames = 1
56
+
57
+
58
+
59
+     num_anc = 3
60
+
61
+     num_qubits_total = 2 * num_reg_qubits + num_anc
62
+
63
+     current_N = 2**num_reg_qubits
64
+
65
+     N_tot_state_vector = 2**num_qubits_total
66
+
67
+     num_ranks = 1
68
+
69
+     rank = 0
70
+
71
+     N_sub_per_rank = int(N_tot_state_vector // num_ranks)
72
+
73
+
74
+
75
+     timesteps_per_frame = 1
76
+
77
+     if frames < T and frames > 0:
78
+
79
+         timesteps_per_frame = int(T / frames)
80
+
81
+     if timesteps_per_frame == 0:
82
+
83
+         timesteps_per_frame = 1
84
+
85
+
86
+
87
+     # Initial state setup
88
+
89
+     if distribution_type == "Sine Wave (Original)":
90
+
91
+         selected_initial_state_function_raw = lambda x, y, N_val_func: \
92
+
93
+             np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
94
+
95
+             np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
96
+
97
+     elif distribution_type == "Gaussian":
98
+
99
+         selected_initial_state_function_raw = lambda x, y, N_val_func: \
100
+
101
+             np.exp(-((x - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2) +
102
+
103
+                      (y - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2))) * 1.8 + 0.2
104
+
105
+     elif distribution_type == "Random":
106
+
107
+         selected_initial_state_function_raw = lambda x, y, N_val_func: \
108
+
109
+             np.random.rand(N_val_func, N_val_func) * 1.5 + 0.2 if isinstance(x, int) else \
110
+
111
+             np.random.rand(x.shape[0], x.shape[1]) * 1.5 + 0.2
112
+
113
+     else:
114
+
115
+         print(f"Warning: Unknown distribution type '{distribution_type}'. Defaulting to Sine Wave.")
116
+
117
+         selected_initial_state_function_raw = lambda x, y, N_val_func: \
118
+
119
+             np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
120
+
121
+             np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
122
+
123
+
124
+
125
+     initial_state_func_eval = lambda x_coords, y_coords: \
126
+
127
+         selected_initial_state_function_raw(x_coords, y_coords, current_N) * \
128
+
129
+         (y_coords < current_N).astype(int)
130
+
131
+
132
+
133
+     with tempfile.TemporaryDirectory() as tmp_npy_dir:
134
+
135
+         intermediate_folder_path = Path(tmp_npy_dir)
136
+
137
+
138
+
139
+         cudaq.set_target('nvidia', option='fp64')
140
+
141
+
142
+
143
+         @cudaq.kernel
144
+
145
+         def alloc_kernel(num_qubits_alloc: int):
146
+
147
+             qubits = cudaq.qvector(num_qubits_alloc)
148
+
149
+
150
+
151
+         from cupy.cuda.memory import MemoryPointer, UnownedMemory
152
+
153
+
154
+
155
+         def to_cupy_array(state):
156
+
157
+             tensor = state.getTensor()
158
+
159
+             pDevice = tensor.data()
160
+
161
+             sizeByte = tensor.get_num_elements() * tensor.get_element_size()
162
+
163
+             mem = UnownedMemory(pDevice, sizeByte, owner=state)
164
+
165
+             memptr_obj = MemoryPointer(mem, 0)
166
+
167
+             cupy_array_val = cp.ndarray(tensor.get_num_elements(),
168
+
169
+                                     dtype=cp.complex128,
170
+
171
+                                     memptr=memptr_obj)
172
+
173
+             return cupy_array_val
174
+
175
+
176
+
177
+         class QLBMAdvecDiffD2Q5_new:
178
+
179
+             def __init__(self, ux=0.2, uy=0.15) -> None:
180
+
181
+                 self.dim = 2
182
+
183
+                 self.ndir = 5
184
+
185
+                 self.nq_dir = math.ceil(np.log2(self.ndir))
186
+
187
+                 self.dirs = []
188
+
189
+                 for dir_int in range(self.ndir):
190
+
191
+                     dir_bin = f"{dir_int:b}".zfill(self.nq_dir)
192
+
193
+                     self.dirs.append(dir_bin)
194
+
195
+                 self.e_unitvec = np.array([0, 1, -1, 1, -1])
196
+
197
+                 self.wts = np.array([2/6, 1/6, 1/6, 1/6, 1/6])
198
+
199
+                 self.cs = 1 / np.sqrt(3)
200
+
201
+                 self.ux = ux
202
+
203
+                 self.uy = uy
204
+
205
+                 self.u = np.array([0, self.ux, self.ux, self.uy, self.uy])
206
+
207
+                 self.wtcoeffs = np.multiply(self.wts, 1 + self.e_unitvec * self.u / self.cs**2)
208
+
209
+                 self.create_circuit()
210
+
211
+
212
+
213
+             def create_circuit(self):
214
+
215
+                 v = np.pad(self.wtcoeffs, (0, 2**num_anc - self.ndir))
216
+
217
+                 v = v**0.5
218
+
219
+                 v[0] += 1
220
+
221
+                 v = v / np.linalg.norm(v)
222
+
223
+                 U_prep = 2 * np.outer(v, v) - np.eye(len(v))
224
+
225
+                 cudaq.register_operation("prep_op", U_prep)
226
+
227
+
228
+
229
+                 def collisionOp(dirs_list):
230
+
231
+                     dirs_i_list_val = []
232
+
233
+                     for dir_str in dirs_list:
234
+
235
+                         dirs_i = [(int(c)) for c in dir_str]
236
+
237
+                         dirs_i_list_val += dirs_i[::-1]
238
+
239
+                     return dirs_i_list_val
240
+
241
+
242
+
243
+                 self.dirs_i_list = collisionOp(self.dirs)
244
+
245
+
246
+
247
+                 @cudaq.kernel
248
+
249
+                 def rshift(q: cudaq.qview, n: int):
250
+
251
+                     for i in range(n):
252
+
253
+                         if i == n - 1:
254
+
255
+                             x(q[n - 1 - i])
256
+
257
+                         elif i == n - 2:
258
+
259
+                             x.ctrl(q[n - 1 - (i + 1)], q[n - 1 - i])
260
+
261
+                         else:
262
+
263
+                             x.ctrl(q[0:n - 1 - i], q[n - 1 - i])
264
+
265
+
266
+
267
+                 @cudaq.kernel
268
+
269
+                 def lshift(q: cudaq.qview, n: int):
270
+
271
+                     for i in range(n):
272
+
273
+                         if i == 0:
274
+
275
+                             x(q[0])
276
+
277
+                         elif i == 1:
278
+
279
+                             x.ctrl(q[0], q[1])
280
+
281
+                         else:
282
+
283
+                             x.ctrl(q[0:i], q[i])
284
+
285
+
286
+
287
+                 @cudaq.kernel
288
+
289
+                 def d2q5_tstep(q: cudaq.qview, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
290
+
291
+                     qx = q[0:nqx]
292
+
293
+                     qy = q[nqx:nqx + nqy]
294
+
295
+                     qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
296
+
297
+                     
298
+
299
+                     idx_lqx = 2
300
+
301
+                     b_list = dirs_i_val[idx_lqx * nq_dir_val:(idx_lqx + 1) * nq_dir_val]
302
+
303
+                     for j in range(nq_dir_val):
304
+
305
+                         if b_list[j] == 0: x(qdir[j])
306
+
307
+                     cudaq.control(lshift, qdir, qx, nqx)
308
+
309
+                     for j in range(nq_dir_val):
310
+
311
+                         if b_list[j] == 0: x(qdir[j])
312
+
313
+
314
+
315
+                     idx_rqx = 1
316
+
317
+                     b_list = dirs_i_val[idx_rqx * nq_dir_val:(idx_rqx + 1) * nq_dir_val]
318
+
319
+                     for j in range(nq_dir_val):
320
+
321
+                         if b_list[j] == 0: x(qdir[j])
322
+
323
+                     cudaq.control(rshift, qdir, qx, nqx)
324
+
325
+                     for j in range(nq_dir_val):
326
+
327
+                         if b_list[j] == 0: x(qdir[j])
328
+
329
+
330
+
331
+                     idx_lqy = 4
332
+
333
+                     b_list = dirs_i_val[idx_lqy * nq_dir_val:(idx_lqy + 1) * nq_dir_val]
334
+
335
+                     for j in range(nq_dir_val):
336
+
337
+                         if b_list[j] == 0: x(qdir[j])
338
+
339
+                     cudaq.control(lshift, qdir, qy, nqy)
340
+
341
+                     for j in range(nq_dir_val):
342
+
343
+                         if b_list[j] == 0: x(qdir[j])
344
+
345
+
346
+
347
+                     idx_rqy = 3
348
+
349
+                     b_list = dirs_i_val[idx_rqy * nq_dir_val:(idx_rqy + 1) * nq_dir_val]
350
+
351
+                     for j in range(nq_dir_val):
352
+
353
+                         if b_list[j] == 0: x(qdir[j])
354
+
355
+                     cudaq.control(rshift, qdir, qy, nqy)
356
+
357
+                     for j in range(nq_dir_val):
358
+
359
+                         if b_list[j] == 0: x(qdir[j])
360
+
361
+
362
+
363
+                 @cudaq.kernel
364
+
365
+                 def d2q5_tstep_wrapper(state_arg: cudaq.State, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
366
+
367
+                     q = cudaq.qvector(state_arg)
368
+
369
+                     qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
370
+
371
+                     prep_op(qdir[2], qdir[1], qdir[0])
372
+
373
+                     d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
374
+
375
+                     prep_op(qdir[2], qdir[1], qdir[0])
376
+
377
+
378
+
379
+                 @cudaq.kernel
380
+
381
+                 def d2q5_tstep_wrapper_hadamard(vec_arg: list[complex], nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
382
+
383
+                     q = cudaq.qvector(vec_arg)
384
+
385
+                     qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
386
+
387
+                     qy = q[nqx:nqx + nqy]
388
+
389
+                     prep_op(qdir[2], qdir[1], qdir[0])
390
+
391
+                     d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
392
+
393
+                     prep_op(qdir[2], qdir[1], qdir[0])
394
+
395
+                     for i in range(nqy):
396
+
397
+                         h(qy[i])
398
+
399
+
400
+
401
+                 def run_timestep_func(vec_arg, hadamard=False):
402
+
403
+                     if hadamard:
404
+
405
+                         result = cudaq.get_state(d2q5_tstep_wrapper_hadamard, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
406
+
407
+                     else:
408
+
409
+                         result = cudaq.get_state(d2q5_tstep_wrapper, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
410
+
411
+                     num_nonzero_ranks = num_ranks / (2**num_anc)
412
+
413
+                     rank_slice_cupy = to_cupy_array(result)
414
+
415
+                     if rank >= num_nonzero_ranks and num_nonzero_ranks > 0:
416
+
417
+                         sub_sv_zeros = np.zeros(N_sub_per_rank, dtype=np.complex128)
418
+
419
+                         cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_zeros.ctypes.data, sub_sv_zeros.nbytes, cp.cuda.runtime.memcpyHostToDevice)
420
+
421
+                     if rank == 0 and num_nonzero_ranks < 1 and N_sub_per_rank > 0:
422
+
423
+                         limit_idx = int(N_tot_state_vector / (2**num_anc))
424
+
425
+                         if limit_idx < rank_slice_cupy.size:
426
+
427
+                             rank_slice_cupy[limit_idx:] = 0
428
+
429
+                     return result
430
+
431
+                 self.run_timestep = run_timestep_func
432
+
433
+
434
+
435
+             def write_state(self, state_to_write, t_step_str_val):
436
+
437
+                 rank_slice_cupy = to_cupy_array(state_to_write)
438
+
439
+                 num_nonzero_ranks = num_ranks / (2**num_anc)
440
+
441
+                 if rank < num_nonzero_ranks or (rank == 0 and num_nonzero_ranks <= 0):
442
+
443
+                     save_path = intermediate_folder_path / f"{t_step_str_val}_{rank}.npy"
444
+
445
+                     with open(save_path, 'wb') as f:
446
+
447
+                         arr_to_save = None
448
+
449
+                         data_limit = N_sub_per_rank
450
+
451
+                         if num_nonzero_ranks < 1 and rank == 0:
452
+
453
+                             data_limit = int(N_tot_state_vector / (2**num_anc))
454
+
455
+                         if data_limit > 0:
456
+
457
+                             relevant_part_cupy = cp.real(rank_slice_cupy[:data_limit])
458
+
459
+                         else:
460
+
461
+                             relevant_part_cupy = cp.array([], dtype=cp.float64)
462
+
463
+                         if relevant_part_cupy.size >= current_N * current_N:
464
+
465
+                             arr_flat = relevant_part_cupy[:current_N * current_N]
466
+
467
+                             if downsampling_factor > 1 and current_N > 0:
468
+
469
+                                 arr_reshaped = arr_flat.reshape((current_N, current_N))
470
+
471
+                                 arr_downsampled = arr_reshaped[::downsampling_factor, ::downsampling_factor]
472
+
473
+                                 arr_to_save = arr_downsampled.flatten()
474
+
475
+                             else:
476
+
477
+                                 arr_to_save = arr_flat
478
+
479
+                         elif relevant_part_cupy.size > 0:
480
+
481
+                             if downsampling_factor > 1:
482
+
483
+                                 arr_to_save = relevant_part_cupy[::downsampling_factor]
484
+
485
+                             else:
486
+
487
+                                 arr_to_save = relevant_part_cupy
488
+
489
+                         if arr_to_save is not None and arr_to_save.size > 0:
490
+
491
+                             np.save(f, arr_to_save.get() if isinstance(arr_to_save, cp.ndarray) else arr_to_save)
492
+
493
+
494
+
495
+             def run_evolution(self, initial_state_arg, total_timesteps, observable=False):
496
+
497
+                 current_state_val = initial_state_arg
498
+
499
+                 for t_iter in range(total_timesteps):
500
+
501
+                     next_state_val = None
502
+
503
+                     if t_iter == total_timesteps - 1 and observable:
504
+
505
+                         next_state_val = self.run_timestep(current_state_val, True)
506
+
507
+                         self.write_state(next_state_val, str(t_iter + 1) + "_h")
508
+
509
+                     else:
510
+
511
+                         next_state_val = self.run_timestep(current_state_val)
512
+
513
+                         # Save data at specific intervals for static plots
514
+
515
+                         if t_iter == 0 or t_iter == total_timesteps // 4 or t_iter == 3 * total_timesteps // 4 or t_iter == total_timesteps - 1 or (t_iter + 1) % timesteps_per_frame == 0:
516
+
517
+                             self.write_state(next_state_val, str(t_iter + 1))
518
+
519
+                             if rank == 0:
520
+
521
+                                 print(f"Timestep: {t_iter + 1}/{total_timesteps}")
522
+
523
+                     cp.get_default_memory_pool().free_all_blocks()
524
+
525
+                     current_state_val = next_state_val
526
+
527
+                 if rank == 0:
528
+
529
+                     print(f"Timestep: {total_timesteps}/{total_timesteps} (Evolution complete)")
530
+
531
+                 cp.get_default_memory_pool().free_all_blocks()
532
+
533
+                 self.final_state = current_state_val
534
+
535
+
536
+
537
+         downsampling_factor = 2**5
538
+
539
+         if current_N == 0:
540
+
541
+             print("Error: current_N is zero. num_reg_qubits likely too small.")
542
+
543
+             return None, None
544
+
545
+         if current_N < downsampling_factor:
546
+
547
+             downsampling_factor = current_N if current_N > 0 else 1
548
+
549
+
550
+
551
+         qlbm_obj = QLBMAdvecDiffD2Q5_new(ux=ux_input, uy=uy_input)
552
+
553
+         initial_state_val = cudaq.get_state(alloc_kernel, num_qubits_total)
554
+
555
+         
556
+
557
+         xv_init = np.arange(current_N)
558
+
559
+         yv_init = np.arange(current_N)
560
+
561
+         initial_grid_2d_X, initial_grid_2d_Y = np.meshgrid(xv_init, yv_init)
562
+
563
+
564
+
565
+         if distribution_type == "Random":
566
+
567
+             initial_grid_2d = selected_initial_state_function_raw(current_N, current_N, current_N)
568
+
569
+         else:
570
+
571
+             initial_grid_2d = initial_state_func_eval(initial_grid_2d_X, initial_grid_2d_Y)
572
+
573
+
574
+
575
+         sub_sv_init_flat = initial_grid_2d.flatten().astype(np.complex128)
576
+
577
+         full_initial_sv_host = np.zeros(N_sub_per_rank, dtype=np.complex128)
578
+
579
+         num_computational_states = current_N * current_N
580
+
581
+
582
+
583
+         if len(sub_sv_init_flat) == num_computational_states:
584
+
585
+             if num_computational_states <= N_sub_per_rank:
586
+
587
+                 full_initial_sv_host[:num_computational_states] = sub_sv_init_flat
588
+
589
+             else:
590
+
591
+                 print(f"Error: Grid data {num_computational_states} > N_sub_per_rank {N_sub_per_rank}")
592
+
593
+                 return None, None
594
+
595
+         else:
596
+
597
+             print(f"Warning: Initial state size {len(sub_sv_init_flat)} != expected {num_computational_states}")
598
+
599
+             fill_len = min(len(sub_sv_init_flat), num_computational_states, N_sub_per_rank)
600
+
601
+             full_initial_sv_host[:fill_len] = sub_sv_init_flat[:fill_len]
602
+
603
+         
604
+
605
+         rank_slice_init = to_cupy_array(initial_state_val)
606
+
607
+         print(f'Rank {rank}: Initializing state with {distribution_type} (ux={ux_input}, uy={uy_input})...')
608
+
609
+         cp.cuda.runtime.memcpy(rank_slice_init.data.ptr, full_initial_sv_host.ctypes.data, full_initial_sv_host.nbytes, cp.cuda.runtime.memcpyHostToDevice)
610
+
611
+         print(f'Rank {rank}: Initial state copied. Size: {len(sub_sv_init_flat)}. N_sub_per_rank: {N_sub_per_rank}')
612
+
613
+
614
+
615
+         print("Starting QLBM evolution...")
616
+
617
+         qlbm_obj.run_evolution(initial_state_val, T)
618
+
619
+         print("QLBM evolution complete.")
620
+
621
+
622
+
623
+         print("Generating animation and plots with Plotly...")
624
+
625
+         downsampled_N = current_N // downsampling_factor
626
+
627
+         if downsampled_N == 0 and current_N > 0:
628
+
629
+             downsampled_N = 1
630
+
631
+         elif current_N == 0:
632
+
633
+             print("Error: current_N is zero before Plotly stage.")
634
+
635
+             return None, None
636
+
637
+
638
+
639
+         # Load data for specific time steps for static plots
640
+
641
+         time_steps = [0, T//4, 3*T//4, T]
642
+
643
+         data_frames = []
644
+
645
+         actual_timesteps = []
646
+
647
+         for t in time_steps:
648
+
649
+             file_path = intermediate_folder_path / f"{t}_{rank}.npy"
650
+
651
+             if file_path.exists():
652
+
653
+                 sol_loaded = np.load(file_path)
654
+
655
+                 if sol_loaded.size == downsampled_N * downsampled_N:
656
+
657
+                     Z_data = np.reshape(sol_loaded, (downsampled_N, downsampled_N))
658
+
659
+                     data_frames.append(Z_data)
660
+
661
+                     actual_timesteps.append(t)
662
+
663
+                 else:
664
+
665
+                     print(f"Warning: File {file_path} size {sol_loaded.size} != expected {downsampled_N*downsampled_N}. Skipping.")
666
+
667
+             else:
668
+
669
+                 print(f"Warning: File {file_path} not found. Skipping.")
670
+
671
+
672
+
673
+         if not data_frames:
674
+
675
+             print("Error: No data frames loaded for static plots.")
676
+
677
+             return None, None
678
+
679
+
680
+
681
+         x_coords_plot = np.linspace(-10, 10, downsampled_N)
682
+
683
+         y_coords_plot = np.linspace(-10, 10, downsampled_N)
684
+
685
+
686
+
687
+         # Calculate global min/max for consistent scaling
688
+
689
+         z_min = min([np.min(Z) for Z in data_frames])
690
+
691
+         z_max = max([np.max(Z) for Z in data_frames])
692
+
693
+         if z_max == z_min:
694
+
695
+             z_max += 1e-9
696
+
697
+
698
+
699
+         # Create interactive Plotly figure with slider
700
+
701
+         fig = go.Figure()
702
+
703
+
704
+
705
+         for i, Z in enumerate(data_frames):
706
+
707
+             fig.add_trace(
708
+
709
+                 go.Surface(
710
+
711
+                     z=Z, x=x_coords_plot, y=y_coords_plot,
712
+
713
+                     colorscale='Viridis',
714
+
715
+                     cmin=z_min, cmax=z_max,
716
+
717
+                     name=f'Time: {actual_timesteps[i]}',
718
+
719
+                     showscale=(i == 0)
720
+
721
+                 )
722
+
723
+             )
724
+
725
+
726
+
727
+         steps = []
728
+
729
+         for i in range(len(data_frames)):
730
+
731
+             step = dict(
732
+
733
+                 method="update",
734
+
735
+                 args=[{"visible": [False] * len(data_frames)}],
736
+
737
+                 label=f"Time: {actual_timesteps[i]}"
738
+
739
+             )
740
+
741
+             step["args"][0]["visible"][i] = True
742
+
743
+             steps.append(step)
744
+
745
+
746
+
747
+         sliders = [dict(active=0, currentvalue={"prefix": "Time: "}, pad={"t": 50}, steps=steps)]
748
+
749
+
750
+
751
+         fig.update_layout(
752
+
753
+             title='QLBM Simulation - Density Evolution',
754
+
755
+             scene=dict(
756
+
757
+                 xaxis_title='X',
758
+
759
+                 yaxis_title='Y',
760
+
761
+                 zaxis_title='Density',
762
+
763
+                 xaxis=dict(range=[x_coords_plot[0], x_coords_plot[-1]]),
764
+
765
+                 yaxis=dict(range=[y_coords_plot[0], y_coords_plot[-1]]),
766
+
767
+                 zaxis=dict(range=[z_min, z_max]),
768
+
769
+             ),
770
+
771
+             sliders=sliders,
772
+
773
+             width=800,
774
+
775
+             height=700
776
+
777
+         )
778
+
779
+
780
+
781
+         # Generate GIF (unchanged from original)
782
+
783
+         plotted_timesteps_str = sorted(list(set([str(t) for t in range(0, T + 1, timesteps_per_frame) if (intermediate_folder_path / f"{t}_{rank}.npy").exists()] + ['0'] if T == 0 else [])), key=lambda k_str: int(k_str))
784
+
785
+         data_frames_list = []
786
+
787
+         actual_timesteps_for_title = []
788
+
789
+         for i_str_val in plotted_timesteps_str:
790
+
791
+             file_path_load = intermediate_folder_path / f"{i_str_val}_{rank}.npy"
792
+
793
+             if file_path_load.exists():
794
+
795
+                 sol_loaded = np.load(file_path_load)
796
+
797
+                 if sol_loaded.size == downsampled_N * downsampled_N:
798
+
799
+                     Z_data = np.reshape(sol_loaded, (downsampled_N, downsampled_N))
800
+
801
+                     data_frames_list.append(Z_data)
802
+
803
+                     actual_timesteps_for_title.append(i_str_val)
804
+
805
+
806
+
807
+         if not data_frames_list:
808
+
809
+             print("Error: No data frames loaded for GIF.")
810
+
811
+             return None, None
812
+
813
+
814
+
815
+         norm_factor_plotly = np.max(np.abs(data_frames_list[0])) if data_frames_list[0].size > 0 else 1.0
816
+
817
+         all_data_max_val = max([np.max(np.abs(d_frame)) for d_frame in data_frames_list]) if data_frames_list else norm_factor_plotly
818
+
819
+         clim_upper_plotly = (all_data_max_val / norm_factor_plotly) * 1.0 or 0.6
820
+
821
+
822
+
823
+         results_base_dir = "Results"
824
+
825
+         gif_frames_for_naming = int(simulation_fps * T) or 1
826
+
827
+         dist_name_part = distribution_type.replace(' ','').replace('(Original)','').replace('(','').replace(')','')
828
+
829
+         specific_folder_name = f"d2q5_plotly_N{current_N}_T{T}_fr{gif_frames_for_naming}_dist{dist_name_part}_ux{ux_input:.2f}_uy{uy_input:.2f}"
830
+
831
+         final_output_dir = Path(results_base_dir) / specific_folder_name
832
+
833
+         os.makedirs(final_output_dir, exist_ok=True)
834
+
835
+         gif_path_to_return = final_output_dir / "animation_plotly.gif"
836
+
837
+
838
+
839
+         Z_initial_placeholder = np.zeros((downsampled_N, downsampled_N))
840
+
841
+         gif_fig = go.Figure(data=[go.Surface(
842
+
843
+             z=Z_initial_placeholder, x=x_coords_plot, y=y_coords_plot,
844
+
845
+             colorscale='Viridis', cmin=0, cmax=clim_upper_plotly,
846
+
847
+             colorbar=dict(title=dict(text='Density', side='right', font=dict(color='white')),
848
+
849
+                           tickfont=dict(size=10, color='white'), bgcolor='rgba(0,0,0,0.4)',
850
+
851
+                           bordercolor='gray', outlinewidth=1, thickness=20, len=0.8, x=0.85, y=0.5))])
852
+
853
+         
854
+
855
+         gif_fig.update_layout(
856
+
857
+             paper_bgcolor='black', plot_bgcolor='black', font=dict(color='white'),
858
+
859
+             scene=dict(
860
+
861
+                 bgcolor='black',
862
+
863
+                 xaxis=dict(title=dict(text='X Axis', font=dict(color='white')), tickfont=dict(color='white'),
864
+
865
+                            gridcolor='rgba(128,128,128,0.3)', zerolinecolor='rgba(128,128,128,0.5)', linecolor='rgba(128,128,128,0.5)'),
866
+
867
+                 yaxis=dict(title=dict(text='Y Axis', font=dict(color='white')), tickfont=dict(color='white'),
868
+
869
+                            gridcolor='rgba(128,128,128,0.3)', zerolinecolor='rgba(128,128,128,0.5)', linecolor='rgba(128,128,128,0.5)'),
870
+
871
+                 zaxis=dict(title=dict(text='Density', font=dict(color='white')), tickfont=dict(color='white'),
872
+
873
+                            range=[0, clim_upper_plotly], gridcolor='rgba(128,128,128,0.3)',
874
+
875
+                            zerolinecolor='rgba(128,128,128,0.5)', linecolor='rgba(128,128,128,0.5)'),
876
+
877
+                 aspectratio=dict(x=1, y=1, z=0.7),
878
+
879
+                 camera=dict(eye=dict(x=1.5, y=1.5, z=1.0), center=dict(x=0, y=0, z=-0.1), up=dict(x=0,y=0,z=1))
880
+
881
+             ),
882
+
883
+             margin=dict(l=10, r=10, b=10, t=60), width=800, height=700
884
+
885
+         )
886
+
887
+
888
+
889
+         temp_image_files = []
890
+
891
+         gif_fps_val = 10
892
+
893
+
894
+
895
+         for k, Z_frame_data in enumerate(data_frames_list):
896
+
897
+             Z_plotly_frame = Z_frame_data / norm_factor_plotly
898
+
899
+             gif_fig.data[0].z = Z_plotly_frame
900
+
901
+             current_ts_title = actual_timesteps_for_title[k] if k < len(actual_timesteps_for_title) else "Unknown"
902
+
903
+             gif_fig.update_layout(
904
+
905
+                 title_text=f'QLBM Simulation (Plotly) - Timestep: {current_ts_title}',
906
+
907
+                 title_x=0.5, title_font_size=16
908
+
909
+             )
910
+
911
+             temp_image_path = intermediate_folder_path / f"plotly_frame_{k:04d}.png"
912
+
913
+             gif_fig.write_image(str(temp_image_path), engine="kaleido")
914
+
915
+             temp_image_files.append(str(temp_image_path))
916
+
917
+
918
+
919
+         if temp_image_files:
920
+
921
+             with imageio.get_writer(str(gif_path_to_return), mode='I', fps=gif_fps_val, loop=0) as writer:
922
+
923
+                 for filename in temp_image_files:
924
+
925
+                     image = imageio.imread(filename)
926
+
927
+                     writer.append_data(image)
928
+
929
+             print(f"Plotly animation saved to {gif_path_to_return}")
930
+
931
+         else:
932
+
933
+             print("Error: No Plotly frames generated for GIF.")
934
+
935
+             return None, None
936
+
937
+
938
+
939
+         return str(gif_path_to_return), fig
940
+
941
+
942
+
943
+ # Gradio Interface Definition
944
+
945
+ def qlbm_gradio_interface(num_reg_qubits_input: int, timescale_input: int, distribution_type_param: str, ux_param: float, uy_param: float):
946
+
947
+     num_reg_qubits_val = int(num_reg_qubits_input)
948
+
949
+     timescale_val = int(timescale_input)
950
+
951
+     ux_val = float(ux_param)
952
+
953
+     uy_val = float(uy_param)
954
+
955
+
956
+
957
+     print(f"Gradio Interface: num_reg_qubits={num_reg_qubits_val}, T={timescale_val}, Distribution={distribution_type_param}, ux={ux_val}, uy={uy_val}")
958
+
959
+
960
+
961
+     gif_path, plot_fig = simulate_qlbm_and_animate(
962
+
963
+         num_reg_qubits=num_reg_qubits_val,
964
+
965
+         T=timescale_val,
966
+
967
+         distribution_type=distribution_type_param,
968
+
969
+         ux_input=ux_val,
970
+
971
+         uy_input=uy_val
972
+
973
+     )
974
+
975
+
976
+
977
+     if gif_path is None or plot_fig is None:
978
+
979
+         gr.Warning("Simulation or plotting failed. Please check console for errors.")
980
+
981
+         return None, None
982
+
983
+     return gif_path, plot_fig
984
+
985
+
986
+
987
+ with gr.Blocks(theme=gr.themes.Soft(), title="QLBM Simulation with Plotly") as qlbm_demo:
988
+
989
+     gr.Markdown(
990
+
991
+     """
992
+
993
+     # ⚛️ Quantum Lattice Boltzmann Method (QLBM) Simulator (Plotly Animation)
994
+
995
+     Welcome to the Quantum Lattice Boltzmann Method (QLBM) simulator! This version uses Plotly for 3D animation and interactive plots.
996
+
997
+
998
+
999
+     **How this Simulator Works:**
1000
+
1001
+     This simulator implements a D2Q5 model on a quantum computer simulator (CUDA-Q).
1002
+
1003
+     - Control grid size (via Number of Register Qubits: $N=2^{\text{num_reg_qubits}}$).
1004
+
1005
+     - Set total simulation time (Timescale T).
1006
+
1007
+     - Choose initial distribution.
1008
+
1009
+     - Set advection velocities `ux` and `uy`.
1010
+
1011
+     The simulation generates a GIF animation and an interactive Plotly figure with a slider for selected time steps.
1012
+
1013
+
1014
+
1015
+     **Note:** Higher qubit counts and longer timescales are computationally intensive. Advection velocities should be small (e.g., < 0.3).
1016
+
1017
+     The output GIF loops indefinitely, and the Plotly figure allows interactive exploration of specific time steps.
1018
+
1019
+     """
1020
+
1021
+     )
1022
+
1023
+     with gr.Row():
1024
+
1025
+         with gr.Column(scale=1):
1026
+
1027
+             gr.Markdown("## Simulation Parameters")
1028
+
1029
+             num_reg_qubits_slider = gr.Slider(
1030
+
1031
+                 minimum=2, maximum=10, value=8, step=1,
1032
+
1033
+                 label="Number of Register Qubits (num_reg_qubits)",
1034
+
1035
+                 info="Grid N = 2^num_reg_qubits. Max 10 (Note: >8 slow; >9 may hit simulator/memory limits on free tiers)."
1036
+
1037
+             )
1038
+
1039
+             timescale_slider = gr.Slider(
1040
+
1041
+                 minimum=0, maximum=2000, value=100, step=10,
1042
+
1043
+                 label="Timescale (T)", info="Total number of timesteps. Max 2000."
1044
+
1045
+             )
1046
+
1047
+             distribution_options = ["Sine Wave (Original)", "Gaussian", "Random"]
1048
+
1049
+             distribution_type_input = gr.Radio(
1050
+
1051
+                 choices=distribution_options, value="Sine Wave (Original)",
1052
+
1053
+                 label="Initial Distribution Type", info="Select the initial pattern of the substance."
1054
+
1055
+             )
1056
+
1057
+             ux_slider = gr.Slider(
1058
+
1059
+                 minimum=-0.4, maximum=0.4, value=0.2, step=0.01,
1060
+
1061
+                 label="Advection Velocity ux", info="x-component of background advection."
1062
+
1063
+             )
1064
+
1065
+             uy_slider = gr.Slider(
1066
+
1067
+               �� minimum=-0.4, maximum=0.4, value=0.15, step=0.01,
1068
+
1069
+                 label="Advection Velocity uy", info="y-component of background advection."
1070
+
1071
+             )
1072
+
1073
+             run_qlbm_btn = gr.Button("Run QLBM Simulation", variant="primary")
1074
+
1075
+
1076
+
1077
+         with gr.Column(scale=2):
1078
+
1079
+             qlbm_plot_output = gr.Image(label="QLBM Simulation Animation (GIF)", type="filepath", height=600)
1080
+
1081
+             qlbm_interactive_plot = gr.Plot(label="Interactive Density Plot with Slider")
1082
+
1083
+
1084
+
1085
+     qlbm_inputs_list = [num_reg_qubits_slider, timescale_slider, distribution_type_input, ux_slider, uy_slider]
1086
+
1087
+     run_qlbm_btn.click(
1088
+
1089
+         fn=qlbm_gradio_interface,
1090
+
1091
+         inputs=qlbm_inputs_list,
1092
+
1093
+         outputs=[qlbm_plot_output, qlbm_interactive_plot]
1094
+
1095
+     )
1096
+
1097
+     gr.Examples(
1098
+
1099
+         examples=[
1100
+
1101
+             [8, 100, "Sine Wave (Original)", 0.2, 0.15],
1102
+
1103
+             [6, 50, "Gaussian", 0.1, 0.05],
1104
+
1105
+             [4, 30, "Random", -0.05, 0.1],
1106
+
1107
+         ],
1108
+
1109
+         inputs=qlbm_inputs_list,
1110
+
1111
+         outputs=[qlbm_plot_output, qlbm_interactive_plot],
1112
+
1113
+         fn=qlbm_gradio_interface,
1114
+
1115
+         cache_examples=False
1116
+
1117
+     )
1118
+
1119
+
1120
+
1121
+ if __name__ == "__main__":
1122
+
1123
+     try:
1124
+
1125
+         cudaq.set_target('nvidia', option='fp64')
1126
+
1127
+         print(f"CUDA-Q Target successfully set to: {cudaq.get_target().name}")
1128
+
1129
+     except Exception as e_target:
1130
+
1131
+         print(f"Warning: Could not set CUDA-Q target to 'nvidia'. Error: {e_target}")
1132
+
1133
+         print(f"Current CUDA-Q Target: {cudaq.get_target().name}. Performance may be affected.")
1134
+
1135
+     
1136
+
1137
+     qlbm_demo.launch()