Spaces:
Runtime error
Runtime error
harishaseebat92
commited on
Commit
·
a80ef73
1
Parent(s):
dae35a5
the plots should have distinct colors
Browse files- em_trame.py +146 -66
- requirements.txt +5 -0
- utils/delta_impulse_generator.py +28 -4
em_trame.py
CHANGED
|
@@ -4,6 +4,8 @@ import pyvista as pv
|
|
| 4 |
import threading
|
| 5 |
import base64
|
| 6 |
from collections import defaultdict
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from trame.app import get_server
|
| 9 |
from trame_vuetify.ui.vuetify3 import SinglePageLayout
|
|
@@ -367,7 +369,7 @@ def _refresh_qpu_plot_figures():
|
|
| 367 |
state.qpu_ts_other_ready = False
|
| 368 |
state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
|
| 369 |
|
| 370 |
-
def _build_qpu_timeseries_plotly_multi(configs, nx: int, T: float, snapshot_dt: float, impulse_pos, progress_callback=None):
|
| 371 |
times = qutils.create_time_frames(T, snapshot_dt)
|
| 372 |
fig = go.Figure()
|
| 373 |
# Gather all validated positions across configs (after expanding 'All') to compute color normalization
|
|
@@ -421,11 +423,16 @@ def _build_qpu_timeseries_plotly_multi(configs, nx: int, T: float, snapshot_dt:
|
|
| 421 |
|
| 422 |
# Fetch time series from QPU for this field and the validated positions
|
| 423 |
try:
|
| 424 |
-
series_map_field = qutils.run_qpu(field_type, valid_positions, None, float(T), float(snapshot_dt), int(nx), None, impulse_pos, progress_callback=_sub_progress)
|
| 425 |
except Exception as e:
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
continue
|
| 428 |
cmap = _cmap_for_field(field_type)
|
|
|
|
| 429 |
for i, (px, py) in enumerate(valid_positions):
|
| 430 |
ys = (series_map_field or {}).get((px, py), [])
|
| 431 |
if not ys or len(ys) != len(times):
|
|
@@ -435,9 +442,16 @@ def _build_qpu_timeseries_plotly_multi(configs, nx: int, T: float, snapshot_dt:
|
|
| 435 |
max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
|
| 436 |
except Exception:
|
| 437 |
pass
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
|
| 442 |
label = _normalized_position_label(px, py, gw, gh)
|
| 443 |
key = (str(field_type), int(px), int(py))
|
|
@@ -531,7 +545,9 @@ def _rebuild_qpu_fig_filtered(filter_value: str, position_filter: str = "All pos
|
|
| 531 |
markers = ["circle", "square", "diamond", "triangle-up", "x"]
|
| 532 |
max_abs = 0.0
|
| 533 |
label_map = qpu_ts_cache.get("key_to_label") or {}
|
| 534 |
-
|
|
|
|
|
|
|
| 535 |
field_name, px, py = k
|
| 536 |
ys = series_map.get(k) or []
|
| 537 |
if not ys or len(ys) != len(times):
|
|
@@ -541,8 +557,15 @@ def _rebuild_qpu_fig_filtered(filter_value: str, position_filter: str = "All pos
|
|
| 541 |
except Exception:
|
| 542 |
pass
|
| 543 |
cmap = _cmap_for_field(field_name)
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
|
| 547 |
label = label_map.get((str(field_name), int(px), int(py))) or _format_grid_label(px, py, field_name)
|
| 548 |
fig.add_trace(go.Scatter(
|
|
@@ -636,14 +659,14 @@ def export_qpu_timeseries_csv():
|
|
| 636 |
field = qpu_ts_cache.get("field") or "Ez"
|
| 637 |
if not times or not series_map:
|
| 638 |
raise ValueError("No QPU time series to export.")
|
| 639 |
-
|
| 640 |
-
os.makedirs(dl_dir, exist_ok=True)
|
| 641 |
nx_val = int(state.nx or 0)
|
| 642 |
from datetime import datetime as _dt
|
| 643 |
-
|
|
|
|
| 644 |
import csv
|
| 645 |
-
with
|
| 646 |
-
writer = csv.writer(
|
| 647 |
# Detect key layout: (px,py) legacy or (field,px,py)
|
| 648 |
keys = list(series_map.keys())
|
| 649 |
if keys and len(keys[0]) == 3:
|
|
@@ -660,7 +683,13 @@ def export_qpu_timeseries_csv():
|
|
| 660 |
for i, t in enumerate(times):
|
| 661 |
row = [t] + [ (series_map.get((px,py)) or [None])[i] if i < len(series_map.get((px,py)) or []) else None for (px,py) in pts ]
|
| 662 |
writer.writerow(row)
|
| 663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
except Exception as e:
|
| 665 |
state.export_status_message = f"QPU CSV export failed: {e}"
|
| 666 |
finally:
|
|
@@ -674,12 +703,20 @@ def export_qpu_timeseries_png():
|
|
| 674 |
field = qpu_ts_cache.get("field") or "Ez"
|
| 675 |
if fig is None:
|
| 676 |
raise ValueError("No QPU time series to export.")
|
| 677 |
-
|
| 678 |
-
os.makedirs(dl_dir, exist_ok=True)
|
| 679 |
nx_val = int(state.nx or 0)
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
except Exception as e:
|
| 684 |
msg = str(e)
|
| 685 |
if "kaleido" in msg.lower():
|
|
@@ -697,12 +734,20 @@ def export_qpu_timeseries_html():
|
|
| 697 |
field = qpu_ts_cache.get("field") or "Ez"
|
| 698 |
if fig is None:
|
| 699 |
raise ValueError("No QPU time series to export.")
|
| 700 |
-
|
| 701 |
-
os.makedirs(dl_dir, exist_ok=True)
|
| 702 |
nx_val = int(state.nx or 0)
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
except Exception as e:
|
| 707 |
state.export_status_message = f"QPU HTML export failed: {e}"
|
| 708 |
finally:
|
|
@@ -836,7 +881,7 @@ def run_simulation_only():
|
|
| 836 |
state.status_message = "Building QPU time series..."
|
| 837 |
state.simulation_progress = 60
|
| 838 |
# Build and render Plotly chart (multi-config)
|
| 839 |
-
fig = _build_qpu_timeseries_plotly_multi(configs, nx, T, snapshot_dt, impulse_pos, progress_callback=_progress_callback)
|
| 840 |
qpu_ts_cache["fig"] = fig
|
| 841 |
try:
|
| 842 |
ctrl.qpu_ts_update(fig)
|
|
@@ -863,7 +908,7 @@ def run_simulation_only():
|
|
| 863 |
state.error_message = "No QPU time series generated. Check Δt, T, nx, and monitor points."
|
| 864 |
state.status_message = "Warning: No QPU time series generated."
|
| 865 |
state.status_type = "warning"
|
| 866 |
-
|
| 867 |
except Exception as e:
|
| 868 |
state.error_message = f"QPU run failed: {e}"
|
| 869 |
state.status_message = f"QPU Error: {e}"
|
|
@@ -871,14 +916,14 @@ def run_simulation_only():
|
|
| 871 |
state.show_progress = False
|
| 872 |
state.run_button_text = "Run Simulation"
|
| 873 |
state.qpu_ts_ready = False
|
| 874 |
-
|
| 875 |
finally:
|
| 876 |
state.is_running = False
|
| 877 |
state.stop_button_disabled = True
|
| 878 |
ctrl.view_update()
|
| 879 |
return
|
| 880 |
|
| 881 |
-
|
| 882 |
state.status_message = "Running simulation... This may take a while."
|
| 883 |
state.simulation_progress = 30
|
| 884 |
# Pass user-defined snapshot Δt; keep solver dt=0.1 inside run_sim
|
|
@@ -887,8 +932,7 @@ def run_simulation_only():
|
|
| 887 |
return stop_simulation
|
| 888 |
|
| 889 |
state.simulation_progress = 50
|
| 890 |
-
simulation_data, snapshot_times = run_sim(nx, na, R, initial_state, T, snapshot_dt=snapshot_dt, stop_check=_stop_check, progress_callback=_progress_callback)
|
| 891 |
-
print("Simulation complete.")
|
| 892 |
log_to_console("Simulation complete.")
|
| 893 |
|
| 894 |
state.simulation_progress = 80
|
|
@@ -1438,7 +1482,8 @@ def update_geometry_preview():
|
|
| 1438 |
denom = max(nx - 1, 1)
|
| 1439 |
x, y = np.arange(nx) / denom, np.arange(nx) / denom
|
| 1440 |
X, Y = np.meshgrid(x, y)
|
| 1441 |
-
Z = np.zeros_like(X,
|
|
|
|
| 1442 |
points = np.c_[X.ravel(), Y.ravel(), Z.ravel()]
|
| 1443 |
poly = pv.PolyData(points)
|
| 1444 |
mesh = poly.delaunay_2d()
|
|
@@ -1972,20 +2017,25 @@ def export_vtk():
|
|
| 1972 |
state.show_export_status = True
|
| 1973 |
return
|
| 1974 |
try:
|
| 1975 |
-
dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
|
| 1976 |
-
os.makedirs(dl_dir, exist_ok=True)
|
| 1977 |
field = state.surface_field or "Ez"
|
| 1978 |
nx = int(state.nx)
|
| 1979 |
suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1980 |
-
|
| 1981 |
-
|
| 1982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1983 |
except Exception as e:
|
| 1984 |
state.export_status_message = f"Export failed: {e}"
|
| 1985 |
state.show_export_status = True
|
| 1986 |
|
| 1987 |
def export_vtk_all_frames():
|
| 1988 |
-
"""Export a .vtp file for each time frame of the selected component into a
|
| 1989 |
global data_frames, X_grids, z_scale, snapshot_times
|
| 1990 |
try:
|
| 1991 |
if not state.simulation_has_run:
|
|
@@ -1997,22 +2047,37 @@ def export_vtk_all_frames():
|
|
| 1997 |
if snapshot_times is None:
|
| 1998 |
raise ValueError("Snapshot times are unavailable.")
|
| 1999 |
|
| 2000 |
-
dl_dir = os.path.expanduser("~/Downloads")
|
| 2001 |
-
suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 2002 |
nx = int(state.nx)
|
| 2003 |
-
|
| 2004 |
-
|
| 2005 |
-
|
| 2006 |
-
|
| 2007 |
-
|
| 2008 |
-
|
| 2009 |
-
|
| 2010 |
-
|
| 2011 |
-
|
| 2012 |
-
|
| 2013 |
-
|
| 2014 |
-
|
| 2015 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2016 |
except Exception as e:
|
| 2017 |
state.export_status_message = f"Export failed: {e}"
|
| 2018 |
finally:
|
|
@@ -2031,12 +2096,13 @@ def export_mp4():
|
|
| 2031 |
if len(frames) < 2:
|
| 2032 |
raise ValueError("Only one frame available; increase T or simulation steps.")
|
| 2033 |
|
| 2034 |
-
# Output path
|
| 2035 |
-
dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
|
| 2036 |
-
os.makedirs(dl_dir, exist_ok=True)
|
| 2037 |
nx = int(state.nx)
|
| 2038 |
suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 2039 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2040 |
|
| 2041 |
# Build with a dedicated off-screen plotter at a macro-block friendly size
|
| 2042 |
movie_plotter = pv.Plotter(off_screen=True, window_size=(1280, 720))
|
|
@@ -2069,7 +2135,7 @@ def export_mp4():
|
|
| 2069 |
except Exception:
|
| 2070 |
movie_plotter.view_isometric()
|
| 2071 |
|
| 2072 |
-
movie_plotter.open_movie(
|
| 2073 |
n_frames = len(frames)
|
| 2074 |
for z_data in frames:
|
| 2075 |
if mesh.n_points != z_data.size:
|
|
@@ -2096,7 +2162,12 @@ def export_mp4():
|
|
| 2096 |
movie_plotter.write_frame()
|
| 2097 |
movie_plotter.close()
|
| 2098 |
|
| 2099 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2100 |
except Exception as e:
|
| 2101 |
state.export_status_message = f"Export failed: {e}"
|
| 2102 |
finally:
|
|
@@ -2319,8 +2390,10 @@ def _rebuild_qpu_fig_others(selected_field: str, position_filter: str = "All pos
|
|
| 2319 |
dashes = ["solid", "dash", "dot", "dashdot"]
|
| 2320 |
markers = ["circle", "square", "diamond", "triangle-up", "x"]
|
| 2321 |
max_abs = 0.0
|
| 2322 |
-
|
| 2323 |
-
|
|
|
|
|
|
|
| 2324 |
ys = series_map.get(k) or []
|
| 2325 |
if not ys or len(ys) != len(times):
|
| 2326 |
continue
|
|
@@ -2328,13 +2401,20 @@ def _rebuild_qpu_fig_others(selected_field: str, position_filter: str = "All pos
|
|
| 2328 |
max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
|
| 2329 |
except Exception:
|
| 2330 |
pass
|
| 2331 |
-
|
| 2332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2333 |
color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
|
| 2334 |
fig.add_trace(go.Scatter(
|
| 2335 |
x=times, y=ys, mode='lines+markers', name=f"({px}, {py})",
|
| 2336 |
-
line=dict(color=color_hex, width=2.5, dash=
|
| 2337 |
-
marker=dict(size=7, symbol=
|
| 2338 |
hovertemplate=f"{sel} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>({px}, {py})</extra>",
|
| 2339 |
))
|
| 2340 |
fig.update_layout(title=f"IBM QPU Time Series (Other components: {sel})", height=660, width=900, margin=dict(l=50, r=30, t=50, b=50), hovermode="x unified", legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""))
|
|
@@ -2814,7 +2894,7 @@ with SinglePageLayout(server) as layout:
|
|
| 2814 |
|
| 2815 |
# Console Window
|
| 2816 |
with vuetify3.VCard(classes="mt-2", style="font-size: 0.8rem;"):
|
| 2817 |
-
with vuetify3.VCardTitle("
|
| 2818 |
pass
|
| 2819 |
with vuetify3.VCardText(classes="py-1 px-2", style="height: 150px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace;"):
|
| 2820 |
vuetify3.VTextarea(
|
|
|
|
| 4 |
import threading
|
| 5 |
import base64
|
| 6 |
from collections import defaultdict
|
| 7 |
+
import tempfile
|
| 8 |
+
from pathlib import Path
|
| 9 |
|
| 10 |
from trame.app import get_server
|
| 11 |
from trame_vuetify.ui.vuetify3 import SinglePageLayout
|
|
|
|
| 369 |
state.qpu_ts_other_ready = False
|
| 370 |
state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
|
| 371 |
|
| 372 |
+
def _build_qpu_timeseries_plotly_multi(configs, nx: int, T: float, snapshot_dt: float, impulse_pos, progress_callback=None, print_callback=None):
|
| 373 |
times = qutils.create_time_frames(T, snapshot_dt)
|
| 374 |
fig = go.Figure()
|
| 375 |
# Gather all validated positions across configs (after expanding 'All') to compute color normalization
|
|
|
|
| 423 |
|
| 424 |
# Fetch time series from QPU for this field and the validated positions
|
| 425 |
try:
|
| 426 |
+
series_map_field = qutils.run_qpu(field_type, valid_positions, None, float(T), float(snapshot_dt), int(nx), None, impulse_pos, progress_callback=_sub_progress, print_callback=print_callback)
|
| 427 |
except Exception as e:
|
| 428 |
+
msg = f"QPU error for {field_type} positions {valid_positions}: {e}"
|
| 429 |
+
if print_callback:
|
| 430 |
+
print_callback(msg)
|
| 431 |
+
else:
|
| 432 |
+
print(msg)
|
| 433 |
continue
|
| 434 |
cmap = _cmap_for_field(field_type)
|
| 435 |
+
num_pts = len(valid_positions)
|
| 436 |
for i, (px, py) in enumerate(valid_positions):
|
| 437 |
ys = (series_map_field or {}).get((px, py), [])
|
| 438 |
if not ys or len(ys) != len(times):
|
|
|
|
| 442 |
max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
|
| 443 |
except Exception:
|
| 444 |
pass
|
| 445 |
+
|
| 446 |
+
# Color keyed by index to ensure distinctness
|
| 447 |
+
if num_pts > 1:
|
| 448 |
+
# Distribute from 0.3 (light) to 0.9 (dark)
|
| 449 |
+
s_index = i / (num_pts - 1)
|
| 450 |
+
s_light = 0.3 + 0.6 * s_index
|
| 451 |
+
else:
|
| 452 |
+
s_light = 0.6 # Medium intensity for single point
|
| 453 |
+
|
| 454 |
+
rgba = cmap(s_light)
|
| 455 |
color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
|
| 456 |
label = _normalized_position_label(px, py, gw, gh)
|
| 457 |
key = (str(field_type), int(px), int(py))
|
|
|
|
| 545 |
markers = ["circle", "square", "diamond", "triangle-up", "x"]
|
| 546 |
max_abs = 0.0
|
| 547 |
label_map = qpu_ts_cache.get("key_to_label") or {}
|
| 548 |
+
sorted_keys = sorted(keys, key=lambda x: (str(x[0]), x[1], x[2]))
|
| 549 |
+
num_keys = len(sorted_keys)
|
| 550 |
+
for i, k in enumerate(sorted_keys):
|
| 551 |
field_name, px, py = k
|
| 552 |
ys = series_map.get(k) or []
|
| 553 |
if not ys or len(ys) != len(times):
|
|
|
|
| 557 |
except Exception:
|
| 558 |
pass
|
| 559 |
cmap = _cmap_for_field(field_name)
|
| 560 |
+
|
| 561 |
+
# Color keyed by index to ensure distinctness
|
| 562 |
+
if num_keys > 1:
|
| 563 |
+
s_index = i / (num_keys - 1)
|
| 564 |
+
s_light = 0.3 + 0.6 * s_index
|
| 565 |
+
else:
|
| 566 |
+
s_light = 0.6
|
| 567 |
+
|
| 568 |
+
rgba = cmap(s_light)
|
| 569 |
color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
|
| 570 |
label = label_map.get((str(field_name), int(px), int(py))) or _format_grid_label(px, py, field_name)
|
| 571 |
fig.add_trace(go.Scatter(
|
|
|
|
| 659 |
field = qpu_ts_cache.get("field") or "Ez"
|
| 660 |
if not times or not series_map:
|
| 661 |
raise ValueError("No QPU time series to export.")
|
| 662 |
+
|
|
|
|
| 663 |
nx_val = int(state.nx or 0)
|
| 664 |
from datetime import datetime as _dt
|
| 665 |
+
filename = f"qpu_timeseries_{field}_nx{nx_val}_{_dt.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 666 |
+
|
| 667 |
import csv
|
| 668 |
+
with tempfile.NamedTemporaryFile(mode='w', newline='', suffix=".csv", delete=False) as tmp:
|
| 669 |
+
writer = csv.writer(tmp)
|
| 670 |
# Detect key layout: (px,py) legacy or (field,px,py)
|
| 671 |
keys = list(series_map.keys())
|
| 672 |
if keys and len(keys[0]) == 3:
|
|
|
|
| 683 |
for i, t in enumerate(times):
|
| 684 |
row = [t] + [ (series_map.get((px,py)) or [None])[i] if i < len(series_map.get((px,py)) or []) else None for (px,py) in pts ]
|
| 685 |
writer.writerow(row)
|
| 686 |
+
temp_path = tmp.name
|
| 687 |
+
|
| 688 |
+
content = Path(temp_path).read_bytes()
|
| 689 |
+
server.controller.download_file(content, filename)
|
| 690 |
+
Path(temp_path).unlink()
|
| 691 |
+
|
| 692 |
+
state.export_status_message = f"Exported QPU CSV to {filename}"
|
| 693 |
except Exception as e:
|
| 694 |
state.export_status_message = f"QPU CSV export failed: {e}"
|
| 695 |
finally:
|
|
|
|
| 703 |
field = qpu_ts_cache.get("field") or "Ez"
|
| 704 |
if fig is None:
|
| 705 |
raise ValueError("No QPU time series to export.")
|
| 706 |
+
|
|
|
|
| 707 |
nx_val = int(state.nx or 0)
|
| 708 |
+
filename = f"qpu_timeseries_{field}_nx{nx_val}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
| 709 |
+
|
| 710 |
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
| 711 |
+
temp_path = tmp.name
|
| 712 |
+
|
| 713 |
+
fig.write_image(temp_path, scale=2, width=900, height=660)
|
| 714 |
+
|
| 715 |
+
content = Path(temp_path).read_bytes()
|
| 716 |
+
server.controller.download_file(content, filename)
|
| 717 |
+
Path(temp_path).unlink()
|
| 718 |
+
|
| 719 |
+
state.export_status_message = f"Exported QPU PNG to {filename}"
|
| 720 |
except Exception as e:
|
| 721 |
msg = str(e)
|
| 722 |
if "kaleido" in msg.lower():
|
|
|
|
| 734 |
field = qpu_ts_cache.get("field") or "Ez"
|
| 735 |
if fig is None:
|
| 736 |
raise ValueError("No QPU time series to export.")
|
| 737 |
+
|
|
|
|
| 738 |
nx_val = int(state.nx or 0)
|
| 739 |
+
filename = f"qpu_timeseries_{field}_nx{nx_val}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
| 740 |
+
|
| 741 |
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp:
|
| 742 |
+
temp_path = tmp.name
|
| 743 |
+
|
| 744 |
+
pio.write_html(fig, file=temp_path, include_plotlyjs="cdn", full_html=True)
|
| 745 |
+
|
| 746 |
+
content = Path(temp_path).read_bytes()
|
| 747 |
+
server.controller.download_file(content, filename)
|
| 748 |
+
Path(temp_path).unlink()
|
| 749 |
+
|
| 750 |
+
state.export_status_message = f"Exported QPU HTML to {filename}"
|
| 751 |
except Exception as e:
|
| 752 |
state.export_status_message = f"QPU HTML export failed: {e}"
|
| 753 |
finally:
|
|
|
|
| 881 |
state.status_message = "Building QPU time series..."
|
| 882 |
state.simulation_progress = 60
|
| 883 |
# Build and render Plotly chart (multi-config)
|
| 884 |
+
fig = _build_qpu_timeseries_plotly_multi(configs, nx, T, snapshot_dt, impulse_pos, progress_callback=_progress_callback, print_callback=log_to_console)
|
| 885 |
qpu_ts_cache["fig"] = fig
|
| 886 |
try:
|
| 887 |
ctrl.qpu_ts_update(fig)
|
|
|
|
| 908 |
state.error_message = "No QPU time series generated. Check Δt, T, nx, and monitor points."
|
| 909 |
state.status_message = "Warning: No QPU time series generated."
|
| 910 |
state.status_type = "warning"
|
| 911 |
+
log_to_console("QPU complete.")
|
| 912 |
except Exception as e:
|
| 913 |
state.error_message = f"QPU run failed: {e}"
|
| 914 |
state.status_message = f"QPU Error: {e}"
|
|
|
|
| 916 |
state.show_progress = False
|
| 917 |
state.run_button_text = "Run Simulation"
|
| 918 |
state.qpu_ts_ready = False
|
| 919 |
+
log_to_console(f"QPU error: {e}")
|
| 920 |
finally:
|
| 921 |
state.is_running = False
|
| 922 |
state.stop_button_disabled = True
|
| 923 |
ctrl.view_update()
|
| 924 |
return
|
| 925 |
|
| 926 |
+
log_to_console("Running simulation...")
|
| 927 |
state.status_message = "Running simulation... This may take a while."
|
| 928 |
state.simulation_progress = 30
|
| 929 |
# Pass user-defined snapshot Δt; keep solver dt=0.1 inside run_sim
|
|
|
|
| 932 |
return stop_simulation
|
| 933 |
|
| 934 |
state.simulation_progress = 50
|
| 935 |
+
simulation_data, snapshot_times = run_sim(nx, na, R, initial_state, T, snapshot_dt=snapshot_dt, stop_check=_stop_check, progress_callback=_progress_callback, print_callback=log_to_console)
|
|
|
|
| 936 |
log_to_console("Simulation complete.")
|
| 937 |
|
| 938 |
state.simulation_progress = 80
|
|
|
|
| 1482 |
denom = max(nx - 1, 1)
|
| 1483 |
x, y = np.arange(nx) / denom, np.arange(nx) / denom
|
| 1484 |
X, Y = np.meshgrid(x, y)
|
| 1485 |
+
Z = np.zeros_like(X,
|
| 1486 |
+
dtype=float)
|
| 1487 |
points = np.c_[X.ravel(), Y.ravel(), Z.ravel()]
|
| 1488 |
poly = pv.PolyData(points)
|
| 1489 |
mesh = poly.delaunay_2d()
|
|
|
|
| 2017 |
state.show_export_status = True
|
| 2018 |
return
|
| 2019 |
try:
|
|
|
|
|
|
|
| 2020 |
field = state.surface_field or "Ez"
|
| 2021 |
nx = int(state.nx)
|
| 2022 |
suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 2023 |
+
filename = f"surface_{field}_nx{nx}_{suffix}.vtp"
|
| 2024 |
+
|
| 2025 |
+
# Write to a temporary file first
|
| 2026 |
+
with tempfile.NamedTemporaryFile(suffix=".vtp", delete=False) as tmp:
|
| 2027 |
+
current_mesh.save(tmp.name)
|
| 2028 |
+
content = Path(tmp.name).read_bytes()
|
| 2029 |
+
server.controller.download_file(content, filename)
|
| 2030 |
+
Path(tmp.name).unlink() # Clean up
|
| 2031 |
+
|
| 2032 |
+
state.export_status_message = f"Exported VTK to {filename}"
|
| 2033 |
except Exception as e:
|
| 2034 |
state.export_status_message = f"Export failed: {e}"
|
| 2035 |
state.show_export_status = True
|
| 2036 |
|
| 2037 |
def export_vtk_all_frames():
|
| 2038 |
+
"""Export a .vtp file for each time frame of the selected component into a zip file."""
|
| 2039 |
global data_frames, X_grids, z_scale, snapshot_times
|
| 2040 |
try:
|
| 2041 |
if not state.simulation_has_run:
|
|
|
|
| 2047 |
if snapshot_times is None:
|
| 2048 |
raise ValueError("Snapshot times are unavailable.")
|
| 2049 |
|
|
|
|
|
|
|
| 2050 |
nx = int(state.nx)
|
| 2051 |
+
suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 2052 |
+
zip_filename = f"vtk_sequence_{field}_nx{nx}_{suffix}.zip"
|
| 2053 |
+
|
| 2054 |
+
import zipfile
|
| 2055 |
+
|
| 2056 |
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
|
| 2057 |
+
temp_zip_path = tmp_zip.name
|
| 2058 |
+
|
| 2059 |
+
with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
| 2060 |
+
times = np.asarray(snapshot_times)
|
| 2061 |
+
for i, (z_data, t) in enumerate(zip(frames, times)):
|
| 2062 |
+
points = np.c_[X_grids[field].ravel(), Y_grids[field].ravel(), z_data.ravel() * z_scale]
|
| 2063 |
+
poly = pv.PolyData(points)
|
| 2064 |
+
mesh = poly.delaunay_2d()
|
| 2065 |
+
mesh["scalars"] = z_data.ravel()
|
| 2066 |
+
|
| 2067 |
+
fname = f"{field}_frame_{i:04d}_t{t:.3f}s.vtp"
|
| 2068 |
+
|
| 2069 |
+
with tempfile.NamedTemporaryFile(suffix=".vtp", delete=False) as tmp_vtp:
|
| 2070 |
+
tmp_vtp_path = tmp_vtp.name
|
| 2071 |
+
|
| 2072 |
+
mesh.save(tmp_vtp_path)
|
| 2073 |
+
zf.write(tmp_vtp_path, arcname=fname)
|
| 2074 |
+
Path(tmp_vtp_path).unlink()
|
| 2075 |
+
|
| 2076 |
+
content = Path(temp_zip_path).read_bytes()
|
| 2077 |
+
server.controller.download_file(content, zip_filename)
|
| 2078 |
+
Path(temp_zip_path).unlink()
|
| 2079 |
+
|
| 2080 |
+
state.export_status_message = f"Exported {len(frames)} frames to {zip_filename}"
|
| 2081 |
except Exception as e:
|
| 2082 |
state.export_status_message = f"Export failed: {e}"
|
| 2083 |
finally:
|
|
|
|
| 2096 |
if len(frames) < 2:
|
| 2097 |
raise ValueError("Only one frame available; increase T or simulation steps.")
|
| 2098 |
|
|
|
|
|
|
|
|
|
|
| 2099 |
nx = int(state.nx)
|
| 2100 |
suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 2101 |
+
filename = f"surface_anim_{field}_nx{nx}_{suffix}.mp4"
|
| 2102 |
+
|
| 2103 |
+
# Create a temporary file for the MP4
|
| 2104 |
+
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
|
| 2105 |
+
temp_path = tmp.name
|
| 2106 |
|
| 2107 |
# Build with a dedicated off-screen plotter at a macro-block friendly size
|
| 2108 |
movie_plotter = pv.Plotter(off_screen=True, window_size=(1280, 720))
|
|
|
|
| 2135 |
except Exception:
|
| 2136 |
movie_plotter.view_isometric()
|
| 2137 |
|
| 2138 |
+
movie_plotter.open_movie(temp_path, framerate=20)
|
| 2139 |
n_frames = len(frames)
|
| 2140 |
for z_data in frames:
|
| 2141 |
if mesh.n_points != z_data.size:
|
|
|
|
| 2162 |
movie_plotter.write_frame()
|
| 2163 |
movie_plotter.close()
|
| 2164 |
|
| 2165 |
+
# Read the file content and trigger download
|
| 2166 |
+
content = Path(temp_path).read_bytes()
|
| 2167 |
+
server.controller.download_file(content, filename)
|
| 2168 |
+
Path(temp_path).unlink() # Clean up
|
| 2169 |
+
|
| 2170 |
+
state.export_status_message = f"Exported MP4 to {filename}"
|
| 2171 |
except Exception as e:
|
| 2172 |
state.export_status_message = f"Export failed: {e}"
|
| 2173 |
finally:
|
|
|
|
| 2390 |
dashes = ["solid", "dash", "dot", "dashdot"]
|
| 2391 |
markers = ["circle", "square", "diamond", "triangle-up", "x"]
|
| 2392 |
max_abs = 0.0
|
| 2393 |
+
sorted_keys = sorted(keys, key=lambda x: (x[1], x[2]))
|
| 2394 |
+
num_keys = len(sorted_keys)
|
| 2395 |
+
for i, k in enumerate(sorted_keys):
|
| 2396 |
+
field_name, px, py = k
|
| 2397 |
ys = series_map.get(k) or []
|
| 2398 |
if not ys or len(ys) != len(times):
|
| 2399 |
continue
|
|
|
|
| 2401 |
max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
|
| 2402 |
except Exception:
|
| 2403 |
pass
|
| 2404 |
+
|
| 2405 |
+
# Color keyed by index to ensure distinctness
|
| 2406 |
+
if num_keys > 1:
|
| 2407 |
+
s_index = i / (num_keys - 1)
|
| 2408 |
+
s_light = 0.3 + 0.6 * s_index
|
| 2409 |
+
else:
|
| 2410 |
+
s_light = 0.6
|
| 2411 |
+
|
| 2412 |
+
rgba = cmap(s_light)
|
| 2413 |
color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
|
| 2414 |
fig.add_trace(go.Scatter(
|
| 2415 |
x=times, y=ys, mode='lines+markers', name=f"({px}, {py})",
|
| 2416 |
+
line=dict(color=color_hex, width=2.5, dash=dashes[i % len(dashes)]),
|
| 2417 |
+
marker=dict(size=7, symbol=markers[i % len(markers)], color=color_hex),
|
| 2418 |
hovertemplate=f"{sel} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>({px}, {py})</extra>",
|
| 2419 |
))
|
| 2420 |
fig.update_layout(title=f"IBM QPU Time Series (Other components: {sel})", height=660, width=900, margin=dict(l=50, r=30, t=50, b=50), hovermode="x unified", legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""))
|
|
|
|
| 2894 |
|
| 2895 |
# Console Window
|
| 2896 |
with vuetify3.VCard(classes="mt-2", style="font-size: 0.8rem;"):
|
| 2897 |
+
with vuetify3.VCardTitle("Status", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
|
| 2898 |
pass
|
| 2899 |
with vuetify3.VCardText(classes="py-1 px-2", style="height: 150px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace;"):
|
| 2900 |
vuetify3.VTextarea(
|
requirements.txt
CHANGED
|
@@ -31,3 +31,8 @@ trame_plotly
|
|
| 31 |
Pillow==10.4.0
|
| 32 |
packaging==25.0
|
| 33 |
python-dateutil==2.9.0.post0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
Pillow==10.4.0
|
| 32 |
packaging==25.0
|
| 33 |
python-dateutil==2.9.0.post0
|
| 34 |
+
|
| 35 |
+
# Export utilities
|
| 36 |
+
imageio
|
| 37 |
+
imageio-ffmpeg
|
| 38 |
+
kaleido
|
utils/delta_impulse_generator.py
CHANGED
|
@@ -245,7 +245,7 @@ def V2(nx, dt):
|
|
| 245 |
qc.append(Wj_block(2, n, "0" + "1" * (n - 1), -dt, -np.pi / 2, xgate=False), list(derivatives[n + 1:2 * n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
|
| 246 |
return qc
|
| 247 |
|
| 248 |
-
def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, progress_callback=None):
|
| 249 |
"""
|
| 250 |
Runs the quantum simulation for electromagnetic scattering with fixed dt=0.1.
|
| 251 |
Captures frames only at user-defined snapshot times: [0, Δt, 2Δt, ..., ≤ T_eff],
|
|
@@ -254,6 +254,12 @@ def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, prog
|
|
| 254 |
Returns:
|
| 255 |
frames (np.ndarray), snapshot_times (np.ndarray)
|
| 256 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
dt = 0.1
|
| 258 |
# Validate total time and compute solver-aligned end time
|
| 259 |
try:
|
|
@@ -310,9 +316,11 @@ def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, prog
|
|
| 310 |
frames.append(sv0[2 ** (na - 1)])
|
| 311 |
next_idx = 1 # next target_times index to capture
|
| 312 |
|
|
|
|
|
|
|
| 313 |
for i in range(steps):
|
| 314 |
if stop_check and stop_check():
|
| 315 |
-
|
| 316 |
break
|
| 317 |
# One solver step
|
| 318 |
qc.append(QFTGate(na), ancilla)
|
|
@@ -342,6 +350,8 @@ def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, prog
|
|
| 342 |
progress_callback(100.0)
|
| 343 |
except Exception:
|
| 344 |
pass
|
|
|
|
|
|
|
| 345 |
|
| 346 |
# Ensure snapshot_times align with number of captured frames (covers early stop)
|
| 347 |
frames_arr = np.asarray(frames)
|
|
@@ -402,13 +412,19 @@ def create_time_frames(total_time, snapshot_interval):
|
|
| 402 |
|
| 403 |
|
| 404 |
|
| 405 |
-
def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos):
|
| 406 |
"""
|
| 407 |
Compute time-series field values. Supports single-point and multi-point modes.
|
| 408 |
|
| 409 |
- Single-point (backward compatible): x, y are integers; returns list[float].
|
| 410 |
- Multi-point: x is a list/tuple of (ix, iy) integer pairs and y is None; returns dict[(ix,iy) -> list[float]].
|
| 411 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
na = 1
|
| 413 |
dt = 0.1
|
| 414 |
R = 4
|
|
@@ -434,6 +450,9 @@ def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos):
|
|
| 434 |
norm = np.linalg.norm(fp)
|
| 435 |
|
| 436 |
time_frames = create_time_frames(T, snapshot_time)
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
# Prepare outputs
|
| 439 |
if multi:
|
|
@@ -441,7 +460,7 @@ def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos):
|
|
| 441 |
else:
|
| 442 |
series_single = []
|
| 443 |
|
| 444 |
-
for time in time_frames:
|
| 445 |
steps = int(math.ceil(time / dt))
|
| 446 |
# Reference Ez field at impulse location for sign
|
| 447 |
Eref = Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref='Ez')
|
|
@@ -464,5 +483,10 @@ def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos):
|
|
| 464 |
series_by_point[(px, py)].append(Field_value)
|
| 465 |
else:
|
| 466 |
series_single.append(Field_value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
return series_by_point if multi else series_single
|
|
|
|
| 245 |
qc.append(Wj_block(2, n, "0" + "1" * (n - 1), -dt, -np.pi / 2, xgate=False), list(derivatives[n + 1:2 * n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
|
| 246 |
return qc
|
| 247 |
|
| 248 |
+
def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, progress_callback=None, print_callback=None):
|
| 249 |
"""
|
| 250 |
Runs the quantum simulation for electromagnetic scattering with fixed dt=0.1.
|
| 251 |
Captures frames only at user-defined snapshot times: [0, Δt, 2Δt, ..., ≤ T_eff],
|
|
|
|
| 254 |
Returns:
|
| 255 |
frames (np.ndarray), snapshot_times (np.ndarray)
|
| 256 |
"""
|
| 257 |
+
def _log(msg):
|
| 258 |
+
if print_callback:
|
| 259 |
+
print_callback(msg)
|
| 260 |
+
else:
|
| 261 |
+
print(msg)
|
| 262 |
+
|
| 263 |
dt = 0.1
|
| 264 |
# Validate total time and compute solver-aligned end time
|
| 265 |
try:
|
|
|
|
| 316 |
frames.append(sv0[2 ** (na - 1)])
|
| 317 |
next_idx = 1 # next target_times index to capture
|
| 318 |
|
| 319 |
+
_log(f"Starting simulation: T={T_eff:.2f}s, steps={steps}, snapshot_dt={snapshot_dt_eff:.2f}s")
|
| 320 |
+
|
| 321 |
for i in range(steps):
|
| 322 |
if stop_check and stop_check():
|
| 323 |
+
_log(f"Simulation interrupted at step {i}/{steps}")
|
| 324 |
break
|
| 325 |
# One solver step
|
| 326 |
qc.append(QFTGate(na), ancilla)
|
|
|
|
| 350 |
progress_callback(100.0)
|
| 351 |
except Exception:
|
| 352 |
pass
|
| 353 |
+
|
| 354 |
+
_log("Simulation completed.")
|
| 355 |
|
| 356 |
# Ensure snapshot_times align with number of captured frames (covers early stop)
|
| 357 |
frames_arr = np.asarray(frames)
|
|
|
|
| 412 |
|
| 413 |
|
| 414 |
|
| 415 |
+
def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos, progress_callback=None, print_callback=None):
|
| 416 |
"""
|
| 417 |
Compute time-series field values. Supports single-point and multi-point modes.
|
| 418 |
|
| 419 |
- Single-point (backward compatible): x, y are integers; returns list[float].
|
| 420 |
- Multi-point: x is a list/tuple of (ix, iy) integer pairs and y is None; returns dict[(ix,iy) -> list[float]].
|
| 421 |
"""
|
| 422 |
+
def _log(msg):
|
| 423 |
+
if print_callback:
|
| 424 |
+
print_callback(msg)
|
| 425 |
+
else:
|
| 426 |
+
print(msg)
|
| 427 |
+
|
| 428 |
na = 1
|
| 429 |
dt = 0.1
|
| 430 |
R = 4
|
|
|
|
| 450 |
norm = np.linalg.norm(fp)
|
| 451 |
|
| 452 |
time_frames = create_time_frames(T, snapshot_time)
|
| 453 |
+
total_frames = len(time_frames)
|
| 454 |
+
|
| 455 |
+
_log(f"Starting QPU simulation: T={T}s, frames={total_frames}, points={len(points)}")
|
| 456 |
|
| 457 |
# Prepare outputs
|
| 458 |
if multi:
|
|
|
|
| 460 |
else:
|
| 461 |
series_single = []
|
| 462 |
|
| 463 |
+
for idx, time in enumerate(time_frames):
|
| 464 |
steps = int(math.ceil(time / dt))
|
| 465 |
# Reference Ez field at impulse location for sign
|
| 466 |
Eref = Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref='Ez')
|
|
|
|
| 483 |
series_by_point[(px, py)].append(Field_value)
|
| 484 |
else:
|
| 485 |
series_single.append(Field_value)
|
| 486 |
+
|
| 487 |
+
if progress_callback:
|
| 488 |
+
progress_callback((idx + 1) / total_frames * 100)
|
| 489 |
+
|
| 490 |
+
_log("QPU simulation completed.")
|
| 491 |
|
| 492 |
return series_by_point if multi else series_single
|