harishaseebat92 commited on
Commit
a80ef73
·
1 Parent(s): dae35a5

the plots should have distinct colors

Browse files
Files changed (3) hide show
  1. em_trame.py +146 -66
  2. requirements.txt +5 -0
  3. 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
- print(f"QPU error for {field_type} positions {valid_positions}: {e}")
 
 
 
 
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
- # Color keyed by normalized (px+py)
439
- s = max(0.0, min(1.0, (px + py) / float(max_sum)))
440
- rgba = cmap(s)
 
 
 
 
 
 
 
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
- for i, k in enumerate(sorted(keys, key=lambda x: (str(x[0]), x[1], x[2]))):
 
 
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
- s = max(0.0, min(1.0, (px + py) / float(max_sum)))
545
- rgba = cmap(s)
 
 
 
 
 
 
 
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
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
640
- os.makedirs(dl_dir, exist_ok=True)
641
  nx_val = int(state.nx or 0)
642
  from datetime import datetime as _dt
643
- path = os.path.join(dl_dir, f"qpu_timeseries_{field}_nx{nx_val}_{_dt.now().strftime('%Y%m%d_%H%M%S')}.csv")
 
644
  import csv
645
- with open(path, "w", newline="") as f:
646
- writer = csv.writer(f)
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
- state.export_status_message = f"Exported QPU CSV to {path}"
 
 
 
 
 
 
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
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
678
- os.makedirs(dl_dir, exist_ok=True)
679
  nx_val = int(state.nx or 0)
680
- path = os.path.join(dl_dir, f"qpu_timeseries_{field}_nx{nx_val}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
681
- fig.write_image(path, scale=2, width=900, height=660)
682
- state.export_status_message = f"Exported QPU PNG to {path}"
 
 
 
 
 
 
 
 
 
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
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
701
- os.makedirs(dl_dir, exist_ok=True)
702
  nx_val = int(state.nx or 0)
703
- path = os.path.join(dl_dir, f"qpu_timeseries_{field}_nx{nx_val}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html")
704
- pio.write_html(fig, file=path, include_plotlyjs="cdn", full_html=True)
705
- state.export_status_message = f"Exported QPU HTML to {path}"
 
 
 
 
 
 
 
 
 
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
- print("QPU complete.")
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
- print(f"QPU error: {e}")
875
  finally:
876
  state.is_running = False
877
  state.stop_button_disabled = True
878
  ctrl.view_update()
879
  return
880
 
881
- print("Running simulation...")
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, dtype=float)
 
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
- path = os.path.join(dl_dir, f"surface_{field}_nx{nx}_{suffix}.vtp")
1981
- current_mesh.save(path)
1982
- state.export_status_message = f"Exported VTK to {path}"
 
 
 
 
 
 
 
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 timestamped folder in Downloads."""
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
- out_dir = os.path.join(dl_dir, f"vtk_sequence_{field}_nx{nx}_{suffix}")
2004
- os.makedirs(out_dir, exist_ok=True)
2005
-
2006
- times = np.asarray(snapshot_times)
2007
- for i, (z_data, t) in enumerate(zip(frames, times)):
2008
- points = np.c_[X_grids[field].ravel(), Y_grids[field].ravel(), z_data.ravel() * z_scale]
2009
- poly = pv.PolyData(points)
2010
- mesh = poly.delaunay_2d()
2011
- mesh["scalars"] = z_data.ravel()
2012
- fname = f"{field}_frame_{i:04d}_t{t:.3f}s.vtp"
2013
- mesh.save(os.path.join(out_dir, fname))
2014
-
2015
- state.export_status_message = f"Exported {len(frames)} frames to {out_dir}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- path = os.path.join(dl_dir, f"surface_anim_{field}_nx{nx}_{suffix}.mp4")
 
 
 
 
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(path, framerate=20)
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
- state.export_status_message = f"Exported MP4 to {path}"
 
 
 
 
 
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
- for i, k in enumerate(sorted(keys, key=lambda x: (x[1], x[2]))):
2323
- _, px, py = k
 
 
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
- s = max(0.0, min(1.0, (px + py) / float(max_sum)))
2332
- rgba = cmap(s)
 
 
 
 
 
 
 
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=dash_styles[i % len(dash_styles)]),
2337
- marker=dict(size=7, symbol=marker_symbols[i % len(marker_symbols)], color=color_hex),
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("Console Output", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
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
- print(f"Simulation interrupted at step {i}/{steps}")
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