harishaseebat92 commited on
Commit
7bf5c9d
·
1 Parent(s): b816b11

Initial upload of source code

Browse files
Files changed (9) hide show
  1. README.md +8 -9
  2. app.py +603 -0
  3. app_o.py +568 -0
  4. app_pyvista.py +666 -0
  5. index.html +19 -0
  6. new-working-branch +0 -0
  7. packages.txt +4 -0
  8. requirements.txt +11 -0
  9. style.css +28 -0
README.md CHANGED
@@ -1,14 +1,13 @@
1
  ---
2
- title: AlphaQuantDemo V2
3
- emoji: 🐠
4
- colorFrom: pink
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.34.0
8
- app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: Duplicate of Original alphaQuantDemo
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: AlphaQuantDemo
3
+ emoji: 👀
4
+ colorFrom: red
5
+ colorTo: gray
6
  sdk: gradio
 
 
7
  pinned: false
8
+ license: apache-2.0
9
+ short_description: testing platforms to build a quantum computing demo
10
+ sdk_version: 5.32.0
11
  ---
12
 
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import tempfile
4
+ import gradio as gr
5
+
6
+ import cudaq
7
+ import numpy as np
8
+ import cupy as cp
9
+
10
+ from pathlib import Path
11
+
12
+ # --- Plotly Imports ---
13
+ import plotly.graph_objects as go
14
+ import plotly.io as pio
15
+ import imageio # For compiling GIF from frames
16
+
17
+ # Set a default Plotly engine for image export
18
+ try:
19
+ pio.kaleido.scope.mathjax = None # Can speed up Kaleido if MathJax not needed
20
+ except AttributeError:
21
+ pass
22
+
23
+
24
+ def simulate_qlbm_and_animate(num_reg_qubits: int, T: int, distribution_type: str, ux_input: float, uy_input: float):
25
+ """
26
+ Simulates a 2D advection-diffusion problem using a Quantum Lattice Boltzmann Method (QLBM)
27
+ and generates a GIF animation of the simulation's evolution using Plotly.
28
+ """
29
+
30
+ video_length = T
31
+ simulation_fps = 0.1
32
+ frames = int(simulation_fps * video_length)
33
+ if frames == 0:
34
+ frames = 1
35
+
36
+ num_anc = 3
37
+ num_qubits_total = 2 * num_reg_qubits + num_anc
38
+ current_N = 2**num_reg_qubits
39
+ N_tot_state_vector = 2**num_qubits_total
40
+ num_ranks = 1
41
+ rank = 0
42
+ N_sub_per_rank = int(N_tot_state_vector // num_ranks)
43
+
44
+ timesteps_per_frame = 1
45
+ if frames < T and frames > 0:
46
+ timesteps_per_frame = int(T / frames)
47
+ if timesteps_per_frame == 0:
48
+ timesteps_per_frame = 1
49
+
50
+ if distribution_type == "Sine Wave (Original)":
51
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
52
+ np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
53
+ np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
54
+ elif distribution_type == "Gaussian":
55
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
56
+ np.exp(-((x - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2) +
57
+ (y - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2))) * 1.8 + 0.2
58
+ elif distribution_type == "Random":
59
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
60
+ np.random.rand(N_val_func, N_val_func) * 1.5 + 0.2 if isinstance(x, int) else \
61
+ np.random.rand(x.shape[0], x.shape[1]) * 1.5 + 0.2
62
+ else:
63
+ print(f"Warning: Unknown distribution type '{distribution_type}'. Defaulting to Sine Wave.")
64
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
65
+ np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
66
+ np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
67
+
68
+ initial_state_func_eval = lambda x_coords, y_coords: \
69
+ selected_initial_state_function_raw(x_coords, y_coords, current_N) * \
70
+ (y_coords < current_N).astype(int)
71
+
72
+ with tempfile.TemporaryDirectory() as tmp_npy_dir:
73
+ intermediate_folder_path = Path(tmp_npy_dir)
74
+
75
+ cudaq.set_target('nvidia', option='fp64')
76
+
77
+ @cudaq.kernel
78
+ def alloc_kernel(num_qubits_alloc: int):
79
+ qubits = cudaq.qvector(num_qubits_alloc)
80
+
81
+ from cupy.cuda.memory import MemoryPointer, UnownedMemory
82
+
83
+ def to_cupy_array(state):
84
+ tensor = state.getTensor()
85
+ pDevice = tensor.data()
86
+ sizeByte = tensor.get_num_elements() * tensor.get_element_size()
87
+ mem = UnownedMemory(pDevice, sizeByte, owner=state)
88
+ memptr_obj = MemoryPointer(mem, 0)
89
+ cupy_array_val = cp.ndarray(tensor.get_num_elements(),
90
+ dtype=cp.complex128,
91
+ memptr=memptr_obj)
92
+ return cupy_array_val
93
+
94
+ class QLBMAdvecDiffD2Q5_new:
95
+ def __init__(self, ux=0.2, uy=0.15) -> None:
96
+ self.dim = 2
97
+ self.ndir = 5
98
+ self.nq_dir = math.ceil(np.log2(self.ndir))
99
+ self.dirs = []
100
+ for dir_int in range(self.ndir):
101
+ dir_bin = f"{dir_int:b}".zfill(self.nq_dir)
102
+ self.dirs.append(dir_bin)
103
+ self.e_unitvec = np.array([0, 1, -1, 1, -1])
104
+ self.wts = np.array([2/6, 1/6, 1/6, 1/6, 1/6])
105
+ self.cs = 1 / np.sqrt(3)
106
+ self.ux = ux
107
+ self.uy = uy
108
+ self.u = np.array([0, self.ux, self.ux, self.uy, self.uy])
109
+ self.wtcoeffs = np.multiply(self.wts, 1 + self.e_unitvec * self.u / self.cs**2)
110
+ self.create_circuit()
111
+
112
+ def create_circuit(self):
113
+ v = np.pad(self.wtcoeffs, (0, 2**num_anc - self.ndir))
114
+ v = v**0.5
115
+ v[0] += 1
116
+ v = v / np.linalg.norm(v)
117
+ U_prep = 2 * np.outer(v, v) - np.eye(len(v))
118
+ cudaq.register_operation("prep_op", U_prep)
119
+
120
+ def collisionOp(dirs_list):
121
+ dirs_i_list_val = []
122
+ for dir_str in dirs_list:
123
+ dirs_i = [(int(c)) for c in dir_str]
124
+ dirs_i_list_val += dirs_i[::-1]
125
+ return dirs_i_list_val
126
+
127
+ self.dirs_i_list = collisionOp(self.dirs)
128
+
129
+ @cudaq.kernel
130
+ def rshift(q: cudaq.qview, n: int):
131
+ for i in range(n):
132
+ if i == n - 1:
133
+ x(q[n - 1 - i])
134
+ elif i == n - 2:
135
+ x.ctrl(q[n - 1 - (i + 1)], q[n - 1 - i])
136
+ else:
137
+ x.ctrl(q[0:n - 1 - i], q[n - 1 - i])
138
+
139
+ @cudaq.kernel
140
+ def lshift(q: cudaq.qview, n: int):
141
+ for i in range(n):
142
+ if i == 0:
143
+ x(q[0])
144
+ elif i == 1:
145
+ x.ctrl(q[0], q[1])
146
+ else:
147
+ x.ctrl(q[0:i], q[i])
148
+
149
+ @cudaq.kernel
150
+ def d2q5_tstep(q: cudaq.qview, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
151
+ qx = q[0:nqx]
152
+ qy = q[nqx:nqx + nqy]
153
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
154
+
155
+ idx_lqx = 2
156
+ b_list = dirs_i_val[idx_lqx * nq_dir_val:(idx_lqx + 1) * nq_dir_val]
157
+ for j in range(nq_dir_val):
158
+ if b_list[j] == 0: x(qdir[j])
159
+ cudaq.control(lshift, qdir, qx, nqx)
160
+ for j in range(nq_dir_val):
161
+ if b_list[j] == 0: x(qdir[j])
162
+
163
+ idx_rqx = 1
164
+ b_list = dirs_i_val[idx_rqx * nq_dir_val:(idx_rqx + 1) * nq_dir_val]
165
+ for j in range(nq_dir_val):
166
+ if b_list[j] == 0: x(qdir[j])
167
+ cudaq.control(rshift, qdir, qx, nqx)
168
+ for j in range(nq_dir_val):
169
+ if b_list[j] == 0: x(qdir[j])
170
+
171
+ idx_lqy = 4
172
+ b_list = dirs_i_val[idx_lqy * nq_dir_val:(idx_lqy + 1) * nq_dir_val]
173
+ for j in range(nq_dir_val):
174
+ if b_list[j] == 0: x(qdir[j])
175
+ cudaq.control(lshift, qdir, qy, nqy)
176
+ for j in range(nq_dir_val):
177
+ if b_list[j] == 0: x(qdir[j])
178
+
179
+ idx_rqy = 3
180
+ b_list = dirs_i_val[idx_rqy * nq_dir_val:(idx_rqy + 1) * nq_dir_val]
181
+ for j in range(nq_dir_val):
182
+ if b_list[j] == 0: x(qdir[j])
183
+ cudaq.control(rshift, qdir, qy, nqy)
184
+ for j in range(nq_dir_val):
185
+ if b_list[j] == 0: x(qdir[j])
186
+
187
+ @cudaq.kernel
188
+ def d2q5_tstep_wrapper(state_arg: cudaq.State, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
189
+ q = cudaq.qvector(state_arg)
190
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
191
+ prep_op(qdir[2], qdir[1], qdir[0]) # Assumes nq_dir_val is always 3
192
+ d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
193
+ prep_op(qdir[2], qdir[1], qdir[0])
194
+
195
+ @cudaq.kernel
196
+ def d2q5_tstep_wrapper_hadamard(vec_arg: list[complex], nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]):
197
+ q = cudaq.qvector(vec_arg)
198
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
199
+ qy = q[nqx:nqx + nqy]
200
+ prep_op(qdir[2], qdir[1], qdir[0])
201
+ d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
202
+ prep_op(qdir[2], qdir[1], qdir[0])
203
+ for i in range(nqy):
204
+ h(qy[i])
205
+
206
+ # MODIFIED run_timestep_func for memory optimization
207
+ def run_timestep_func(vec_arg, hadamard=False):
208
+ if hadamard:
209
+ result = cudaq.get_state(d2q5_tstep_wrapper_hadamard, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
210
+ else:
211
+ result = cudaq.get_state(d2q5_tstep_wrapper, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
212
+
213
+ num_nonzero_ranks = num_ranks / (2**num_anc)
214
+ rank_slice_cupy = to_cupy_array(result) # This is a cupy.ndarray view
215
+
216
+ if rank >= num_nonzero_ranks and num_nonzero_ranks > 0:
217
+ # This part of the original code implies multi-GPU/rank scenarios not fully active with num_ranks=1
218
+ # For num_ranks=1, rank is 0, so this block isn't hit if num_nonzero_ranks > 0.
219
+ # If num_nonzero_ranks is effectively 0 (e.g. 1/(2^3) < 1, which it is), this condition for rank=0 is also false.
220
+ # The original logic: if rank >= num_nonzero_ranks: ...
221
+ # If num_ranks=1, num_anc=3 -> num_nonzero_ranks = 1/8. rank=0 is not >= 1/8.
222
+ # This block seems intended for ranks that should contain no data.
223
+ # For now, keeping original logic path, but the optimization is for the other 'if' block.
224
+ sub_sv_zeros = np.zeros(N_sub_per_rank, dtype=np.complex128)
225
+ cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_zeros.ctypes.data, sub_sv_zeros.nbytes, cp.cuda.runtime.memcpyHostToDevice)
226
+
227
+ # OPTIMIZED BLOCK: Modify directly on GPU if conditions are met
228
+ if rank == 0 and num_nonzero_ranks < 1 and N_sub_per_rank > 0:
229
+ # num_nonzero_ranks = 1/8 < 1, so this condition is met for rank 0
230
+ limit_idx = int(N_tot_state_vector / (2**num_anc))
231
+ if limit_idx < rank_slice_cupy.size:
232
+ rank_slice_cupy[limit_idx:] = 0 # Modify directly on GPU using CuPy
233
+ # No explicit memcpy back to GPU needed as rank_slice_cupy is the GPU array view.
234
+ return result
235
+ self.run_timestep = run_timestep_func
236
+ # END OF MODIFIED run_timestep_func
237
+
238
+ def write_state(self, state_to_write, t_step_str_val):
239
+ rank_slice_cupy = to_cupy_array(state_to_write)
240
+ num_nonzero_ranks = num_ranks / (2**num_anc)
241
+
242
+ if rank < num_nonzero_ranks or (rank==0 and num_nonzero_ranks <=0):
243
+ save_path = intermediate_folder_path / f"{t_step_str_val}_{rank}.npy"
244
+ with open(save_path, 'wb') as f:
245
+ arr_to_save = None
246
+ data_limit = N_sub_per_rank
247
+ if num_nonzero_ranks < 1 and rank == 0: # Only part of rank 0 has data
248
+ data_limit = int(N_tot_state_vector / (2**num_anc))
249
+
250
+ if data_limit > 0:
251
+ relevant_part_cupy = cp.real(rank_slice_cupy[:data_limit])
252
+ else:
253
+ relevant_part_cupy = cp.array([], dtype=cp.float64)
254
+
255
+ if relevant_part_cupy.size >= current_N * current_N: # Enough data for full grid
256
+ arr_flat = relevant_part_cupy[:current_N * current_N]
257
+ # CORRECTED CONDITION: Removed reference to downsampled_N
258
+ if downsampling_factor > 1 and current_N > 0:
259
+ arr_reshaped = arr_flat.reshape((current_N, current_N))
260
+ arr_downsampled = arr_reshaped[::downsampling_factor, ::downsampling_factor]
261
+ arr_to_save = arr_downsampled.flatten()
262
+ else: # No downsampling or conditions not met for it
263
+ arr_to_save = arr_flat
264
+ elif relevant_part_cupy.size > 0 : # Partial data
265
+ if downsampling_factor > 1:
266
+ arr_to_save = relevant_part_cupy[::downsampling_factor]
267
+ else:
268
+ arr_to_save = relevant_part_cupy
269
+
270
+ if arr_to_save is not None and arr_to_save.size > 0:
271
+ np.save(f, arr_to_save.get() if isinstance(arr_to_save, cp.ndarray) else arr_to_save)
272
+
273
+ def run_evolution(self, initial_state_arg, total_timesteps, observable=False):
274
+ current_state_val = initial_state_arg
275
+ for t_iter in range(total_timesteps):
276
+ next_state_val = None
277
+ if t_iter == total_timesteps - 1 and observable:
278
+ next_state_val = self.run_timestep(current_state_val, True)
279
+ self.write_state(next_state_val, str(t_iter + 1) + "_h")
280
+ else:
281
+ next_state_val = self.run_timestep(current_state_val)
282
+ if (t_iter + 1) % timesteps_per_frame == 0:
283
+ self.write_state(next_state_val, str(t_iter + 1))
284
+ if rank == 0:
285
+ print(f"Timestep: {t_iter + 1}/{total_timesteps}")
286
+
287
+ cp.get_default_memory_pool().free_all_blocks()
288
+ current_state_val = next_state_val
289
+
290
+ last_saved_timestep_name = str(total_timesteps) if not observable else str(total_timesteps) + "_h"
291
+ final_state_file_path = intermediate_folder_path / f"{last_saved_timestep_name}_{rank}.npy"
292
+ final_T_state_file_path = intermediate_folder_path / f"{total_timesteps}_{rank}.npy"
293
+
294
+ needs_final_save = True
295
+ if observable and final_state_file_path.exists():
296
+ needs_final_save = False
297
+ elif not observable and final_T_state_file_path.exists() and total_timesteps > 0 and total_timesteps % timesteps_per_frame == 0 :
298
+ needs_final_save = False
299
+ elif not observable and final_T_state_file_path.exists():
300
+ needs_final_save = False
301
+
302
+ if needs_final_save and total_timesteps > 0 :
303
+ if not observable:
304
+ self.write_state(current_state_val, str(total_timesteps))
305
+ elif total_timesteps == 0 and current_state_val is not None:
306
+ self.write_state(current_state_val, "0")
307
+
308
+ if rank == 0:
309
+ print(f"Timestep: {total_timesteps}/{total_timesteps} (Evolution complete)")
310
+ cp.get_default_memory_pool().free_all_blocks()
311
+ self.final_state = current_state_val
312
+ # --- END OF QLBM CLASS ---
313
+
314
+ downsampling_factor = 2**5
315
+ if current_N == 0:
316
+ print("Error: current_N is zero. num_reg_qubits likely too small.")
317
+ return None
318
+ if current_N < downsampling_factor:
319
+ downsampling_factor = current_N if current_N > 0 else 1
320
+
321
+ qlbm_obj = QLBMAdvecDiffD2Q5_new(ux=ux_input, uy=uy_input)
322
+ initial_state_val = cudaq.get_state(alloc_kernel, num_qubits_total)
323
+
324
+ xv_init = np.arange(current_N)
325
+ yv_init = np.arange(current_N)
326
+ initial_grid_2d_X, initial_grid_2d_Y = np.meshgrid(xv_init, yv_init)
327
+
328
+ if distribution_type == "Random":
329
+ initial_grid_2d = selected_initial_state_function_raw(current_N, current_N, current_N) # Pass N for shape
330
+ else:
331
+ initial_grid_2d = initial_state_func_eval(initial_grid_2d_X, initial_grid_2d_Y)
332
+
333
+ sub_sv_init_flat = initial_grid_2d.flatten().astype(np.complex128)
334
+ full_initial_sv_host = np.zeros(N_sub_per_rank, dtype=np.complex128)
335
+ num_computational_states = current_N * current_N
336
+
337
+ if len(sub_sv_init_flat) == num_computational_states:
338
+ if num_computational_states <= N_sub_per_rank:
339
+ full_initial_sv_host[:num_computational_states] = sub_sv_init_flat
340
+ else:
341
+ print(f"Error: Grid data {num_computational_states} > N_sub_per_rank {N_sub_per_rank}")
342
+ return None
343
+ else:
344
+ print(f"Warning: Initial state size {len(sub_sv_init_flat)} != expected {num_computational_states}")
345
+ fill_len = min(len(sub_sv_init_flat), num_computational_states, N_sub_per_rank)
346
+ full_initial_sv_host[:fill_len] = sub_sv_init_flat[:fill_len]
347
+
348
+ rank_slice_init = to_cupy_array(initial_state_val)
349
+ print(f'Rank {rank}: Initializing state with {distribution_type} (ux={ux_input}, uy={uy_input})...')
350
+ cp.cuda.runtime.memcpy(rank_slice_init.data.ptr, full_initial_sv_host.ctypes.data, full_initial_sv_host.nbytes, cp.cuda.runtime.memcpyHostToDevice)
351
+ print(f'Rank {rank}: Initial state copied. Size: {len(sub_sv_init_flat)}. N_sub_per_rank: {N_sub_per_rank}')
352
+
353
+ print("Starting QLBM evolution...")
354
+ qlbm_obj.run_evolution(initial_state_val, T)
355
+ print("QLBM evolution complete.")
356
+
357
+ print("Generating animation with Plotly...")
358
+ downsampled_N = current_N // downsampling_factor
359
+ if downsampled_N == 0 and current_N > 0:
360
+ downsampled_N = 1
361
+ elif current_N == 0:
362
+ print("Error: current_N is zero before Plotly stage.")
363
+ return None
364
+
365
+ plotted_timesteps_str = []
366
+ if T == 0:
367
+ if (intermediate_folder_path / f"0_{rank}.npy").exists():
368
+ plotted_timesteps_str.append("0")
369
+ else:
370
+ if timesteps_per_frame > 0:
371
+ for t_step_val in range(timesteps_per_frame, T + 1, timesteps_per_frame):
372
+ if intermediate_folder_path.joinpath(f"{t_step_val}_{rank}.npy").exists():
373
+ plotted_timesteps_str.append(str(t_step_val))
374
+ final_T_path = intermediate_folder_path.joinpath(f"{T}_{rank}.npy")
375
+ if final_T_path.exists() and str(T) not in plotted_timesteps_str:
376
+ plotted_timesteps_str.append(str(T))
377
+
378
+ plotted_timesteps_str = sorted(list(set(plotted_timesteps_str)), key=lambda k_str: int(k_str))
379
+
380
+ if not plotted_timesteps_str :
381
+ print(f"Warning: No .npy files found for plotting T={T}. Animation may fail.")
382
+ return None
383
+
384
+ data_frames_list = []
385
+ actual_timesteps_for_title = []
386
+ for i_str_val in plotted_timesteps_str:
387
+ try:
388
+ file_path_load = intermediate_folder_path / f"{i_str_val}_{rank}.npy"
389
+ sol_loaded = np.load(file_path_load)
390
+ if sol_loaded.size == downsampled_N * downsampled_N:
391
+ Z_data = np.reshape(sol_loaded, (downsampled_N, downsampled_N))
392
+ data_frames_list.append(Z_data)
393
+ actual_timesteps_for_title.append(i_str_val)
394
+ else:
395
+ print(f"Warning: File {file_path_load} size {sol_loaded.size} != expected {downsampled_N*downsampled_N}. Skipping.")
396
+ continue
397
+ except FileNotFoundError:
398
+ print(f"Warning: File {file_path_load} not found. Skipping.")
399
+ continue
400
+ except Exception as e_load:
401
+ print(f"Error loading/reshaping {file_path_load}: {e_load}. Skipping.")
402
+ continue
403
+
404
+ if not data_frames_list:
405
+ print("Error: No data frames loaded. Cannot create animation.")
406
+ return None
407
+
408
+ x_coords_plot = np.linspace(-10, 10, downsampled_N)
409
+ y_coords_plot = np.linspace(-10, 10, downsampled_N)
410
+
411
+ norm_factor_plotly = np.max(np.abs(data_frames_list[0])) if data_frames_list[0].size > 0 and np.max(np.abs(data_frames_list[0])) != 0 else 1.0
412
+ if norm_factor_plotly == 0: norm_factor_plotly = 1.0
413
+
414
+ all_data_max_val = norm_factor_plotly
415
+ if len(data_frames_list) > 0: # Should always be true if we reached here
416
+ max_vals_frames = [np.max(np.abs(d_frame)) for d_frame in data_frames_list if d_frame.size > 0]
417
+ if max_vals_frames:
418
+ global_max_val_frames = max(max_vals_frames)
419
+ if global_max_val_frames > 0 : all_data_max_val = global_max_val_frames
420
+
421
+ clim_upper_plotly = (all_data_max_val / norm_factor_plotly) * 1.0
422
+ if clim_upper_plotly == 0 : clim_upper_plotly = 0.6
423
+
424
+ results_base_dir = "Results"
425
+ gif_frames_for_naming = int(simulation_fps * T)
426
+ if gif_frames_for_naming == 0: gif_frames_for_naming = 1
427
+ dist_name_part = distribution_type.replace(' ','').replace('(Original)','').replace('(','').replace(')','')
428
+ specific_folder_name = (f"d2q5_plotly_N{current_N}_T{T}_fr{gif_frames_for_naming}_"
429
+ f"dist{dist_name_part}_ux{ux_input:.2f}_uy{uy_input:.2f}")
430
+ final_output_dir = Path(results_base_dir) / specific_folder_name
431
+ os.makedirs(final_output_dir, exist_ok=True)
432
+ gif_path_to_return = final_output_dir / "animation_plotly.gif"
433
+
434
+ Z_initial_placeholder = np.zeros((downsampled_N, downsampled_N))
435
+ fig = go.Figure(data=[go.Surface(
436
+ z=Z_initial_placeholder, x=x_coords_plot, y=y_coords_plot,
437
+ colorscale='Viridis', cmin=0, cmax=clim_upper_plotly,
438
+ colorbar=dict(
439
+ title=dict(text='Density', side='right', font=dict(color='white')),
440
+ tickfont=dict(size=10, color='white'),
441
+ bgcolor='rgba(0,0,0,0.4)', bordercolor='gray', outlinewidth=1,
442
+ thickness=20, len=0.8, x=0.85, y=0.5
443
+ ))])
444
+
445
+ fig.update_layout(
446
+ paper_bgcolor='black', plot_bgcolor='black', font=dict(color='white'),
447
+ scene=dict(
448
+ bgcolor='black',
449
+ xaxis=dict(title=dict(text='X Axis', font=dict(color='white')),
450
+ tickfont=dict(color='white'),
451
+ gridcolor='rgba(128,128,128,0.3)', zerolinecolor='rgba(128,128,128,0.5)',
452
+ linecolor='rgba(128,128,128,0.5)'),
453
+ yaxis=dict(title=dict(text='Y Axis', font=dict(color='white')),
454
+ tickfont=dict(color='white'),
455
+ gridcolor='rgba(128,128,128,0.3)', zerolinecolor='rgba(128,128,128,0.5)',
456
+ linecolor='rgba(128,128,128,0.5)'),
457
+ zaxis=dict(title=dict(text='Density', font=dict(color='white')),
458
+ tickfont=dict(color='white'),
459
+ range=[0, clim_upper_plotly], gridcolor='rgba(128,128,128,0.3)',
460
+ zerolinecolor='rgba(128,128,128,0.5)', linecolor='rgba(128,128,128,0.5)'),
461
+ aspectratio=dict(x=1, y=1, z=0.7),
462
+ 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))
463
+ ),
464
+ margin=dict(l=10, r=10, b=10, t=60), width=800, height=700
465
+ )
466
+
467
+ temp_image_files = []
468
+ gif_fps_val = 10
469
+
470
+ try:
471
+ for k, Z_frame_data in enumerate(data_frames_list):
472
+ Z_plotly_frame = Z_frame_data / norm_factor_plotly
473
+ fig.data[0].z = Z_plotly_frame
474
+ current_ts_title = actual_timesteps_for_title[k] if k < len(actual_timesteps_for_title) else "Unknown"
475
+ fig.update_layout(
476
+ title_text=f'QLBM Simulation (Plotly) - Timestep: {current_ts_title}',
477
+ title_x=0.5, title_font_size=16
478
+ )
479
+ temp_image_path = intermediate_folder_path / f"plotly_frame_{k:04d}.png"
480
+ fig.write_image(str(temp_image_path), engine="kaleido")
481
+ temp_image_files.append(str(temp_image_path))
482
+
483
+ if not temp_image_files:
484
+ print("Error: No Plotly frames were successfully generated.")
485
+ # Cleanup potentially created empty directory
486
+ if not any(final_output_dir.iterdir()): # Check if directory is empty
487
+ try:
488
+ os.rmdir(final_output_dir)
489
+ except OSError:
490
+ pass # Ignore if it fails (e.g. not empty for other reasons)
491
+ return None
492
+
493
+ with imageio.get_writer(str(gif_path_to_return), mode='I', fps=gif_fps_val, loop=0) as writer:
494
+ for filename in temp_image_files:
495
+ image = imageio.imread(filename)
496
+ writer.append_data(image)
497
+
498
+ print(f"Plotly animation saved to {gif_path_to_return}")
499
+
500
+ except Exception as e_plotly_anim:
501
+ print(f"Error during Plotly animation/saving: {e_plotly_anim}")
502
+ print("Ensure 'kaleido' and 'imageio' are installed ('pip install kaleido imageio').")
503
+ return None
504
+
505
+ return str(gif_path_to_return)
506
+
507
+ # --- Gradio Interface Definition ---
508
+ def qlbm_gradio_interface(num_reg_qubits_input: int, timescale_input: int, distribution_type_param: str, ux_param: float, uy_param: float):
509
+ num_reg_qubits_val = int(num_reg_qubits_input)
510
+ timescale_val = int(timescale_input)
511
+ ux_val = float(ux_param)
512
+ uy_val = float(uy_param)
513
+
514
+ print(f"Gradio Interface: num_reg_qubits={num_reg_qubits_val}, T={timescale_val}, Distribution={distribution_type_param}, ux={ux_val}, uy={uy_val}")
515
+
516
+ gif_path_output = simulate_qlbm_and_animate(
517
+ num_reg_qubits=num_reg_qubits_val,
518
+ T=timescale_val,
519
+ distribution_type=distribution_type_param,
520
+ ux_input=ux_val,
521
+ uy_input=uy_val
522
+ )
523
+
524
+ if gif_path_output is None:
525
+ gr.Warning("Animation generation failed. Please check console for errors.")
526
+ return None
527
+ return gif_path_output
528
+
529
+ with gr.Blocks(theme=gr.themes.Soft(), title="QLBM Simulation with Plotly") as qlbm_demo:
530
+ gr.Markdown(
531
+ """
532
+ # ⚛️ Quantum Lattice Boltzmann Method (QLBM) Simulator (Plotly Animation)
533
+ Welcome to the Quantum Lattice Boltzmann Method (QLBM) simulator! This version uses Plotly for 3D animation.
534
+
535
+ **How this Simulator Works:**
536
+ This simulator implements a D2Q5 model on a quantum computer simulator (CUDA-Q).
537
+ - Control grid size (via Number of Register Qubits: $N=2^{\text{num_reg_qubits}}$).
538
+ - Set total simulation time (Timescale T).
539
+ - Choose initial distribution.
540
+ - Set advection velocities `ux` and `uy`.
541
+ The simulation evolves this state, and a Plotly animation of the density is generated as a GIF.
542
+
543
+ **Note:** Higher qubit counts and longer timescales are computationally intensive. Advection velocities should be small (e.g., < 0.3).
544
+ The output GIF will loop indefinitely. Values for "Number of Register Qubits" above 9 may be unstable or very slow on shared/limited hardware.
545
+ """
546
+ )
547
+ with gr.Row():
548
+ with gr.Column(scale=1):
549
+ gr.Markdown("## Simulation Parameters")
550
+ num_reg_qubits_slider = gr.Slider(
551
+ minimum=2, maximum=10, value=8, step=1, # Max set to 10
552
+ label="Number of Register Qubits (num_reg_qubits)",
553
+ info="Grid N = 2^num_reg_qubits. Max 10 (Note: >8 slow; >9 may hit simulator/memory limits on free tiers)."
554
+ )
555
+ timescale_slider = gr.Slider(
556
+ minimum=0, maximum=2000, value=100, step=10,
557
+ label="Timescale (T)", info="Total number of timesteps. Max 2000."
558
+ )
559
+ distribution_options = ["Sine Wave (Original)", "Gaussian", "Random"]
560
+ distribution_type_input = gr.Radio(
561
+ choices=distribution_options, value="Sine Wave (Original)",
562
+ label="Initial Distribution Type", info="Select the initial pattern of the substance."
563
+ )
564
+ ux_slider = gr.Slider(
565
+ minimum=-0.4, maximum=0.4, value=0.2, step=0.01,
566
+ label="Advection Velocity ux", info="x-component of background advection."
567
+ )
568
+ uy_slider = gr.Slider(
569
+ minimum=-0.4, maximum=0.4, value=0.15, step=0.01,
570
+ label="Advection Velocity uy", info="y-component of background advection."
571
+ )
572
+ run_qlbm_btn = gr.Button("Run QLBM Simulation (Plotly)", variant="primary")
573
+
574
+ with gr.Column(scale=2):
575
+ qlbm_plot_output = gr.Image(label="QLBM Simulation Animation (Plotly GIF)", type="filepath", height=600)
576
+
577
+ qlbm_inputs_list = [num_reg_qubits_slider, timescale_slider, distribution_type_input, ux_slider, uy_slider]
578
+ run_qlbm_btn.click(
579
+ fn=qlbm_gradio_interface,
580
+ inputs=qlbm_inputs_list,
581
+ outputs=qlbm_plot_output
582
+ )
583
+ gr.Examples(
584
+ examples=[
585
+ [8, 100, "Sine Wave (Original)", 0.2, 0.15],
586
+ [6, 50, "Gaussian", 0.1, 0.05],
587
+ [4, 30, "Random", -0.05, 0.1],
588
+ ],
589
+ inputs=qlbm_inputs_list,
590
+ outputs=qlbm_plot_output,
591
+ fn=qlbm_gradio_interface,
592
+ cache_examples=False
593
+ )
594
+
595
+ if __name__ == "__main__":
596
+ try:
597
+ cudaq.set_target('nvidia', option='fp64')
598
+ print(f"CUDA-Q Target successfully set to: {cudaq.get_target().name}")
599
+ except Exception as e_target:
600
+ print(f"Warning: Could not set CUDA-Q target to 'nvidia'. Error: {e_target}")
601
+ print(f"Current CUDA-Q Target: {cudaq.get_target().name}. Performance may be affected.")
602
+
603
+ qlbm_demo.launch()
app_o.py ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import math
5
+ import threading
6
+ import tempfile
7
+ import gradio as gr
8
+
9
+ import cudaq
10
+ import numpy as np
11
+ import cupy as cp
12
+ # from scipy import interpolate # Not used
13
+
14
+ from pathlib import Path
15
+
16
+ from matplotlib import pyplot as plt
17
+ from matplotlib.animation import FuncAnimation
18
+
19
+ import matplotlib
20
+ matplotlib.use('Agg')
21
+ import matplotlib.pyplot as plt
22
+
23
+ def simulate_qlbm_and_animate(num_reg_qubits: int, T: int, distribution_type: str, ux_input: float, uy_input: float):
24
+ """
25
+ Simulates a 2D advection-diffusion problem using a Quantum Lattice Boltzmann Method (QLBM)
26
+ and generates a GIF animation of the simulation's evolution.
27
+
28
+ Args:
29
+ num_reg_qubits (int): The number of register qubits (determines grid size N=2^num_reg_qubits).
30
+ T (int): The total number of timesteps to run the simulation.
31
+ distribution_type (str): The type of initial distribution to use.
32
+ ux_input (float): Advection velocity in the x-direction.
33
+ uy_input (float): Advection velocity in the y-direction.
34
+
35
+ Returns:
36
+ str: The file path to the generated GIF animation, or None if an error occurs.
37
+ """
38
+
39
+ video_length = T
40
+ simulation_fps = 0.1
41
+ frames = int(simulation_fps * video_length)
42
+ if frames == 0:
43
+ frames = 1
44
+
45
+ num_anc = 3
46
+ num_qubits_total = 2 * num_reg_qubits + num_anc
47
+ current_N = 2**num_reg_qubits
48
+ N_tot_state_vector = 2**num_qubits_total
49
+ num_ranks = 1
50
+ rank = 0
51
+ N_sub_per_rank = int(N_tot_state_vector // num_ranks)
52
+
53
+ timesteps_per_frame = 1
54
+ if frames < T and frames > 0:
55
+ timesteps_per_frame = int(T / frames)
56
+ if timesteps_per_frame == 0:
57
+ timesteps_per_frame = 1
58
+
59
+ if distribution_type == "Sine Wave (Original)":
60
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
61
+ np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
62
+ np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
63
+ elif distribution_type == "Gaussian":
64
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
65
+ np.exp(-((x - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2) +
66
+ (y - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2))) * 1.8 + 0.2
67
+ elif distribution_type == "Random":
68
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
69
+ np.random.rand(x.shape[0], x.shape[1]) * 1.5 + 0.2
70
+ else:
71
+ print(f"Warning: Unknown distribution type '{distribution_type}'. Defaulting to Sine Wave.")
72
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
73
+ np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
74
+ np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
75
+
76
+ initial_state_func_eval = lambda x_coords, y_coords: \
77
+ selected_initial_state_function_raw(x_coords, y_coords, current_N) * \
78
+ (y_coords < current_N).astype(int)
79
+
80
+
81
+ with tempfile.TemporaryDirectory() as tmp_npy_dir:
82
+ intermediate_folder_path = Path(tmp_npy_dir)
83
+
84
+ cudaq.set_target('nvidia', option='fp64')
85
+
86
+ @cudaq.kernel
87
+ def alloc_kernel(num_qubits_alloc: int):
88
+ qubits = cudaq.qvector(num_qubits_alloc)
89
+
90
+ from cupy.cuda.memory import MemoryPointer, UnownedMemory
91
+
92
+ def to_cupy_array(state):
93
+ tensor = state.getTensor()
94
+ pDevice = tensor.data()
95
+ sizeByte = tensor.get_num_elements() * tensor.get_element_size()
96
+ mem = UnownedMemory(pDevice, sizeByte, owner=state)
97
+ memptr_obj = MemoryPointer(mem, 0)
98
+ cupy_array = cp.ndarray(tensor.get_num_elements(),
99
+ dtype=cp.complex128,
100
+ memptr=memptr_obj)
101
+ return cupy_array
102
+
103
+ class QLBMAdvecDiffD2Q5_new:
104
+ def __init__(self, ux=0.2, uy=0.15) -> None: # ux, uy are now passed here
105
+ self.dim = 2
106
+ self.ndir = 5
107
+ self.nq_dir = math.ceil(np.log2(self.ndir))
108
+ self.dirs = []
109
+ for dir_int in range(self.ndir):
110
+ dir_bin = f"{dir_int:b}".zfill(self.nq_dir)
111
+ self.dirs.append(dir_bin)
112
+ self.e_unitvec = np.array([0, 1, -1, 1, -1])
113
+ self.wts = np.array([2/6, 1/6, 1/6, 1/6, 1/6])
114
+ self.cs = 1 / np.sqrt(3)
115
+ self.ux = ux # Use passed ux
116
+ self.uy = uy # Use passed uy
117
+ self.u = np.array([0, self.ux, self.ux, self.uy, self.uy])
118
+ self.wtcoeffs = np.multiply(self.wts, 1 + self.e_unitvec * self.u / self.cs**2)
119
+ self.create_circuit()
120
+
121
+ def create_circuit(self):
122
+ v = np.pad(self.wtcoeffs, (0, 2**num_anc - self.ndir))
123
+ v = v**0.5
124
+ v[0] += 1
125
+ v = v / np.linalg.norm(v)
126
+ U_prep = 2 * np.outer(v, v) - np.eye(len(v))
127
+ cudaq.register_operation("prep_op", U_prep)
128
+
129
+ def collisionOp(dirs):
130
+ dirs_i_list = []
131
+ for dir_ in dirs:
132
+ dirs_i = [(int(c)) for c in dir_]
133
+ dirs_i_list += dirs_i[::-1]
134
+ return dirs_i_list
135
+
136
+ self.dirs_i_list = collisionOp(self.dirs)
137
+
138
+ @cudaq.kernel
139
+ def rshift(q: cudaq.qview, n: int):
140
+ for i in range(n):
141
+ if i == n - 1:
142
+ x(q[n - 1 - i])
143
+ elif i == n - 2:
144
+ x.ctrl(q[n - 1 - (i + 1)], q[n - 1 - i])
145
+ else:
146
+ x.ctrl(q[0:n - 1 - i], q[n - 1 - i])
147
+
148
+ @cudaq.kernel
149
+ def lshift(q: cudaq.qview, n: int):
150
+ for i in range(n):
151
+ if i == 0:
152
+ x(q[0])
153
+ elif i == 1:
154
+ x.ctrl(q[0], q[1])
155
+ else:
156
+ x.ctrl(q[0:i], q[i])
157
+
158
+ @cudaq.kernel
159
+ def d2q5_tstep(q: cudaq.qview, nqx: int, nqy: int, nq_dir: int, dirs_i: list[int]):
160
+ qx = q[0:nqx]
161
+ qy = q[nqx:nqx + nqy]
162
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir]
163
+ i = 2
164
+ b_list = dirs_i[i * nq_dir:(i + 1) * nq_dir]
165
+ for j in range(nq_dir):
166
+ b = b_list[j]
167
+ if b == 0:
168
+ x(qdir[j])
169
+ cudaq.control(lshift, qdir, qx, nqx)
170
+ for j in range(nq_dir):
171
+ b = b_list[j]
172
+ if b == 0:
173
+ x(qdir[j])
174
+ i = 1
175
+ b_list = dirs_i[i * nq_dir:(i + 1) * nq_dir]
176
+ for j in range(nq_dir):
177
+ b = b_list[j]
178
+ if b == 0:
179
+ x(qdir[j])
180
+ cudaq.control(rshift, qdir, qx, nqx)
181
+ for j in range(nq_dir):
182
+ b = b_list[j]
183
+ if b == 0:
184
+ x(qdir[j])
185
+ i = 4
186
+ b_list = dirs_i[i * nq_dir:(i + 1) * nq_dir]
187
+ for j in range(nq_dir):
188
+ b = b_list[j]
189
+ if b == 0:
190
+ x(qdir[j])
191
+ cudaq.control(lshift, qdir, qy, nqy)
192
+ for j in range(nq_dir):
193
+ b = b_list[j]
194
+ if b == 0:
195
+ x(qdir[j])
196
+ i = 3
197
+ b_list = dirs_i[i * nq_dir:(i + 1) * nq_dir]
198
+ for j in range(nq_dir):
199
+ b = b_list[j]
200
+ if b == 0:
201
+ x(qdir[j])
202
+ cudaq.control(rshift, qdir, qy, nqy)
203
+ for j in range(nq_dir):
204
+ b = b_list[j]
205
+ if b == 0:
206
+ x(qdir[j])
207
+
208
+ @cudaq.kernel
209
+ def d2q5_tstep_wrapper(state_arg: cudaq.State, nqx: int, nqy: int, nq_dir: int, dirs_i: list[int]):
210
+ q = cudaq.qvector(state_arg)
211
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir]
212
+ prep_op(qdir[2], qdir[1], qdir[0])
213
+ d2q5_tstep(q, nqx, nqy, nq_dir, dirs_i)
214
+ prep_op(qdir[2], qdir[1], qdir[0])
215
+
216
+ @cudaq.kernel
217
+ def d2q5_tstep_wrapper_hadamard(vec: list[complex], nqx: int, nqy: int, nq_dir: int, dirs_i: list[int]):
218
+ q = cudaq.qvector(vec)
219
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir]
220
+ qy = q[nqx:nqx + nqy]
221
+ prep_op(qdir[2], qdir[1], qdir[0])
222
+ d2q5_tstep(q, nqx, nqy, nq_dir, dirs_i)
223
+ prep_op(qdir[2], qdir[1], qdir[0])
224
+ for i in range(nqy):
225
+ h(qy[i])
226
+
227
+ def run_timestep_func(vec, hadamard=False):
228
+ if hadamard:
229
+ result = cudaq.get_state(d2q5_tstep_wrapper_hadamard, vec, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
230
+ else:
231
+ result = cudaq.get_state(d2q5_tstep_wrapper, vec, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
232
+
233
+ num_nonzero_ranks = num_ranks / (2**num_anc)
234
+ rank_slice_cupy = to_cupy_array(result)
235
+
236
+ if rank >= num_nonzero_ranks:
237
+ sub_sv_zeros = np.zeros(N_sub_per_rank, dtype=np.complex128)
238
+ cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_zeros.ctypes.data, sub_sv_zeros.nbytes, cp.cuda.runtime.memcpyHostToDevice)
239
+
240
+ if rank == 0 and num_nonzero_ranks < 1:
241
+ sub_sv_get = (rank_slice_cupy).get()
242
+ sub_sv_get[int(N_tot_state_vector / (2**num_anc)):] = 0
243
+ cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_get.ctypes.data, sub_sv_get.nbytes, cp.cuda.runtime.memcpyHostToDevice)
244
+ return result
245
+
246
+ self.run_timestep = run_timestep_func
247
+
248
+ def write_state(self, state_to_write, t_step):
249
+ rank_slice_cupy = to_cupy_array(state_to_write)
250
+ num_nonzero_ranks = num_ranks / (2**num_anc)
251
+
252
+ if rank < num_nonzero_ranks:
253
+ save_path = intermediate_folder_path / f"{t_step}_{rank}.npy"
254
+ with open(save_path, 'wb') as f:
255
+ arr_to_save = None
256
+ if num_nonzero_ranks < 1:
257
+ arr_to_save = cp.real(rank_slice_cupy)[:int(N_tot_state_vector / (2**num_anc))]
258
+ else:
259
+ arr_to_save = cp.real(rank_slice_cupy)
260
+
261
+ if len(arr_to_save) > current_N :
262
+ arr_to_save=arr_to_save.reshape((current_N, current_N))
263
+ arr_to_save=arr_to_save[::downsampling_factor,::downsampling_factor]
264
+ arr_to_save=arr_to_save.flatten()
265
+ elif len(arr_to_save) > 0 :
266
+ arr_to_save=arr_to_save[::downsampling_factor]
267
+
268
+ np.save(f, arr_to_save.get() if isinstance(arr_to_save, cp.ndarray) else arr_to_save)
269
+
270
+
271
+ def run_evolution(self, initial_state_arg, total_timesteps, observable=False):
272
+ current_state = initial_state_arg
273
+ for t_iter in range(total_timesteps):
274
+ next_state = None
275
+ if t_iter == total_timesteps - 1 and observable:
276
+ next_state = self.run_timestep(current_state, True)
277
+ self.write_state(next_state, str(t_iter + 1) + "_h")
278
+ else:
279
+ next_state = self.run_timestep(current_state)
280
+ if (t_iter + 1) % timesteps_per_frame == 0:
281
+ self.write_state(next_state, t_iter + 1)
282
+ if rank == 0:
283
+ print(f"Timestep: {t_iter + 1}/{total_timesteps}")
284
+
285
+ cp.get_default_memory_pool().free_all_blocks()
286
+ current_state = next_state
287
+
288
+ last_saved_timestep_name = str(total_timesteps) if not observable else str(total_timesteps) + "_h"
289
+ # Check if the very last state (T or T_h) was potentially saved by the loop or observable condition.
290
+ # If not, save it.
291
+ final_state_file_path = intermediate_folder_path.joinpath(f"{last_saved_timestep_name}_{rank}.npy")
292
+ # Also check if the non-observable final T state was saved if T aligns with timesteps_per_frame
293
+ final_T_state_file_path = intermediate_folder_path.joinpath(f"{total_timesteps}_{rank}.npy")
294
+
295
+ needs_final_save = True
296
+ if observable and final_state_file_path.exists(): # T_h was saved
297
+ needs_final_save = False
298
+ elif not observable and final_T_state_file_path.exists() and total_timesteps % timesteps_per_frame == 0 : # T was saved by loop
299
+ needs_final_save = False
300
+ elif not observable and final_T_state_file_path.exists(): # T was saved by a previous final write
301
+ needs_final_save = False
302
+
303
+
304
+ if needs_final_save and total_timesteps > 0:
305
+ if not observable: # Save as T_0.npy
306
+ self.write_state(current_state, total_timesteps)
307
+ # If observable, it should have been saved as T_h. If it wasn't (e.g. T=0), this won't run.
308
+
309
+ if rank == 0:
310
+ print(f"Timestep: {total_timesteps}/{total_timesteps} (Evolution complete)")
311
+ cp.get_default_memory_pool().free_all_blocks()
312
+ self.final_state = current_state
313
+
314
+ downsampling_factor = 2**5
315
+ # Pass ux_input and uy_input to the constructor
316
+ qlbm_obj = QLBMAdvecDiffD2Q5_new(ux=ux_input, uy=uy_input)
317
+
318
+ initial_state = cudaq.get_state(alloc_kernel, num_qubits_total)
319
+ rank_slice_init = to_cupy_array(initial_state)
320
+
321
+ xv_init = np.arange(current_N)
322
+ yv_init = np.arange(current_N)
323
+
324
+ print(f'Start initializing state with {distribution_type} distribution (ux={ux_input}, uy={uy_input})...')
325
+ initial_grid_2d = initial_state_func_eval(*np.meshgrid(xv_init, yv_init))
326
+ sub_sv_init_flat = initial_grid_2d.flatten().astype(np.complex128)
327
+
328
+ full_initial_sv_host = np.zeros(N_sub_per_rank, dtype=np.complex128)
329
+ num_computational_states = current_N * current_N
330
+
331
+ if len(sub_sv_init_flat) == num_computational_states:
332
+ full_initial_sv_host[:num_computational_states] = sub_sv_init_flat
333
+ else:
334
+ print(f"Warning: Mismatch in expected initial state size {num_computational_states} and calculated {len(sub_sv_init_flat)}")
335
+ full_initial_sv_host[:len(sub_sv_init_flat)] = sub_sv_init_flat
336
+
337
+ print(f'Rank {rank}: Initial state (N*N part) prepared. Size: {len(sub_sv_init_flat)}. Total N_sub_per_rank: {N_sub_per_rank}')
338
+ cp.cuda.runtime.memcpy(rank_slice_init.data.ptr, full_initial_sv_host.ctypes.data, full_initial_sv_host.nbytes, cp.cuda.runtime.memcpyHostToDevice)
339
+ print(f'Rank {rank}: Initial state copied to device.')
340
+
341
+ print("Starting QLBM evolution...")
342
+ qlbm_obj.run_evolution(initial_state, T)
343
+ print("QLBM evolution complete.")
344
+
345
+ print("Generating animation...")
346
+ downsampled_N = current_N // downsampling_factor
347
+ if downsampled_N == 0:
348
+ print("Error: Downsampled grid size is zero. Check num_reg_qubits and downsampling_factor.")
349
+ downsampled_N = 1
350
+
351
+ plotted_timesteps_str = [] # To store frame names (can include '_h')
352
+ if timesteps_per_frame > 0:
353
+ # Add regular frames saved by the loop
354
+ for t_step in range(timesteps_per_frame, T + 1, timesteps_per_frame):
355
+ if intermediate_folder_path.joinpath(f"{t_step}_0.npy").exists():
356
+ plotted_timesteps_str.append(str(t_step))
357
+
358
+ # Ensure the very last timestep (T or T_h) is considered for plotting
359
+ final_T_path = intermediate_folder_path.joinpath(f"{T}_0.npy")
360
+ final_Th_path = intermediate_folder_path.joinpath(f"{T}_h_0.npy")
361
+
362
+ if T > 0:
363
+ if final_Th_path.exists(): # Prefer T_h if it exists
364
+ if str(T)+"_h" not in plotted_timesteps_str:
365
+ plotted_timesteps_str.append(str(T)+"_h")
366
+ elif final_T_path.exists(): # Else, use T if it exists
367
+ if str(T) not in plotted_timesteps_str:
368
+ plotted_timesteps_str.append(str(T))
369
+
370
+ # Remove duplicates and sort (handle numeric and '_h' strings appropriately for sorting if needed, though string sort is often fine here)
371
+ plotted_timesteps_str = sorted(list(set(plotted_timesteps_str)), key=lambda k: (int(str(k).replace('_h','')), str(k)))
372
+
373
+
374
+ if not plotted_timesteps_str and T > 0 : # Fallback if T is small and not caught by loop
375
+ if final_Th_path.exists(): plotted_timesteps_str = [str(T)+"_h"]
376
+ elif final_T_path.exists(): plotted_timesteps_str = [str(T)]
377
+ elif not plotted_timesteps_str and T == 0:
378
+ print("Warning: T=0, no frames to plot for animation.")
379
+ return None
380
+
381
+ data = []
382
+ actual_timesteps_for_title = []
383
+
384
+ for i_str in plotted_timesteps_str:
385
+ try:
386
+ file_path = intermediate_folder_path / f"{i_str}_0.npy"
387
+ sol = np.load(file_path)
388
+ if sol.size == downsampled_N * downsampled_N:
389
+ Z = np.reshape(sol, (downsampled_N, downsampled_N))
390
+ data.append(Z)
391
+ actual_timesteps_for_title.append(str(i_str).replace('_h',''))
392
+ else:
393
+ print(f"Warning: File {file_path} has unexpected size {sol.size}. Expected {downsampled_N*downsampled_N}. Skipping this frame.")
394
+ continue
395
+ except FileNotFoundError:
396
+ print(f"Warning: File not found for timestep {i_str} ({file_path}). Skipping this frame.")
397
+ continue
398
+ except Exception as e:
399
+ print(f"Error loading or reshaping file for timestep {i_str}: {e}. Skipping this frame.")
400
+ continue
401
+
402
+ if not data:
403
+ print("Error: No data available to create animation. Check .npy file generation and paths.")
404
+ return None
405
+
406
+ fig = plt.figure(figsize=(10, 8))
407
+ ax = fig.add_subplot(111, projection='3d')
408
+
409
+ x_plot = np.linspace(-10, 10, downsampled_N)
410
+ y_plot = np.linspace(-10, 10, downsampled_N)
411
+ X_plot, Y_plot = np.meshgrid(x_plot, y_plot)
412
+
413
+ norm_factor = np.linalg.norm(data[0]) if data[0].size > 0 else 1.0
414
+ if norm_factor == 0:
415
+ norm_factor = 1.0
416
+
417
+ def update_frame(frame_idx):
418
+ ax.clear()
419
+ Z = data[frame_idx]
420
+ surf = ax.plot_surface(X_plot, Y_plot, Z / norm_factor, cmap='viridis', linewidth=0, antialiased=False)
421
+ current_timestep_for_title = actual_timesteps_for_title[frame_idx] if frame_idx < len(actual_timesteps_for_title) else "Unknown"
422
+ ax.set_title(f'Quantum Simulation Evolution (Timestep: {current_timestep_for_title})')
423
+ ax.set_xlabel('x')
424
+ ax.set_ylabel('y')
425
+ ax.set_zlim(0, 0.6)
426
+ return surf,
427
+
428
+ gif_animation_fps = 10
429
+ ani = FuncAnimation(fig, update_frame, frames=len(data), blit=False)
430
+
431
+ results_base_dir = "Results"
432
+ gif_frames_for_naming = int(simulation_fps * T)
433
+ if gif_frames_for_naming == 0: gif_frames_for_naming = 1
434
+
435
+ # Sanitize distribution_type for filename
436
+ dist_name_part = distribution_type.replace(' ','').replace('(Original)','').replace('(','').replace(')','')
437
+
438
+ specific_folder_name = (f"d2q5_nq{current_N}x{current_N}_T{T}_fr{gif_frames_for_naming}_"
439
+ f"dist{dist_name_part}_ux{ux_input:.2f}_uy{uy_input:.2f}")
440
+ final_output_dir = Path(results_base_dir) / specific_folder_name
441
+ os.makedirs(final_output_dir, exist_ok=True)
442
+ gif_path_to_return = final_output_dir / "animation.gif"
443
+
444
+ try:
445
+ ani.save(str(gif_path_to_return), writer='pillow', fps=gif_animation_fps)
446
+ print(f"Animation saved to {gif_path_to_return}")
447
+ except Exception as e:
448
+ print(f"Error saving animation: {e}")
449
+ plt.close(fig)
450
+ return None
451
+ plt.close(fig)
452
+ return str(gif_path_to_return)
453
+
454
+ # --- Gradio Interface Definition ---
455
+
456
+ def qlbm_gradio_interface(num_reg_qubits_input: int, timescale_input: int, distribution_type_param: str, ux_param: float, uy_param: float):
457
+ num_reg_qubits_val = int(num_reg_qubits_input)
458
+ timescale_val = int(timescale_input)
459
+ ux_val = float(ux_param)
460
+ uy_val = float(uy_param)
461
+
462
+ print(f"Gradio Interface: num_reg_qubits={num_reg_qubits_val}, T={timescale_val}, Distribution={distribution_type_param}, ux={ux_val}, uy={uy_val}")
463
+
464
+ gif_path_output = simulate_qlbm_and_animate(
465
+ num_reg_qubits=num_reg_qubits_val,
466
+ T=timescale_val,
467
+ distribution_type=distribution_type_param,
468
+ ux_input=ux_val,
469
+ uy_input=uy_val
470
+ )
471
+
472
+ if gif_path_output is None:
473
+ gr.Warning("Animation generation failed. Please check console for errors.")
474
+ return None
475
+ return gif_path_output
476
+
477
+ with gr.Blocks(theme=gr.themes.Soft(), title="QLBM Simulation") as qlbm_demo:
478
+ gr.Markdown(
479
+ """
480
+ # ⚛️ Quantum Lattice Boltzmann Method (QLBM) Simulator
481
+ Welcome to the Quantum Lattice Boltzmann Method (QLBM) simulator!
482
+
483
+ **What is QLBM?**
484
+ The Lattice Boltzmann Method (LBM) is a computational fluid dynamics technique for simulating fluid flow. QLBM is its quantum counterpart, leveraging quantum algorithms to potentially offer advantages for certain types of simulations. It's used here to model advection-diffusion phenomena, which describe how substances are transported due to bulk motion (advection) and spread out due to random motion (diffusion).
485
+
486
+ **How this Simulator Works:**
487
+ This simulator implements a D2Q5 model (2 dimensions, 5 velocity directions) on a quantum computer simulator (using CUDA-Q).
488
+ - You can control the grid size (via Number of Register Qubits, where grid size is $2^N \\times 2^N$).
489
+ - You can set the total simulation time (Timescale T).
490
+ - You can choose the initial distribution of the substance on the grid.
491
+ - You can set the advection velocities `ux` (x-direction) and `uy` (y-direction).
492
+ The simulation will then evolve this initial state over time, and an animation of the substance's density will be generated.
493
+
494
+ **Note:** Simulations with higher qubit counts and longer timescales can be computationally intensive and may take a significant amount of time to complete.
495
+ The $N$ in $2^N \\times 2^N$ grid refers to `num_reg_qubits`. Advection velocities `ux` and `uy` should generally be kept small (e.g., < 0.3) for model stability.
496
+ """
497
+ )
498
+
499
+ with gr.Row():
500
+ with gr.Column(scale=1):
501
+ gr.Markdown("## Simulation Parameters")
502
+ num_reg_qubits_slider = gr.Slider(
503
+ minimum=8,
504
+ maximum=30,
505
+ value=8,
506
+ step=1,
507
+ label="Number of Register Qubits (num_reg_qubits)",
508
+ info="Determines the N for grid size (2^N x 2^N). Max 30 (Note: >8 can be very slow)."
509
+ )
510
+ timescale_slider = gr.Slider(
511
+ minimum=10,
512
+ maximum=10000,
513
+ value=100,
514
+ step=100,
515
+ label="Timestep (T)",
516
+ info="Total number of timesteps. Max 2000 (Note: higher values increase run time significantly)."
517
+ )
518
+ distribution_options = ["Sine Wave (Original)", "Gaussian", "Random"]
519
+ distribution_type_input = gr.Radio(
520
+ choices=distribution_options,
521
+ value="Sine Wave (Original)",
522
+ label="Initial Distribution Type",
523
+ info="Select the initial pattern of the substance on the grid."
524
+ )
525
+ ux_slider = gr.Slider(
526
+ minimum=-0.4,
527
+ maximum=0.4,
528
+ value=0.2,
529
+ step=0.01,
530
+ label="Advection Velocity ux",
531
+ info="x-component of the background advection velocity."
532
+ )
533
+ uy_slider = gr.Slider(
534
+ minimum=-0.4,
535
+ maximum=0.4,
536
+ value=0.15,
537
+ step=0.01,
538
+ label="Advection Velocity uy",
539
+ info="y-component of the background advection velocity."
540
+ )
541
+
542
+ run_qlbm_btn = gr.Button("Run QLBM Simulation", variant="primary")
543
+
544
+ with gr.Column(scale=3):
545
+ qlbm_plot_output = gr.Image(label="QLBM Simulation Animation", type="filepath")
546
+
547
+ qlbm_inputs_list = [num_reg_qubits_slider, timescale_slider, distribution_type_input, ux_slider, uy_slider]
548
+
549
+ run_qlbm_btn.click(
550
+ fn=qlbm_gradio_interface,
551
+ inputs=qlbm_inputs_list,
552
+ outputs=qlbm_plot_output
553
+ )
554
+
555
+ gr.Examples(
556
+ examples=[
557
+ [8, 100, "Sine Wave (Original)", 0.2, 0.15],
558
+ [6, 50, "Gaussian", 0.1, 0.05],
559
+ [4, 30, "Random", -0.05, 0.1],
560
+ ],
561
+ inputs=qlbm_inputs_list,
562
+ outputs=qlbm_plot_output,
563
+ fn=qlbm_gradio_interface,
564
+ cache_examples=False
565
+ )
566
+
567
+ if __name__ == "__main__":
568
+ qlbm_demo.launch()
app_pyvista.py ADDED
@@ -0,0 +1,666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys # Not explicitly used in the core logic, but often good for path manipulations
3
+ # import time # Not explicitly used in the core logic after modifications
4
+ import math
5
+ # import threading # Not explicitly used in the core logic
6
+ import tempfile
7
+ import gradio as gr
8
+
9
+ import cudaq
10
+ import numpy as np
11
+ import cupy as cp
12
+ # from scipy import interpolate # Not used
13
+
14
+ from pathlib import Path
15
+
16
+ # --- PyVista Import ---
17
+ import pyvista as pv
18
+ # Set a global theme for PyVista if desired (optional, can be set per plotter too)
19
+ pv.global_theme.anti_aliasing = 'fxaa' # Smooths plot edges
20
+ pv.global_theme.lighting = True # Enable lighting for better 3D effect
21
+
22
+ # --- Removed Matplotlib imports ---
23
+ # from matplotlib import pyplot as plt
24
+ # from matplotlib.animation import FuncAnimation
25
+ # import matplotlib
26
+ # matplotlib.use('Agg') # Not needed for PyVista off-screen plotting
27
+ # import matplotlib.pyplot as plt # Duplicate, also removed
28
+
29
+ def simulate_qlbm_and_animate(num_reg_qubits: int, T: int, distribution_type: str, ux_input: float, uy_input: float):
30
+ """
31
+ Simulates a 2D advection-diffusion problem using a Quantum Lattice Boltzmann Method (QLBM)
32
+ and generates a GIF animation of the simulation's evolution using PyVista.
33
+
34
+ Args:
35
+ num_reg_qubits (int): The number of register qubits (determines grid size N=2^num_reg_qubits).
36
+ T (int): The total number of timesteps to run the simulation.
37
+ distribution_type (str): The type of initial distribution to use.
38
+ ux_input (float): Advection velocity in the x-direction.
39
+ uy_input (float): Advection velocity in the y-direction.
40
+
41
+ Returns:
42
+ str: The file path to the generated GIF animation, or None if an error occurs.
43
+ """
44
+
45
+ video_length = T
46
+ simulation_fps = 0.1
47
+ frames = int(simulation_fps * video_length)
48
+ if frames == 0:
49
+ frames = 1
50
+
51
+ num_anc = 3
52
+ num_qubits_total = 2 * num_reg_qubits + num_anc
53
+ current_N = 2**num_reg_qubits
54
+ N_tot_state_vector = 2**num_qubits_total
55
+ num_ranks = 1
56
+ rank = 0
57
+ N_sub_per_rank = int(N_tot_state_vector // num_ranks)
58
+
59
+ timesteps_per_frame = 1
60
+ if frames < T and frames > 0:
61
+ timesteps_per_frame = int(T / frames)
62
+ if timesteps_per_frame == 0:
63
+ timesteps_per_frame = 1
64
+
65
+ # Initial state selection (as per the provided snippet)
66
+ if distribution_type == "Sine Wave (Original)":
67
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
68
+ np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
69
+ np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
70
+ elif distribution_type == "Gaussian":
71
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
72
+ np.exp(-((x - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2) +
73
+ (y - N_val_func / 2)**2 / (2 * (N_val_func / 5)**2))) * 1.8 + 0.2
74
+ elif distribution_type == "Random":
75
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
76
+ np.random.rand(x.shape[0], x.shape[1]) * 1.5 + 0.2
77
+ else:
78
+ print(f"Warning: Unknown distribution type '{distribution_type}'. Defaulting to Sine Wave.")
79
+ selected_initial_state_function_raw = lambda x, y, N_val_func: \
80
+ np.sin(x * 2 * np.pi / N_val_func) * (1 - 0.5 * x / N_val_func) * \
81
+ np.sin(y * 4 * np.pi / N_val_func) * (1 - 0.5 * y / N_val_func) + 1
82
+
83
+ initial_state_func_eval = lambda x_coords, y_coords: \
84
+ selected_initial_state_function_raw(x_coords, y_coords, current_N) * \
85
+ (y_coords < current_N).astype(int)
86
+
87
+
88
+ with tempfile.TemporaryDirectory() as tmp_npy_dir:
89
+ intermediate_folder_path = Path(tmp_npy_dir)
90
+
91
+ cudaq.set_target('nvidia', option='fp64') # As per user's snippet
92
+
93
+ @cudaq.kernel
94
+ def alloc_kernel(num_qubits_alloc: int):
95
+ qubits = cudaq.qvector(num_qubits_alloc)
96
+
97
+ from cupy.cuda.memory import MemoryPointer, UnownedMemory
98
+
99
+ def to_cupy_array(state):
100
+ tensor = state.getTensor()
101
+ pDevice = tensor.data()
102
+ sizeByte = tensor.get_num_elements() * tensor.get_element_size()
103
+ mem = UnownedMemory(pDevice, sizeByte, owner=state)
104
+ memptr_obj = MemoryPointer(mem, 0)
105
+ cupy_array_val = cp.ndarray(tensor.get_num_elements(), # Renamed cupy_array
106
+ dtype=cp.complex128,
107
+ memptr=memptr_obj)
108
+ return cupy_array_val
109
+
110
+ class QLBMAdvecDiffD2Q5_new:
111
+ def __init__(self, ux=0.2, uy=0.15) -> None: # ux, uy are now passed here
112
+ self.dim = 2
113
+ self.ndir = 5
114
+ self.nq_dir = math.ceil(np.log2(self.ndir))
115
+ self.dirs = []
116
+ for dir_int in range(self.ndir):
117
+ dir_bin = f"{dir_int:b}".zfill(self.nq_dir)
118
+ self.dirs.append(dir_bin)
119
+ self.e_unitvec = np.array([0, 1, -1, 1, -1])
120
+ self.wts = np.array([2/6, 1/6, 1/6, 1/6, 1/6])
121
+ self.cs = 1 / np.sqrt(3)
122
+ self.ux = ux # Use passed ux
123
+ self.uy = uy # Use passed uy
124
+ self.u = np.array([0, self.ux, self.ux, self.uy, self.uy])
125
+ self.wtcoeffs = np.multiply(self.wts, 1 + self.e_unitvec * self.u / self.cs**2)
126
+ self.create_circuit()
127
+
128
+ def create_circuit(self):
129
+ v = np.pad(self.wtcoeffs, (0, 2**num_anc - self.ndir))
130
+ v = v**0.5
131
+ v[0] += 1 # This specific logic from original snippet
132
+ v = v / np.linalg.norm(v)
133
+ U_prep = 2 * np.outer(v, v) - np.eye(len(v))
134
+ cudaq.register_operation("prep_op", U_prep)
135
+
136
+ def collisionOp(dirs_list): # Renamed dirs
137
+ dirs_i_list_val = [] # Renamed dirs_i_list
138
+ for dir_str in dirs_list: # Renamed dir_
139
+ dirs_i = [(int(c)) for c in dir_str]
140
+ dirs_i_list_val += dirs_i[::-1]
141
+ return dirs_i_list_val
142
+
143
+ self.dirs_i_list = collisionOp(self.dirs)
144
+
145
+ @cudaq.kernel
146
+ def rshift(q: cudaq.qview, n: int):
147
+ for i in range(n):
148
+ if i == n - 1:
149
+ x(q[n - 1 - i])
150
+ elif i == n - 2:
151
+ x.ctrl(q[n - 1 - (i + 1)], q[n - 1 - i])
152
+ else:
153
+ x.ctrl(q[0:n - 1 - i], q[n - 1 - i])
154
+
155
+ @cudaq.kernel
156
+ def lshift(q: cudaq.qview, n: int):
157
+ for i in range(n):
158
+ if i == 0:
159
+ x(q[0])
160
+ elif i == 1:
161
+ x.ctrl(q[0], q[1])
162
+ else:
163
+ x.ctrl(q[0:i], q[i])
164
+
165
+ @cudaq.kernel
166
+ def d2q5_tstep(q: cudaq.qview, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]): # Renamed args
167
+ qx = q[0:nqx]
168
+ qy = q[nqx:nqx + nqy]
169
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir_val] # Renamed nq_dir
170
+ # Original logic for d2q5_tstep based on fixed indices i=1,2,3,4
171
+ # This assumes a specific mapping of self.dirs indices to shift operations.
172
+ # For +x (rshift qx), assume it corresponds to i=1's block in dirs_i_val
173
+ # For -x (lshift qx), assume it corresponds to i=2's block in dirs_i_val
174
+ # For +y (rshift qy), assume it corresponds to i=3's block in dirs_i_val
175
+ # For -y (lshift qy), assume it corresponds to i=4's block in dirs_i_val
176
+ # Important: The `dirs_i_val` is constructed based on `self.dirs`. The mapping between `self.dirs[i]`
177
+ # and the actual shift operation (+x, -x, +y, -y) must be consistent with these hardcoded indices.
178
+ # The snippet used i=1,2,3,4 which implies self.dirs[1] to self.dirs[4] are active.
179
+
180
+ # Example interpretation (matching original snippet structure):
181
+ # Original i=2 -> lshift qx (-x)
182
+ idx_lqx = 2
183
+ b_list = dirs_i_val[idx_lqx * nq_dir_val:(idx_lqx + 1) * nq_dir_val]
184
+ for j in range(nq_dir_val):
185
+ if b_list[j] == 0: x(qdir[j])
186
+ cudaq.control(lshift, qdir, qx, nqx)
187
+ for j in range(nq_dir_val):
188
+ if b_list[j] == 0: x(qdir[j])
189
+
190
+ # Original i=1 -> rshift qx (+x)
191
+ idx_rqx = 1
192
+ b_list = dirs_i_val[idx_rqx * nq_dir_val:(idx_rqx + 1) * nq_dir_val]
193
+ for j in range(nq_dir_val):
194
+ if b_list[j] == 0: x(qdir[j])
195
+ cudaq.control(rshift, qdir, qx, nqx)
196
+ for j in range(nq_dir_val):
197
+ if b_list[j] == 0: x(qdir[j])
198
+
199
+ # Original i=4 -> lshift qy (-y)
200
+ idx_lqy = 4
201
+ b_list = dirs_i_val[idx_lqy * nq_dir_val:(idx_lqy + 1) * nq_dir_val]
202
+ for j in range(nq_dir_val):
203
+ if b_list[j] == 0: x(qdir[j])
204
+ cudaq.control(lshift, qdir, qy, nqy)
205
+ for j in range(nq_dir_val):
206
+ if b_list[j] == 0: x(qdir[j])
207
+
208
+ # Original i=3 -> rshift qy (+y)
209
+ idx_rqy = 3
210
+ b_list = dirs_i_val[idx_rqy * nq_dir_val:(idx_rqy + 1) * nq_dir_val]
211
+ for j in range(nq_dir_val):
212
+ if b_list[j] == 0: x(qdir[j])
213
+ cudaq.control(rshift, qdir, qy, nqy)
214
+ for j in range(nq_dir_val):
215
+ if b_list[j] == 0: x(qdir[j])
216
+
217
+
218
+ @cudaq.kernel
219
+ def d2q5_tstep_wrapper(state_arg: cudaq.State, nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]): # Renamed args
220
+ q = cudaq.qvector(state_arg)
221
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
222
+ prep_op(qdir[2], qdir[1], qdir[0]) # Assuming nq_dir_val is always 3 for prep_op
223
+ d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
224
+ prep_op(qdir[2], qdir[1], qdir[0])
225
+
226
+ @cudaq.kernel
227
+ def d2q5_tstep_wrapper_hadamard(vec_arg: list[complex], nqx: int, nqy: int, nq_dir_val: int, dirs_i_val: list[int]): # Renamed args
228
+ q = cudaq.qvector(vec_arg) # Renamed vec
229
+ qdir = q[nqx + nqy:nqx + nqy + nq_dir_val]
230
+ qy = q[nqx:nqx + nqy]
231
+ prep_op(qdir[2], qdir[1], qdir[0])
232
+ d2q5_tstep(q, nqx, nqy, nq_dir_val, dirs_i_val)
233
+ prep_op(qdir[2], qdir[1], qdir[0])
234
+ for i in range(nqy):
235
+ h(qy[i])
236
+
237
+ def run_timestep_func(vec_arg, hadamard=False): # Renamed vec
238
+ if hadamard:
239
+ result = cudaq.get_state(d2q5_tstep_wrapper_hadamard, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
240
+ else:
241
+ result = cudaq.get_state(d2q5_tstep_wrapper, vec_arg, num_reg_qubits, num_reg_qubits, self.nq_dir, self.dirs_i_list)
242
+
243
+ num_nonzero_ranks = num_ranks / (2**num_anc)
244
+ rank_slice_cupy = to_cupy_array(result)
245
+
246
+ if rank >= num_nonzero_ranks and num_nonzero_ranks > 0: # Check num_nonzero_ranks > 0
247
+ sub_sv_zeros = np.zeros(N_sub_per_rank, dtype=np.complex128)
248
+ cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_zeros.ctypes.data, sub_sv_zeros.nbytes, cp.cuda.runtime.memcpyHostToDevice)
249
+
250
+ if rank == 0 and num_nonzero_ranks < 1 and N_sub_per_rank > 0 : # Check N_sub_per_rank > 0
251
+ sub_sv_get = (rank_slice_cupy).get()
252
+ limit_idx = int(N_tot_state_vector / (2**num_anc))
253
+ if limit_idx < N_sub_per_rank : # Ensure index is within bounds
254
+ sub_sv_get[limit_idx:] = 0
255
+ cp.cuda.runtime.memcpy(rank_slice_cupy.data.ptr, sub_sv_get.ctypes.data, sub_sv_get.nbytes, cp.cuda.runtime.memcpyHostToDevice)
256
+ return result
257
+ self.run_timestep = run_timestep_func
258
+
259
+ def write_state(self, state_to_write, t_step_str_val): # Renamed t_step, t_step_str
260
+ rank_slice_cupy = to_cupy_array(state_to_write)
261
+ num_nonzero_ranks = num_ranks / (2**num_anc)
262
+
263
+ if rank < num_nonzero_ranks or (rank==0 and num_nonzero_ranks <=0): # Logic for saving if this rank has data
264
+ save_path = intermediate_folder_path / f"{t_step_str_val}_{rank}.npy" # Use t_step_str_val
265
+ with open(save_path, 'wb') as f:
266
+ arr_to_save = None
267
+ data_limit = N_sub_per_rank
268
+ if num_nonzero_ranks < 1 and rank == 0:
269
+ data_limit = int(N_tot_state_vector / (2**num_anc))
270
+
271
+ if data_limit > 0:
272
+ relevant_part_cupy = cp.real(rank_slice_cupy[:data_limit])
273
+ else:
274
+ relevant_part_cupy = cp.array([], dtype=cp.float64) # Empty array if no data
275
+
276
+ # Reshape and downsample based on current_N and downsampling_factor
277
+ # This assumes the relevant_part_cupy[:current_N*current_N] is the 2D grid data
278
+ if relevant_part_cupy.size >= current_N * current_N:
279
+ arr_flat = relevant_part_cupy[:current_N * current_N]
280
+ if downsampling_factor > 1:
281
+ arr_reshaped = arr_flat.reshape((current_N, current_N))
282
+ arr_downsampled = arr_reshaped[::downsampling_factor, ::downsampling_factor]
283
+ arr_to_save = arr_downsampled.flatten()
284
+ else:
285
+ arr_to_save = arr_flat # No downsampling needed
286
+ elif relevant_part_cupy.size > 0 : # Partial data less than full grid
287
+ # This case is tricky for downsampling. For now, save what is available if it's 1D after downsampling.
288
+ # The original code had a direct downsample on 1D array.
289
+ # For consistency, if it's smaller than current_N*current_N, it's likely already "flat" for that part.
290
+ if downsampling_factor > 1:
291
+ arr_to_save = relevant_part_cupy[::downsampling_factor]
292
+ else:
293
+ arr_to_save = relevant_part_cupy
294
+
295
+ if arr_to_save is not None and arr_to_save.size > 0:
296
+ np.save(f, arr_to_save.get() if isinstance(arr_to_save, cp.ndarray) else arr_to_save)
297
+ # else: print(f"Debug: No data to save for {save_path}")
298
+
299
+ def run_evolution(self, initial_state_arg, total_timesteps, observable=False):
300
+ current_state_val = initial_state_arg # Renamed current_state
301
+ for t_iter in range(total_timesteps):
302
+ next_state_val = None # Renamed next_state
303
+ if t_iter == total_timesteps - 1 and observable:
304
+ next_state_val = self.run_timestep(current_state_val, True)
305
+ self.write_state(next_state_val, str(t_iter + 1) + "_h")
306
+ else:
307
+ next_state_val = self.run_timestep(current_state_val)
308
+ if (t_iter + 1) % timesteps_per_frame == 0:
309
+ self.write_state(next_state_val, str(t_iter + 1)) # Pass as string
310
+ if rank == 0:
311
+ print(f"Timestep: {t_iter + 1}/{total_timesteps}")
312
+
313
+ cp.get_default_memory_pool().free_all_blocks()
314
+ current_state_val = next_state_val
315
+
316
+ last_saved_timestep_name = str(total_timesteps) if not observable else str(total_timesteps) + "_h"
317
+ final_state_file_path = intermediate_folder_path / f"{last_saved_timestep_name}_{rank}.npy"
318
+ final_T_state_file_path = intermediate_folder_path / f"{total_timesteps}_{rank}.npy"
319
+
320
+ needs_final_save = True
321
+ if observable and final_state_file_path.exists():
322
+ needs_final_save = False
323
+ elif not observable and final_T_state_file_path.exists() and total_timesteps > 0 and total_timesteps % timesteps_per_frame == 0 :
324
+ needs_final_save = False
325
+ elif not observable and final_T_state_file_path.exists(): # Saved by previous final write
326
+ needs_final_save = False
327
+
328
+ if needs_final_save and total_timesteps > 0 :
329
+ if not observable:
330
+ self.write_state(current_state_val, str(total_timesteps)) # Pass as string
331
+ elif total_timesteps == 0: # Save initial state if T=0
332
+ self.write_state(current_state_val, "0")
333
+
334
+
335
+ if rank == 0:
336
+ print(f"Timestep: {total_timesteps}/{total_timesteps} (Evolution complete)")
337
+ cp.get_default_memory_pool().free_all_blocks()
338
+ self.final_state = current_state_val
339
+
340
+ # downsampling_factor from user's snippet
341
+ downsampling_factor = 2**5
342
+ if current_N < downsampling_factor and current_N > 0: # Adjust if N is too small
343
+ downsampling_factor = current_N
344
+ elif current_N == 0: # Should be caught by num_reg_qubits check
345
+ print("Error: current_N is 0, cannot proceed.")
346
+ return None
347
+
348
+ qlbm_obj = QLBMAdvecDiffD2Q5_new(ux=ux_input, uy=uy_input)
349
+
350
+ initial_state_val = cudaq.get_state(alloc_kernel, num_qubits_total) # Renamed initial_state
351
+ rank_slice_init = to_cupy_array(initial_state_val)
352
+
353
+ xv_init = np.arange(current_N)
354
+ yv_init = np.arange(current_N)
355
+
356
+ print(f'Start initializing state with {distribution_type} distribution (ux={ux_input}, uy={uy_input})...')
357
+ initial_grid_2d = initial_state_func_eval(*np.meshgrid(xv_init, yv_init))
358
+ sub_sv_init_flat = initial_grid_2d.flatten().astype(np.complex128)
359
+
360
+ full_initial_sv_host = np.zeros(N_sub_per_rank, dtype=np.complex128)
361
+ num_computational_states = current_N * current_N
362
+
363
+ if len(sub_sv_init_flat) == num_computational_states:
364
+ if num_computational_states <= N_sub_per_rank:
365
+ full_initial_sv_host[:num_computational_states] = sub_sv_init_flat
366
+ else: # Should not happen if N_sub_per_rank is correctly N_tot_state_vector
367
+ print(f"Error: Grid data {num_computational_states} too large for N_sub_per_rank {N_sub_per_rank}")
368
+ return None
369
+ else:
370
+ print(f"Warning: Mismatch in expected initial state size {num_computational_states} and calculated {len(sub_sv_init_flat)}")
371
+ fill_len = min(len(sub_sv_init_flat), num_computational_states, N_sub_per_rank)
372
+ full_initial_sv_host[:fill_len] = sub_sv_init_flat[:fill_len]
373
+
374
+
375
+ print(f'Rank {rank}: Initial state (N*N part) prepared. Size: {len(sub_sv_init_flat)}. Total N_sub_per_rank: {N_sub_per_rank}')
376
+ cp.cuda.runtime.memcpy(rank_slice_init.data.ptr, full_initial_sv_host.ctypes.data, full_initial_sv_host.nbytes, cp.cuda.runtime.memcpyHostToDevice)
377
+ print(f'Rank {rank}: Initial state copied to device.')
378
+
379
+ print("Starting QLBM evolution...")
380
+ qlbm_obj.run_evolution(initial_state_val, T) # observable is False by default
381
+ print("QLBM evolution complete.")
382
+
383
+ print("Generating animation with PyVista...")
384
+ downsampled_N = current_N // downsampling_factor
385
+ if downsampled_N == 0 and current_N > 0: # Ensure at least 1x1 grid if not empty
386
+ downsampled_N = 1
387
+ elif current_N == 0:
388
+ print("Error: Downsampled grid size is zero because current_N is zero.")
389
+ return None
390
+
391
+ plotted_timesteps_str = []
392
+ if T == 0: # Handle T=0 case for plotting initial state
393
+ if (intermediate_folder_path / f"0_{rank}.npy").exists():
394
+ plotted_timesteps_str.append("0")
395
+ else:
396
+ if timesteps_per_frame > 0:
397
+ for t_step_val in range(timesteps_per_frame, T + 1, timesteps_per_frame): # Renamed t_step
398
+ if intermediate_folder_path.joinpath(f"{t_step_val}_{rank}.npy").exists(): # rank is 0
399
+ plotted_timesteps_str.append(str(t_step_val))
400
+
401
+ final_T_path = intermediate_folder_path.joinpath(f"{T}_{rank}.npy")
402
+ final_Th_path = intermediate_folder_path.joinpath(f"{T}_h_{rank}.npy") # Though observable=False
403
+
404
+ if T > 0: # Redundant due to outer T==0 check, but safe
405
+ if final_Th_path.exists():
406
+ if str(T)+"_h" not in plotted_timesteps_str:
407
+ plotted_timesteps_str.append(str(T)+"_h")
408
+ elif final_T_path.exists():
409
+ if str(T) not in plotted_timesteps_str:
410
+ plotted_timesteps_str.append(str(T))
411
+
412
+ plotted_timesteps_str = sorted(list(set(plotted_timesteps_str)), key=lambda k_str: (int(k_str.replace('_h','')), k_str))
413
+
414
+
415
+ if not plotted_timesteps_str : # Combined check for T=0 and T>0
416
+ print(f"Warning: No .npy files found for plotting for T={T}. Animation may be empty or fail.")
417
+ return None # No frames to plot
418
+
419
+ data_frames_list = [] # Renamed data
420
+ actual_timesteps_for_title = []
421
+
422
+ for i_str_val in plotted_timesteps_str: # Renamed i_str
423
+ try:
424
+ file_path_load = intermediate_folder_path / f"{i_str_val}_{rank}.npy" # Renamed file_path
425
+ sol_loaded = np.load(file_path_load) # Renamed sol
426
+ if sol_loaded.size == downsampled_N * downsampled_N:
427
+ Z_data = np.reshape(sol_loaded, (downsampled_N, downsampled_N)) # Renamed Z
428
+ data_frames_list.append(Z_data)
429
+ actual_timesteps_for_title.append(str(i_str_val).replace('_h',''))
430
+ else:
431
+ print(f"Warning: File {file_path_load} has unexpected size {sol_loaded.size}. Expected {downsampled_N*downsampled_N}. Skipping this frame.")
432
+ continue
433
+ except FileNotFoundError:
434
+ print(f"Warning: File not found for timestep {i_str_val} ({file_path_load}). Skipping this frame.")
435
+ continue
436
+ except Exception as e_load: # Renamed e
437
+ print(f"Error loading or reshaping file for timestep {i_str_val}: {e_load}. Skipping this frame.")
438
+ continue
439
+
440
+ if not data_frames_list:
441
+ print("Error: No data available to create animation after loading. Check .npy file generation and paths.")
442
+ return None
443
+
444
+ # --- PyVista Animation Setup ---
445
+ # Define plot ranges (as in original matplotlib version)
446
+ x_coords_plot = np.linspace(-10, 10, downsampled_N) # Renamed x_plot
447
+ y_coords_plot = np.linspace(-10, 10, downsampled_N) # Renamed y_plot
448
+ X_mesh_plot, Y_mesh_plot = np.meshgrid(x_coords_plot, y_coords_plot) # Renamed X_plot, Y_plot
449
+
450
+ # Normalization factor for Z values (geometry and color)
451
+ # Use the max absolute value of the first frame for consistent scaling reference
452
+ norm_factor_pv = np.max(np.abs(data_frames_list[0])) if data_frames_list[0].size > 0 and np.max(np.abs(data_frames_list[0])) != 0 else 1.0
453
+ if norm_factor_pv == 0: norm_factor_pv = 1.0
454
+
455
+ # Determine a Z-axis limit for color mapping (clim)
456
+ # Based on max value across all frames (normalized by norm_factor_pv for consistency if geometry is also normalized by it)
457
+ all_data_max_val = norm_factor_pv # Initialize with first frame's norm
458
+ if len(data_frames_list) > 0:
459
+ max_vals_frames = [np.max(np.abs(d_frame)) for d_frame in data_frames_list if d_frame.size > 0] # Renamed d
460
+ if max_vals_frames:
461
+ global_max_val_frames = max(max_vals_frames) # Renamed global_max_val
462
+ if global_max_val_frames > 0 : all_data_max_val = global_max_val_frames
463
+
464
+ # clim_upper is based on the range of actual scalar values being plotted
465
+ # If we plot Z_frame/norm_factor_pv as scalars, clim_upper is all_data_max_val/norm_factor_pv
466
+ # If we plot Z_frame as scalars, clim_upper is all_data_max_val
467
+ # Let's plot Z_frame/norm_factor_pv as scalars for consistency with previous z_lim=0.6 idea
468
+
469
+ clim_upper_pv = (all_data_max_val / norm_factor_pv) * 1.0 # Target a bit above max normalized value, e.g. *1.0 for exact max
470
+ if clim_upper_pv == 0 : clim_upper_pv = 0.6 # Fallback like original
471
+
472
+ # GIF generation setup
473
+ results_base_dir = "Results"
474
+ gif_frames_for_naming = int(simulation_fps * T)
475
+ if gif_frames_for_naming == 0: gif_frames_for_naming = 1
476
+ dist_name_part = distribution_type.replace(' ','').replace('(Original)','').replace('(','').replace(')','')
477
+ specific_folder_name = (f"d2q5_pv_N{current_N}_T{T}_fr{gif_frames_for_naming}_"
478
+ f"dist{dist_name_part}_ux{ux_input:.2f}_uy{uy_input:.2f}")
479
+ final_output_dir = Path(results_base_dir) / specific_folder_name
480
+ os.makedirs(final_output_dir, exist_ok=True)
481
+ gif_path_to_return = final_output_dir / "animation_pv.gif" # Changed name slightly
482
+
483
+ plotter = pv.Plotter(off_screen=True, window_size=[1000, 800], notebook=False)
484
+ # Set a fixed camera position for consistent view across frames
485
+ plotter.camera_position = 'xy' # Simple top-down-ish view
486
+ plotter.camera.azimuth = 30 # Rotate a bit
487
+ plotter.camera.elevation = 25 # Look down a bit
488
+ plotter.camera.zoom(1.2) # Zoom in slightly
489
+
490
+ try:
491
+ plotter.open_gif(str(gif_path_to_return), fps=10)
492
+
493
+ # Corrected sargs dictionary
494
+ sargs = dict(
495
+ title='Density',
496
+ vertical=True, # Ensure this is 'vertical'
497
+ position_x=0.05, # X position of the scalar bar
498
+ position_y=0.1, # Y position of the scalar bar
499
+ height=0.8, # Height of the scalar bar
500
+ width=0.1, # Width of the scalar bar
501
+ label_font_size=16, # Font size for labels on the bar
502
+ title_font_size=18 # Font size for the title of the bar
503
+ )
504
+
505
+ for frame_idx, Z_frame_data in enumerate(data_frames_list):
506
+ plotter.clear_actors()
507
+
508
+ z_coords_geometry = (Z_frame_data / norm_factor_pv)
509
+
510
+ if not X_mesh_plot.flags.c_contiguous: X_mesh_plot_flat = X_mesh_plot.ravel(order='C')
511
+ else: X_mesh_plot_flat = X_mesh_plot.ravel()
512
+ if not Y_mesh_plot.flags.c_contiguous: Y_mesh_plot_flat = Y_mesh_plot.ravel(order='C')
513
+ else: Y_mesh_plot_flat = Y_mesh_plot.ravel()
514
+ if not z_coords_geometry.flags.c_contiguous: z_coords_geometry_flat = z_coords_geometry.ravel(order='C')
515
+ else: z_coords_geometry_flat = z_coords_geometry.ravel()
516
+
517
+ surface_mesh = pv.StructuredGrid()
518
+ surface_mesh.points = np.column_stack((X_mesh_plot_flat, Y_mesh_plot_flat, z_coords_geometry_flat))
519
+ surface_mesh.dimensions = (downsampled_N, downsampled_N, 1)
520
+ surface_mesh["density"] = z_coords_geometry_flat
521
+
522
+ plotter.add_mesh(surface_mesh, scalars="density", cmap="viridis",
523
+ clim=[0, clim_upper_pv], scalar_bar_args=sargs, # sargs is passed here
524
+ show_edges=False)
525
+
526
+ current_ts_title = actual_timesteps_for_title[frame_idx] if frame_idx < len(actual_timesteps_for_title) else "Unknown"
527
+ plotter.add_text(f'QLBM Simulation (Timestep: {current_ts_title})',
528
+ position='upper_edge', font_size=18, color='black', font='arial')
529
+
530
+ plotter.write_frame()
531
+
532
+ plotter.close()
533
+ print(f"PyVista animation saved to {gif_path_to_return}")
534
+
535
+ except Exception as e_pv_anim:
536
+ print(f"Error during PyVista animation/saving: {e_pv_anim}")
537
+ if 'plotter' in locals() and hasattr(plotter, 'close'):
538
+ plotter.close()
539
+ return None
540
+
541
+ return str(gif_path_to_return)
542
+
543
+
544
+
545
+ # --- Gradio Interface Definition (remains largely the same) ---
546
+ def qlbm_gradio_interface(num_reg_qubits_input: int, timescale_input: int, distribution_type_param: str, ux_param: float, uy_param: float):
547
+ num_reg_qubits_val = int(num_reg_qubits_input)
548
+ timescale_val = int(timescale_input)
549
+ ux_val = float(ux_param)
550
+ uy_val = float(uy_param)
551
+
552
+ print(f"Gradio Interface: num_reg_qubits={num_reg_qubits_val}, T={timescale_val}, Distribution={distribution_type_param}, ux={ux_val}, uy={uy_val}")
553
+
554
+ gif_path_output = simulate_qlbm_and_animate(
555
+ num_reg_qubits=num_reg_qubits_val,
556
+ T=timescale_val,
557
+ distribution_type=distribution_type_param,
558
+ ux_input=ux_val,
559
+ uy_input=uy_val
560
+ )
561
+
562
+ if gif_path_output is None:
563
+ gr.Warning("Animation generation failed. Please check console for errors.")
564
+ return None
565
+ return gif_path_output
566
+
567
+ with gr.Blocks(theme=gr.themes.Soft(), title="QLBM Simulation with PyVista") as qlbm_demo:
568
+ gr.Markdown(
569
+ """
570
+ # ⚛️ Quantum Lattice Boltzmann Method (QLBM) Simulator (PyVista Animation)
571
+ Welcome to the Quantum Lattice Boltzmann Method (QLBM) simulator! This version uses PyVista for 3D animation.
572
+
573
+ **What is QLBM?**
574
+ The Lattice Boltzmann Method (LBM) is a computational fluid dynamics technique for simulating fluid flow. QLBM is its quantum counterpart, leveraging quantum algorithms to potentially offer advantages for certain types of simulations. It's used here to model advection-diffusion phenomena.
575
+
576
+ **How this Simulator Works:**
577
+ This simulator implements a D2Q5 model on a quantum computer simulator (CUDA-Q).
578
+ - Control grid size (via Number of Register Qubits: $N=2^{\text{num_reg_qubits}}$).
579
+ - Set total simulation time (Timescale T).
580
+ - Choose initial distribution.
581
+ - Set advection velocities `ux` and `uy`.
582
+ The simulation evolves this state, and a PyVista animation of the density is generated.
583
+
584
+ **Note:** Higher qubit counts and longer timescales are computationally intensive. Advection velocities should be small (e.g., < 0.3).
585
+ """
586
+ )
587
+
588
+ with gr.Row():
589
+ with gr.Column(scale=1):
590
+ gr.Markdown("## Simulation Parameters")
591
+ num_reg_qubits_slider = gr.Slider(
592
+ minimum=2,
593
+ maximum=11, # Max from original snippet
594
+ value=8,
595
+ step=1,
596
+ label="Number of Register Qubits (num_reg_qubits)",
597
+ info="Determines N for grid size (2^N x 2^N). Max 11 (Note: >8 can be very slow)."
598
+ )
599
+ timescale_slider = gr.Slider(
600
+ minimum=0, # Allow T=0
601
+ maximum=2000, # Max from original snippet
602
+ value=100,
603
+ step=10, # Step from original snippet
604
+ label="Timescale (T)",
605
+ info="Total number of timesteps. Max 2000."
606
+ )
607
+ distribution_options = ["Sine Wave (Original)", "Gaussian", "Random"]
608
+ distribution_type_input = gr.Radio(
609
+ choices=distribution_options,
610
+ value="Sine Wave (Original)",
611
+ label="Initial Distribution Type",
612
+ info="Select the initial pattern of the substance on the grid."
613
+ )
614
+ ux_slider = gr.Slider(
615
+ minimum=-0.4,
616
+ maximum=0.4,
617
+ value=0.2,
618
+ step=0.01,
619
+ label="Advection Velocity ux",
620
+ info="x-component of the background advection velocity."
621
+ )
622
+ uy_slider = gr.Slider(
623
+ minimum=-0.4,
624
+ maximum=0.4,
625
+ value=0.15,
626
+ step=0.01,
627
+ label="Advection Velocity uy",
628
+ info="y-component of the background advection velocity."
629
+ )
630
+
631
+ run_qlbm_btn = gr.Button("Run QLBM Simulation (PyVista)", variant="primary")
632
+
633
+ with gr.Column(scale=3):
634
+ qlbm_plot_output = gr.Image(label="QLBM Simulation Animation (PyVista GIF)", type="filepath", height=600) # Keep as Image for GIF
635
+
636
+ qlbm_inputs_list = [num_reg_qubits_slider, timescale_slider, distribution_type_input, ux_slider, uy_slider]
637
+
638
+ run_qlbm_btn.click(
639
+ fn=qlbm_gradio_interface,
640
+ inputs=qlbm_inputs_list,
641
+ outputs=qlbm_plot_output
642
+ )
643
+
644
+ gr.Examples(
645
+ examples=[
646
+ [8, 100, "Sine Wave (Original)", 0.2, 0.15],
647
+ [6, 50, "Gaussian", 0.1, 0.05],
648
+ [4, 30, "Random", -0.05, 0.1],
649
+ [7, 70, "Sine Wave (Original)", 0.0, 0.0] # Example with no advection
650
+ ],
651
+ inputs=qlbm_inputs_list,
652
+ outputs=qlbm_plot_output,
653
+ fn=qlbm_gradio_interface,
654
+ cache_examples=False
655
+ )
656
+
657
+ if __name__ == "__main__":
658
+ # It's good practice to set the CUDA-Q target early if it's fixed.
659
+ try:
660
+ cudaq.set_target('nvidia', option='fp64')
661
+ print(f"CUDA-Q Target successfully set to: {cudaq.get_target().name}")
662
+ except Exception as e_target:
663
+ print(f"Warning: Could not set CUDA-Q target to 'nvidia'. Error: {e_target}")
664
+ print(f"Current CUDA-Q Target: {cudaq.get_target().name}. Performance may be affected if not using GPU.")
665
+
666
+ qlbm_demo.launch()
index.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width" />
6
+ <title>My static Space</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <div class="card">
11
+ <h1>Welcome to your static Space!</h1>
12
+ <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
+ <p>
14
+ Also don't forget to check the
15
+ <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
+ </p>
17
+ </div>
18
+ </body>
19
+ </html>
new-working-branch ADDED
File without changes
packages.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ libgl1-mesa-glx
2
+ libosmesa6-dev
3
+ libegl1-mesa-dev
4
+ xvfb
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pyvista
2
+ kaleido # <-- Ensure this line is present
3
+ imageio
4
+ matplotlib
5
+ plotly
6
+ imageio-ffmpeg # Still recommended for broader codec support
7
+ gradio
8
+ cudaq # <--- This is the corrected line
9
+ numpy
10
+ cupy-cuda12x # Replace XXX with your target CUDA version (e.g., cupy-cuda118 or cupy-cuda12x)
11
+
style.css ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ padding: 2rem;
3
+ font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
+ }
5
+
6
+ h1 {
7
+ font-size: 16px;
8
+ margin-top: 0;
9
+ }
10
+
11
+ p {
12
+ color: rgb(107, 114, 128);
13
+ font-size: 15px;
14
+ margin-bottom: 10px;
15
+ margin-top: 5px;
16
+ }
17
+
18
+ .card {
19
+ max-width: 620px;
20
+ margin: 0 auto;
21
+ padding: 16px;
22
+ border: 1px solid lightgray;
23
+ border-radius: 16px;
24
+ }
25
+
26
+ .card p:last-child {
27
+ margin-bottom: 0;
28
+ }