harishaseebat92 commited on
Commit
b5dbacc
·
1 Parent(s): 067e2f3

EM: IBM QPU Integration

Browse files
.gitignore CHANGED
@@ -9,3 +9,6 @@ __pycache__/
9
  *.pyd
10
  .Python
11
  *.so
 
 
 
 
9
  *.pyd
10
  .Python
11
  *.so
12
+
13
+ # Virtual Environments
14
+ **/aqc_venv/
Dockerfile CHANGED
@@ -49,6 +49,10 @@ RUN python3 -m pip install --upgrade pip setuptools wheel \
49
  COPY --chown=user:user . .
50
  COPY docker/nginx.conf /etc/nginx/nginx.conf
51
 
 
 
 
 
52
  # Prepare writable directories for nginx (running as non-root later)
53
  RUN mkdir -p /tmp/nginx/body /tmp/nginx/proxy /tmp/nginx/fastcgi /tmp/nginx/uwsgi /tmp/nginx/scgi \
54
  && touch /tmp/nginx.access.log /tmp/nginx.error.log \
 
49
  COPY --chown=user:user . .
50
  COPY docker/nginx.conf /etc/nginx/nginx.conf
51
 
52
+ # 7. Install the local adapt-aqc package
53
+ # We do this after copying the files so the source code is available
54
+ RUN python3 -m pip install ./utils/adapt-aqc
55
+
56
  # Prepare writable directories for nginx (running as non-root later)
57
  RUN mkdir -p /tmp/nginx/body /tmp/nginx/proxy /tmp/nginx/fastcgi /tmp/nginx/uwsgi /tmp/nginx/scgi \
58
  && touch /tmp/nginx.access.log /tmp/nginx.error.log \
em/simulation.py CHANGED
@@ -4,9 +4,10 @@ EM Embedded - Simulation Module
4
  Contains simulation logic including run_simulation_only, reset_to_defaults,
5
  and stop handlers.
6
  """
 
7
  import numpy as np
8
 
9
- from .state import state, ctrl, _apply_workflow_highlights, is_statevector_estimator_selected
10
  from .globals import (
11
  plotter, simulation_data, current_mesh, snapshot_times,
12
  stop_simulation, qpu_ts_cache, sim_ts_cache, set_stop_simulation, reset_globals
@@ -356,6 +357,7 @@ def run_simulation_only():
356
  return qutils.run_sve(
357
  field_type,
358
  positions,
 
359
  None,
360
  total_time,
361
  snapshot_dt,
@@ -420,16 +422,204 @@ def run_simulation_only():
420
  pass
421
  return
422
 
423
- # IBM QPU placeholder branch
424
- if state.backend_type == "QPU":
425
- state.error_message = "IBM QPU backend is not yet available in this build. Please select the Statevector Estimator from the Simulator menu."
426
- state.status_message = "IBM QPU backend unavailable."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  state.status_type = "warning"
428
  state.show_progress = False
429
  state.is_running = False
430
  state.run_button_text = "RUN!"
431
  state.stop_button_disabled = True
432
- log_to_console("IBM QPU backend not connected. Use the Statevector Estimator simulator instead.")
433
  try:
434
  ctrl.view_update()
435
  except Exception:
 
4
  Contains simulation logic including run_simulation_only, reset_to_defaults,
5
  and stop handlers.
6
  """
7
+ import re
8
  import numpy as np
9
 
10
+ from .state import state, ctrl, _apply_workflow_highlights, is_statevector_estimator_selected, is_ibm_qpu_selected
11
  from .globals import (
12
  plotter, simulation_data, current_mesh, snapshot_times,
13
  stop_simulation, qpu_ts_cache, sim_ts_cache, set_stop_simulation, reset_globals
 
357
  return qutils.run_sve(
358
  field_type,
359
  positions,
360
+
361
  None,
362
  total_time,
363
  snapshot_dt,
 
422
  pass
423
  return
424
 
425
+ # IBM QPU branch
426
+ ibm_qpu_selected = is_ibm_qpu_selected()
427
+ if ibm_qpu_selected:
428
+ try:
429
+ log_to_console("Running IBM QPU simulation...")
430
+ state.status_message = "Running IBM QPU simulation..."
431
+ state.simulation_progress = 5
432
+ state.qpu_ts_ready = False
433
+ state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
434
+ state.qpu_ts_other_ready = False
435
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
436
+
437
+ # Import IBM QPU backend
438
+ try:
439
+ from quantum.utils.EBU_Quantum.no_body.base_functions import get_field_values as ibm_get_field_values, create_time_frames as ibm_create_time_frames
440
+ except ModuleNotFoundError:
441
+ from utils.EBU_Quantum.no_body.base_functions import get_field_values as ibm_get_field_values, create_time_frames as ibm_create_time_frames
442
+
443
+ # Inputs for IBM QPU (single field, single position only!)
444
+ snapshot_dt = float(state.dt_user)
445
+ ix_imp, iy_imp = nearest_node_index(float(state.impulse_x), float(state.impulse_y), nx)
446
+ impulse_pos = (ix_imp, iy_imp)
447
+
448
+ # Get field and single position from UI
449
+ # IBM QPU only supports one field and one position!
450
+ field_type = (state.qpu_field_components or "Ez").strip()
451
+ if field_type == "All":
452
+ field_type = "Ez" # Default to Ez if 'All' selected (not supported by IBM QPU)
453
+ log_to_console("Warning: IBM QPU only supports single field. Defaulting to Ez.")
454
+
455
+ # Parse single monitor position
456
+ pts_str = str(state.qpu_monitor_gridpoints or "").strip()
457
+ raw_pts = [tuple(map(int, m)) for m in re.findall(r"\((\d+)\s*,\s*(\d+)\)", pts_str)]
458
+ if not raw_pts:
459
+ # Default to impulse position
460
+ monitor_x, monitor_y = impulse_pos
461
+ log_to_console(f"No monitor position specified. Using impulse position ({monitor_x}, {monitor_y}).")
462
+ else:
463
+ # Use only the first position (IBM QPU restriction)
464
+ monitor_x, monitor_y = raw_pts[0]
465
+ if len(raw_pts) > 1:
466
+ log_to_console(f"Warning: IBM QPU only supports single position. Using first: ({monitor_x}, {monitor_y})")
467
+
468
+ state.status_message = "Step 1: Circuit Construction & Optimization (0-40%)..."
469
+ state.simulation_progress = 10
470
+
471
+ def _ibm_progress_callback(pct):
472
+ state.simulation_progress = int(pct)
473
+ # Update status message based on progress stage
474
+ if pct < 40:
475
+ state.status_message = f"Step 1: Circuit Construction & Optimization ({int(pct)}%)"
476
+ elif pct < 90:
477
+ state.status_message = f"Step 2: Circuit Execution ({int(pct)}%)"
478
+ else:
479
+ state.status_message = f"Step 3: Result Processing ({int(pct)}%)"
480
+
481
+ # Call the IBM QPU get_field_values function
482
+ field_values = ibm_get_field_values(
483
+ field=field_type,
484
+ x=monitor_x,
485
+ y=monitor_y,
486
+ T=float(T),
487
+ snapshot_time=snapshot_dt,
488
+ nx=nx,
489
+ impulse_pos=impulse_pos,
490
+ shots=10000,
491
+ pm_optimization_level=2,
492
+ simulation="False",
493
+ optimization="False",
494
+ platform="IBM",
495
+ progress_callback=_ibm_progress_callback,
496
+ print_callback=log_to_console,
497
+ )
498
+
499
+ # Build time frames to match the output
500
+ times = ibm_create_time_frames(float(T), snapshot_dt)
501
+
502
+ # Build Plotly figure for the single time series
503
+ import plotly.graph_objects as go
504
+ fig = go.Figure()
505
+
506
+ # Determine grid dimensions for label
507
+ if field_type == 'Ez':
508
+ gw, gh = nx, nx
509
+ elif field_type == 'Hx':
510
+ gw, gh = nx, nx - 1
511
+ else:
512
+ gw, gh = nx - 1, nx
513
+
514
+ from .utils import normalized_position_label
515
+ label = normalized_position_label(monitor_x, monitor_y, gw, gh)
516
+
517
+ # Color based on field type
518
+ if field_type == 'Ez':
519
+ color = "#d32f2f" # Red
520
+ elif field_type == 'Hx':
521
+ color = "#388e3c" # Green
522
+ else:
523
+ color = "#1976d2" # Blue
524
+
525
+ fig.add_trace(
526
+ go.Scatter(
527
+ x=list(times),
528
+ y=[float(v) for v in field_values],
529
+ mode='lines+markers',
530
+ name=f"{field_type} @ {label}",
531
+ line=dict(color=color, width=2.5),
532
+ marker=dict(size=7, symbol="circle", color=color),
533
+ hovertemplate=f"{field_type} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>{label}</extra>",
534
+ )
535
+ )
536
+
537
+ max_abs = max((abs(float(v)) for v in field_values), default=1.0)
538
+ pad = 0.12 * max_abs if max_abs > 0 else 0.1
539
+
540
+ fig.update_layout(
541
+ title=f"IBM QPU Time Series - {field_type} @ {label}",
542
+ height=660, width=900,
543
+ margin=dict(l=50, r=30, t=50, b=50),
544
+ hovermode="x unified",
545
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""),
546
+ paper_bgcolor="#FFFFFF",
547
+ plot_bgcolor="#FFFFFF",
548
+ )
549
+ fig.update_xaxes(title_text="Time (s)", title_font=dict(size=22), tickfont=dict(size=16), showgrid=True, gridcolor="rgba(0,0,0,.06)")
550
+ fig.update_yaxes(title_text="Field Value", title_font=dict(size=22), tickfont=dict(size=16), showgrid=True, gridcolor="rgba(0,0,0,.06)")
551
+ fig.update_yaxes(range=[-max_abs - pad, max_abs + pad])
552
+
553
+ # Cache the figure for export
554
+ qpu_ts_cache["fig"] = fig
555
+ qpu_ts_cache["times"] = list(times)
556
+ qpu_ts_cache["series_map"] = {(field_type, monitor_x, monitor_y): list(field_values)}
557
+ qpu_ts_cache["field"] = field_type
558
+ qpu_ts_cache["unique_fields"] = [field_type]
559
+
560
+ try:
561
+ ctrl.qpu_ts_update(fig)
562
+ except Exception:
563
+ pass
564
+
565
+ state.simulation_has_run = True
566
+ state.run_button_text = "Successful!"
567
+ state.simulation_progress = 100
568
+ state.status_message = "IBM QPU simulation completed successfully!"
569
+ log_to_console("IBM QPU run completed")
570
+ state.status_type = "success"
571
+ state.show_progress = False
572
+
573
+ ready = bool(field_values) and len(field_values) > 0
574
+ state.qpu_ts_ready = ready
575
+ state.qpu_plot_style = (
576
+ "width: 900px; height: 660px; margin: 0 auto;"
577
+ if ready else "display: none; width: 900px; height: 660px; margin: 0 auto;"
578
+ )
579
+ state.qpu_ts_other_ready = False
580
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
581
+
582
+ # Set filter options for single result
583
+ state.qpu_plot_field_options = ["All", field_type]
584
+ state.qpu_plot_filter = "All"
585
+ state.qpu_plot_position_options = ["All positions", label]
586
+ state.qpu_plot_position_filter = "All positions"
587
+
588
+ if not ready:
589
+ state.error_message = "No IBM QPU time series generated. Check Δt, T, nx, and monitor position."
590
+ state.status_message = "Warning: No IBM QPU time series generated."
591
+ state.status_type = "warning"
592
+ log_to_console("IBM QPU complete.")
593
+
594
+ except Exception as e:
595
+ import traceback
596
+ state.error_message = f"IBM QPU run failed: {e}"
597
+ state.status_message = f"IBM QPU Error: {e}"
598
+ state.status_type = "error"
599
+ state.show_progress = False
600
+ state.run_button_text = "RUN!"
601
+ state.qpu_ts_ready = False
602
+ log_to_console(f"IBM QPU error: {e}")
603
+ log_to_console(traceback.format_exc())
604
+ finally:
605
+ state.is_running = False
606
+ state.stop_button_disabled = True
607
+ try:
608
+ ctrl.view_update()
609
+ except Exception:
610
+ pass
611
+ return
612
+
613
+ # IonQ QPU placeholder branch (not yet implemented)
614
+ if state.backend_type == "QPU" and state.selected_qpu == "IonQ QPU":
615
+ state.error_message = "IonQ QPU backend is not yet available in this build. Please select IBM QPU or the Statevector Estimator."
616
+ state.status_message = "IonQ QPU backend unavailable."
617
  state.status_type = "warning"
618
  state.show_progress = False
619
  state.is_running = False
620
  state.run_button_text = "RUN!"
621
  state.stop_button_disabled = True
622
+ log_to_console("IonQ QPU backend not connected. Use IBM QPU or Statevector Estimator instead.")
623
  try:
624
  ctrl.view_update()
625
  except Exception:
em/state.py CHANGED
@@ -11,6 +11,7 @@ __all__ = [
11
  "enable_point_picking_on_plotter",
12
  "_apply_workflow_highlights", "_determine_workflow_step",
13
  "is_statevector_estimator_selected",
 
14
  ]
15
 
16
 
@@ -266,6 +267,8 @@ def _init_state_defaults():
266
  "qpu_plot_field_options": ["All"],
267
  "qpu_plot_position_filter": "All positions",
268
  "qpu_plot_position_options": ["All positions"],
 
 
269
  # Additional QPU monitor slots
270
  "qpu_monitor_count": 0,
271
  "qpu_field_components_2": "Ez",
@@ -348,5 +351,15 @@ def is_statevector_estimator_selected() -> bool:
348
  return False
349
 
350
 
 
 
 
 
 
 
 
 
 
 
351
  # Initialize state defaults at module load time
352
  _init_state_defaults()
 
11
  "enable_point_picking_on_plotter",
12
  "_apply_workflow_highlights", "_determine_workflow_step",
13
  "is_statevector_estimator_selected",
14
+ "is_ibm_qpu_selected",
15
  ]
16
 
17
 
 
267
  "qpu_plot_field_options": ["All"],
268
  "qpu_plot_position_filter": "All positions",
269
  "qpu_plot_position_options": ["All positions"],
270
+ # IBM QPU specific options (single field only - no "All")
271
+ "ibm_qpu_field_options": ["Ez", "Hx", "Hy"],
272
  # Additional QPU monitor slots
273
  "qpu_monitor_count": 0,
274
  "qpu_field_components_2": "Ez",
 
351
  return False
352
 
353
 
354
+ def is_ibm_qpu_selected() -> bool:
355
+ """Return True when the QPU dropdown targets the IBM QPU."""
356
+ try:
357
+ if state.backend_type != "QPU":
358
+ return False
359
+ return (state.selected_qpu or "").strip().lower() == "ibm qpu"
360
+ except AttributeError:
361
+ return False
362
+
363
+
364
  # Initialize state defaults at module load time
365
  _init_state_defaults()
em/ui.py CHANGED
@@ -353,7 +353,7 @@ def _build_meshing_card():
353
  density="compact",
354
  color="primary",
355
  ):
356
- vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : [16, 32, 64, 128, 256, 512][modelValue] }}"])
357
  # Hover content: enlarged Plotly graph
358
  with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 644px;"):
359
  qubit_fig_widget = plotly_widgets.Figure(
@@ -414,8 +414,48 @@ def _build_output_preferences_card():
414
  vuetify3.VTextField(v_bind="props", v_model=("dt_user", 0.1), label="Δt", type="number", step="0.1", density="compact", color="primary", classes="mt-1")
415
  vuetify3.VAlert(v_if="temporal_warning", type="warning", variant="tonal", density="compact", children=["{{ temporal_warning }}"], classes="mt-1")
416
 
417
- # QPU monitor options
418
- with vuetify3.VContainer(v_if="backend_type === 'QPU' || (backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator')", classes="pa-0 mt-2"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  with vuetify3.VRow(dense=True, classes="mb-1 align-center"):
420
  with vuetify3.VCol(cols=4, sm=3, md=3):
421
  vuetify3.VSelect(
 
353
  density="compact",
354
  color="primary",
355
  ):
356
+ vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : [8, 16, 32, 64, 128, 256, 512][modelValue] }}"])
357
  # Hover content: enlarged Plotly graph
358
  with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 644px;"):
359
  qubit_fig_widget = plotly_widgets.Figure(
 
414
  vuetify3.VTextField(v_bind="props", v_model=("dt_user", 0.1), label="Δt", type="number", step="0.1", density="compact", color="primary", classes="mt-1")
415
  vuetify3.VAlert(v_if="temporal_warning", type="warning", variant="tonal", density="compact", children=["{{ temporal_warning }}"], classes="mt-1")
416
 
417
+ # IBM QPU monitor options (single field, single position ONLY)
418
+ with vuetify3.VContainer(v_if="backend_type === 'QPU' && selected_qpu === 'IBM QPU'", classes="pa-0 mt-2"):
419
+ vuetify3.VAlert(
420
+ type="info",
421
+ variant="tonal",
422
+ density="compact",
423
+ children=["IBM QPU: Only ONE field component and ONE monitor position supported."],
424
+ classes="mb-2",
425
+ )
426
+ with vuetify3.VRow(dense=True, classes="mb-1 align-center"):
427
+ with vuetify3.VCol(cols=4, sm=3, md=3):
428
+ vuetify3.VSelect(
429
+ label="Field",
430
+ v_model=("qpu_field_components", "Ez"),
431
+ items=("ibm_qpu_field_options", ["Ez", "Hx", "Hy"]),
432
+ density="compact",
433
+ color="primary",
434
+ hide_details=True,
435
+ style="max-width: 160px;",
436
+ )
437
+ with vuetify3.VCol(cols=8, sm=9, md=9):
438
+ vuetify3.VTextField(
439
+ label="Monitor position (x, y) in [0,1]",
440
+ v_model=("qpu_monitor_samples", "(0.5, 0.5)"),
441
+ density="compact",
442
+ color="primary",
443
+ hide_details=True,
444
+ style="max-width: 320px;",
445
+ hint="Single position only for IBM QPU",
446
+ )
447
+ vuetify3.VAlert(
448
+ v_if="qpu_monitor_sample_info",
449
+ type="info",
450
+ variant="tonal",
451
+ density="compact",
452
+ children=["{{ qpu_monitor_sample_info }}"],
453
+ classes="mb-1",
454
+ style="white-space: pre-line;",
455
+ )
456
+
457
+ # Statevector Estimator monitor options (supports multiple fields and positions)
458
+ with vuetify3.VContainer(v_if="backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator'", classes="pa-0 mt-2"):
459
  with vuetify3.VRow(dense=True, classes="mb-1 align-center"):
460
  with vuetify3.VCol(cols=4, sm=3, md=3):
461
  vuetify3.VSelect(
requirements.txt CHANGED
@@ -37,6 +37,7 @@ trame_plotly
37
  Pillow==10.4.0
38
  packaging==25.0
39
  python-dateutil==2.9.0.post0
 
40
 
41
  # Export utilities
42
  imageio
 
37
  Pillow==10.4.0
38
  packaging==25.0
39
  python-dateutil==2.9.0.post0
40
+ pathlib
41
 
42
  # Export utilities
43
  imageio
utils/base_functions.py DELETED
@@ -1,443 +0,0 @@
1
- import numpy as np
2
- import scipy.sparse as sp
3
- import math
4
- import random
5
- import matplotlib.pyplot as plt
6
- from scipy.special import jn
7
- from scipy.sparse import identity, csr_matrix, kron, diags, eye
8
- from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
9
- from qiskit.circuit.library import MCXGate, MCPhaseGate, RXGate, CRXGate, QFTGate, StatePreparation, PauliEvolutionGate, RZGate
10
- from qiskit.quantum_info import SparsePauliOp, Statevector, Operator, Pauli
11
- from scipy.linalg import expm
12
- # from tools import *
13
- from qiskit.qasm3 import dumps # QASM 3 exporter
14
- from qiskit.qasm3 import loads
15
- from qiskit.circuit.library import QFT
16
- from qiskit.primitives import StatevectorEstimator
17
- from qiskit import transpile
18
- from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit, apply_circuit_to_state, compute_overlap
19
- from qiskit_aer import AerSimulator
20
-
21
-
22
- simulator_settings = AerSimulator(
23
- method="matrix_product_state",
24
- matrix_product_state_max_bond_dimension=100,
25
- )
26
-
27
- def Wj(j, theta, lam, name='Wj', xgate=False):
28
- if not xgate:
29
- name = f' $W_{j}$ '
30
- qc=QuantumCircuit(j, name=name)
31
-
32
- if j > 1:
33
- qc.cx(j-1, range(j-1))
34
- if lam != 0:
35
- qc.p(lam, j-1)
36
- qc.h(j-1)
37
- if xgate:
38
- qc.x(range(j-1))
39
-
40
- # the multicontrolled rz gate
41
- # it will be decomposed in qiskit
42
- if j > 1:
43
- qc.mcrz(theta, range(j-1), j-1)
44
- else:
45
- qc.rz(theta, j-1)
46
-
47
- if xgate:
48
- qc.x(range(j-1))
49
- qc.h(j-1)
50
- if lam != 0:
51
- qc.p(-lam, j-1)
52
- if j > 1:
53
- qc.cx(j-1, range(j-1))
54
-
55
- return qc
56
-
57
- def Wj_block(j, n, ctrl_state, theta, lam, name='Wj_block', xgate=False):
58
- if not xgate:
59
- name = f' $W_{j}_block$ '
60
- qc=QuantumCircuit(n + j, name=name)
61
-
62
- if j > 1:
63
- qc.cx(n + j-1, range(n, n+j-1))
64
- if lam != 0:
65
- qc.p(lam, n + j -1)
66
- qc.h(n + j -1)
67
-
68
- if xgate and j>1:
69
- if isinstance(xgate, (list, tuple)): # selective application
70
- for idx, flag in enumerate(xgate):
71
- if flag: # only apply where flag == 1
72
- qc.x(n + idx)
73
- elif xgate is True: # apply to all
74
- qc.x(range(n, n+j-1))
75
-
76
- # the multicontrolled rz gate
77
- # it will be decomposed in qiskit
78
- if j > 1:
79
- mcrz = RZGate(theta).control(len(ctrl_state) + j-1, ctrl_state = "1"*(j-1)+ctrl_state)
80
- qc.append(mcrz, range(0, n + j))
81
- else:
82
- mcrz = RZGate(theta).control(len(ctrl_state), ctrl_state = ctrl_state)
83
- qc.append(mcrz, range(0, n+j))
84
-
85
- if xgate and j>1:
86
- if isinstance(xgate, (list, tuple)): # selective application
87
- for idx, flag in enumerate(xgate):
88
- if flag: # only apply where flag == 1
89
- qc.x(n + idx)
90
- elif xgate is True: # apply to all
91
- qc.x(range(n, n+j-1))
92
-
93
- qc.h(n+ j-1)
94
- if lam != 0:
95
- qc.p(-lam, n + j-1)
96
- if j > 1:
97
- qc.cx(n + j-1, range(n, n +j-1))
98
-
99
- return qc.to_gate(label=name)
100
-
101
- def V1(nx, dt, name = "V1"):
102
- n = int(np.ceil(np.log2(nx)))
103
-
104
- derivatives = QuantumRegister(2*n)
105
- blocks = QuantumRegister(2)
106
-
107
- qc = QuantumCircuit(derivatives, blocks)
108
-
109
- W1 = Wj_block(2, n, "0"*n, -dt , 0, xgate=True)
110
- qc.append(W1, list(derivatives[0:n])+list(blocks[:]))
111
-
112
- # qc.barrier()
113
-
114
- W2 = Wj_block(3, n-1, "1"*(n-1), dt , 0, xgate=[0,1])
115
- qc.append(W2, list(derivatives[1:n])+[derivatives[0]]+list(blocks[:]))
116
-
117
- # qc.barrier()
118
-
119
- W3 = Wj_block(1, n+1, "0"*(n+1), dt , 0, xgate=False)
120
- qc.append(W3, list(derivatives[n:2*n])+list(blocks[:]))
121
-
122
- # qc.barrier()
123
-
124
- W4 = Wj_block(2, n, "0"+"1"*(n-1), -dt , 0, xgate=False)
125
- qc.append(W4, list(derivatives[n+1:2*n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
126
-
127
- return qc
128
-
129
- def V2(nx, dt, name = "V2"):
130
- n = int(np.ceil(np.log2(nx)))
131
-
132
- derivatives = QuantumRegister(2*n)
133
- blocks = QuantumRegister(2)
134
-
135
- qc = QuantumCircuit(derivatives, blocks)
136
-
137
- W1 = Wj_block(2, 0, "", -2*dt , -np.pi/2, xgate=True)
138
- qc.append(W1, list(blocks[:]))
139
-
140
- # qc.barrier()
141
-
142
- for j in range(1, n+1):
143
- W2 = Wj_block(2+j, 0, "", 2*dt , -np.pi/2, xgate=[1]*(j-1)+[0,1])
144
- qc.append(W2, list(derivatives[0:j])+list(blocks[:]))
145
-
146
- # qc.barrier()
147
-
148
- W3 = Wj_block(2, n, "0"*n, -dt , -np.pi/2, xgate=True)
149
- qc.append(W3, list(derivatives[0:n])+list(blocks[:]))
150
-
151
- # qc.barrier()
152
-
153
- W4 = Wj_block(2, n, "1"*n, 2*dt , -np.pi/2, xgate=True)
154
- qc.append(W4, list(derivatives[0:n])+list(blocks[:]))
155
-
156
- # qc.barrier()
157
-
158
- W5 = Wj_block(3, n-1, "1"*(n-1), dt , -np.pi/2, xgate=[0,1])
159
- qc.append(W5, list(derivatives[1:n])+[derivatives[0]]+list(blocks[:]))
160
-
161
- # qc.barrier()
162
-
163
- W6 = Wj_block(1, 1, "0", 2*dt , -np.pi/2, xgate=False)
164
- qc.append(W6, list(blocks[:]))
165
-
166
- # qc.barrier()
167
-
168
- for j in range(1, n+1):
169
- W7 = Wj_block(1+j, 1, "0", -2*dt , -np.pi/2, xgate=[1]*(j-1))
170
- qc.append(W7, [blocks[0]]+list(derivatives[n:n+j])+[blocks[1]])
171
-
172
- # qc.barrier()
173
-
174
- W8 = Wj_block(1, n+1, "0"*(n+1), dt , -np.pi/2, xgate=False)
175
- qc.append(W8, list(derivatives[n:2*n])+list(blocks[:]))
176
-
177
- # qc.barrier()
178
-
179
- W9 = Wj_block(1, n+1, "0"+"1"*(n), -2*dt , -np.pi/2, xgate=False)
180
- qc.append(W9, list(derivatives[n:2*n])+list(blocks[:]))
181
-
182
- # qc.barrier()
183
-
184
- W10 = Wj_block(2, n, "0"+"1"*(n-1), -dt , -np.pi/2, xgate=False)
185
- qc.append(W10, list(derivatives[n+1:2*n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
186
-
187
- # qc.barrier()
188
-
189
- return qc
190
-
191
- def schro(nx, na, R, dt, initial_state, steps):
192
-
193
- nq = int(np.ceil(np.log2(nx)))
194
-
195
- # warped phase transformation
196
- dp = 2 * R * np.pi / 2**na
197
- p = np.arange(- R * np.pi, R * np.pi, step=dp)
198
- fp = np.exp(-np.abs(p))
199
- norm1 = np.linalg.norm(fp[2**(na-1):]) # norm of p>=0
200
-
201
- # construct quantum circuit
202
- system = QuantumRegister(2*nq+2, name='system')
203
- ancilla = QuantumRegister(na, name='ancilla')
204
- qc = QuantumCircuit(system, ancilla)
205
-
206
- # initialization
207
- prep = StatePreparation(initial_state)
208
- anc_prep = StatePreparation(fp / np.linalg.norm(fp))
209
-
210
- qc.append(prep, system)
211
- # qc.append(anc_prep, ancilla)
212
- qc.initialize(fp / np.linalg.norm(fp), ancilla)
213
-
214
-
215
- # QFT
216
- qc.append(QFTGate(na), ancilla)
217
- qc.x(ancilla[-1])
218
-
219
- A1 = V1(nx, dt, name = "V1").to_gate()
220
- A2 = V2(nx, dt, name = "V2")
221
-
222
-
223
- # Hamiltonian simulation for Nt steps
224
- for i in range(steps):
225
- # circuit for one step
226
- for j in range(na):
227
- # repeat controlled H1 for 2**j times
228
- qc.append(A1.control().repeat(2**j), [ancilla[j]] + system[:])
229
-
230
- # qc.append(A1.inverse().control(ctrl_state = "0").repeat(2**(na-1)), [ancilla[na-1]] + system[:])
231
- qc.append(A1.inverse().repeat(2**(na-1)), system[:])
232
- qc.append(A2, system[:])
233
-
234
- # rearrange eta
235
- qc.x(ancilla[-1])
236
- qc.append(QFTGate(na).inverse(), ancilla)
237
-
238
- return qc
239
-
240
-
241
-
242
- def circ_for_magnitude(field, x, y, nx, na, R, dt, initial_state, steps):
243
-
244
- qc = schro(nx, na, R, dt, initial_state, steps)
245
- naimark = QuantumRegister(1, name='Naimark')
246
- qc.add_register(naimark)
247
-
248
- if field == 'Ez':
249
- index = nx * y + x
250
- elif field == 'Hx':
251
- index = 2*nx*nx + nx * y + x
252
- else:
253
- index = 3*nx*nx + nx * y + x
254
-
255
- index_bin = format(index, f'0{qc.num_qubits-2}b')
256
- ctrl_state = '1' + index_bin
257
- ctrl_qubits = qc.qubits[:-1]
258
- qc.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state)
259
-
260
- return qc
261
-
262
- def circuits_for_sign(field, x, y, nx, na, dt, R, initial_state, steps, xref, yref, field_ref = 'Ez'):
263
- qc = schro(nx, na, R, dt, initial_state, steps)
264
-
265
- naimark = QuantumRegister(1, name='Naimark')
266
- qc.add_register(naimark)
267
-
268
- if field == 'Ez':
269
- index = nx * y + x
270
- elif field == 'Hx':
271
- index = 2*nx*nx + nx * y + x
272
- else:
273
- index = 3*nx*nx + nx * y + x
274
-
275
- if field_ref == 'Ez':
276
- index_ref = nx * yref + xref
277
- elif field_ref == 'Hx':
278
- index_ref = 2*nx*nx + nx * yref + xref
279
- else:
280
- index_ref = 3*nx*nx + nx * yref + xref
281
-
282
- index_bin = [(index >> i) & 1 for i in range(qc.num_qubits-2)]
283
- index_ref_bin = [(index_ref >> i) & 1 for i in range(qc.num_qubits-2)]
284
- index_bin.append(1)
285
- index_ref_bin.append(1)
286
-
287
- #Convert reference bitstring to 00000
288
- for i, bit in enumerate(index_ref_bin):
289
- if bit == 1:
290
- qc.x(i)
291
-
292
- d_bits = [b ^ r for b, r in zip(index_ref_bin, index_bin)]
293
- control = d_bits.index(1)
294
-
295
- #Convert the other bitstring to 0001000
296
- for target, bit in enumerate(d_bits):
297
- if bit == 1 and target != control:
298
- qc.cx(control, target)
299
- qc.h(control)
300
-
301
- ctrl_state_sum = '0'*(qc.num_qubits-1)
302
- ctrl_state_diff = '0'*(qc.num_qubits-1-control-1)+'1'+'0'*(control)
303
-
304
- qcdiff = qc.copy()
305
-
306
- ctrl_qubits = qc.qubits[:-1]
307
-
308
- qc.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state_sum)
309
- qcdiff.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state_diff)
310
-
311
- return qc, qcdiff
312
-
313
- def get_absolute_field_value(qc, nq, na, offset, norm):
314
-
315
- pauli_label = 'Z'+'I'*(2*nq+2+na)
316
- observable = SparsePauliOp(Pauli(pauli_label))
317
- ########################################################################################
318
- estimator = StatevectorEstimator()
319
-
320
- # === Run Estimator (no parameters needed) ===
321
- pub = (qc, observable)
322
- job = estimator.run([pub])
323
- result = job.result()[0]
324
- z_exp = result.data.evs.item()
325
- #########################################################################################
326
- # === Compute projector expectation ===
327
- pi_expect = (1 - z_exp) / 2
328
-
329
- Absolute_value = norm*np.sqrt(pi_expect)-offset
330
-
331
- return Absolute_value
332
-
333
- def get_relative_sign(qc, qcdiff, nq, na):
334
-
335
- pauli_label = 'Z'+'I'*(2*nq+2+na)
336
- observable = SparsePauliOp(Pauli(pauli_label))
337
- ########################################################################################
338
- estimator = StatevectorEstimator()
339
-
340
- # === Run Estimator ===
341
- pub = (qc, observable)
342
- job = estimator.run([pub])
343
- result = job.result()[0]
344
- z_exp = result.data.evs.item()
345
-
346
- pub_diff = (qcdiff, observable)
347
- job_diff = estimator.run([pub_diff])
348
- result_diff = job_diff.result()[0]
349
- z_exp_diff = result_diff.data.evs.item()
350
- #########################################################################################
351
- # === Compute projector expectation ===
352
- pi_expect_sum = (1 - z_exp) / 2
353
- pi_expect_diff = (1 - z_exp_diff) / 2
354
-
355
- relative_sign = 'same' if pi_expect_sum >= pi_expect_diff else 'different'
356
-
357
- return relative_sign
358
-
359
- def Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref = 'Ez'):
360
- if steps < 31:
361
- offset = 1
362
- else :
363
- offset = 0.15
364
- deltastate = np.zeros(4*nx*nx)
365
- # deltastate[nx*nx//2+nx//2:nx*nx//2+nx//2+1] = 1
366
- deltastate[nx*yref+xref] = 1
367
- deltastate[0:nx*nx] = deltastate[0:nx*nx] + offset
368
- norm1 = np.linalg.norm(deltastate)
369
- initial_state = deltastate/norm1
370
-
371
- dp = 2 * R * np.pi / 2**na
372
- p = np.arange(- R * np.pi, R * np.pi, step=dp)
373
- fp = np.exp(-np.abs(p))
374
- norm2 = np.linalg.norm(fp)
375
- norm = norm1 * norm2
376
-
377
- qc = circ_for_magnitude(field_ref, xref, yref, nx, na, R, dt, initial_state, steps)
378
-
379
- Ezref = get_absolute_field_value(qc, nq, na, offset, norm)
380
-
381
- return Ezref
382
-
383
-
384
- def transpile_circ(circ, basis_gates=None):
385
- """
386
- Transpile the circuit to the specified basis gates.
387
- """
388
- if basis_gates is None:
389
- basis_gates = ['z', 'y', 'x', 'sdg', 's', 'h', 'rz', 'ry', 'rx', 'ecr', 'cz', 'cx']
390
-
391
- transpiled_circ = transpile(circ, basis_gates=basis_gates)
392
- return transpiled_circ
393
-
394
- def compute_fidelity(circ1, circ2):
395
-
396
- circ_1 = tensornetwork_from_circuit(transpile_circ(circ1), simulator_settings)
397
- circ_2 = tensornetwork_from_circuit(transpile_circ(circ2), simulator_settings)
398
- fidelity = abs(compute_overlap(circ_1, circ_2))**2
399
-
400
- return fidelity
401
-
402
- # def create_impulse_state(grid_dims, impulse_pos):
403
- # """
404
- # Creates an initial state vector with a single delta impulse at a specified grid position.
405
-
406
- # The 2D grid is flattened into a 1D vector in row-major order, and this
407
- # vector is then padded to match the full simulation state space size (4x).
408
-
409
- # Args:
410
- # grid_dims (tuple): A tuple (width, height) defining the simulation grid dimensions.
411
- # For your original code, this would be (nx, nx).
412
- # impulse_pos (tuple): A tuple (x, y) for the position of the impulse.
413
- # Coordinates are 0-indexed.
414
-
415
- # Returns:
416
- # numpy.ndarray: The full, padded initial state vector with a single 1.
417
-
418
- # Raises:
419
- # ValueError: If the impulse position is outside the grid dimensions.
420
- # """
421
- # grid_width, grid_height = grid_dims
422
- # impulse_x, impulse_y = impulse_pos
423
-
424
- # # --- Input Validation ---
425
- # # Ensure the requested impulse position is actually on the grid.
426
- # if not (0 <= impulse_x < grid_width and 0 <= impulse_y < grid_height):
427
- # raise ValueError(f"Impulse position ({impulse_x}, {impulse_y}) is outside the "
428
- # f"grid dimensions ({grid_width}x{grid_height}).")
429
-
430
- # # --- 1. Calculate the 1D Array Index ---
431
- # # Convert the (x, y) coordinate to a single index in a flattened 1D array.
432
- # # The formula for row-major order is: index = y_coord * width + x_coord
433
- # flat_index = impulse_y * grid_width + impulse_x
434
-
435
- # # --- 2. Create the Full, Padded State Vector ---
436
- # grid_size = grid_width * grid_height
437
- # total_size = 4 * grid_size # The simulation space is 4x the grid size.
438
- # initial_state = np.zeros(total_size)
439
-
440
- # # --- 3. Set the Delta Impulse ---
441
- # initial_state[flat_index] = 1
442
-
443
- # return initial_state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/base_ionq.py DELETED
@@ -1,458 +0,0 @@
1
- import numpy as np
2
- import scipy.sparse as sp
3
- import math
4
- import random
5
- import matplotlib.pyplot as plt
6
- from scipy.special import jn
7
- from scipy.sparse import identity, csr_matrix, kron, diags, eye
8
- from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
9
- from qiskit.circuit.library import MCXGate, MCPhaseGate, RXGate, CRXGate, QFTGate, StatePreparation, PauliEvolutionGate, RZGate
10
- from qiskit.quantum_info import SparsePauliOp, Statevector, Operator, Pauli
11
- from scipy.linalg import expm
12
- # from tools import *
13
- from qiskit.qasm3 import dumps # QASM 3 exporter
14
- from qiskit.qasm3 import loads
15
- from qiskit.circuit.library import QFT
16
- from qiskit.primitives import StatevectorEstimator
17
- from qiskit import transpile
18
- from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit, apply_circuit_to_state, compute_overlap
19
- from qiskit_aer import AerSimulator
20
- from qiskit_ionq import IonQProvider
21
- import os
22
- my_api_key = os.getenv("IONQ_API_KEY")
23
- from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator
24
-
25
-
26
-
27
-
28
- # provider = IonQProvider()
29
-
30
- api_token = "SgUkiDq1r2bVEadyiUfvtuxQ03Qci6UW"
31
- provider = IonQProvider(api_token)
32
- ionq_backend = provider.get_backend("simulator")
33
-
34
-
35
-
36
-
37
- simulator_settings = AerSimulator(
38
- method="matrix_product_state",
39
- matrix_product_state_max_bond_dimension=100,
40
- )
41
-
42
- def Wj(j, theta, lam, name='Wj', xgate=False):
43
- if not xgate:
44
- name = f' $W_{j}$ '
45
- qc=QuantumCircuit(j, name=name)
46
-
47
- if j > 1:
48
- qc.cx(j-1, range(j-1))
49
- if lam != 0:
50
- qc.p(lam, j-1)
51
- qc.h(j-1)
52
- if xgate:
53
- qc.x(range(j-1))
54
-
55
- # the multicontrolled rz gate
56
- # it will be decomposed in qiskit
57
- if j > 1:
58
- qc.mcrz(theta, range(j-1), j-1)
59
- else:
60
- qc.rz(theta, j-1)
61
-
62
- if xgate:
63
- qc.x(range(j-1))
64
- qc.h(j-1)
65
- if lam != 0:
66
- qc.p(-lam, j-1)
67
- if j > 1:
68
- qc.cx(j-1, range(j-1))
69
-
70
- return qc
71
-
72
- def Wj_block(j, n, ctrl_state, theta, lam, name='Wj_block', xgate=False):
73
- if not xgate:
74
- name = f' $W_{j}_block$ '
75
- qc=QuantumCircuit(n + j, name=name)
76
-
77
- if j > 1:
78
- qc.cx(n + j-1, range(n, n+j-1))
79
- if lam != 0:
80
- qc.p(lam, n + j -1)
81
- qc.h(n + j -1)
82
-
83
- if xgate and j>1:
84
- if isinstance(xgate, (list, tuple)): # selective application
85
- for idx, flag in enumerate(xgate):
86
- if flag: # only apply where flag == 1
87
- qc.x(n + idx)
88
- elif xgate is True: # apply to all
89
- qc.x(range(n, n+j-1))
90
-
91
- # the multicontrolled rz gate
92
- # it will be decomposed in qiskit
93
- if j > 1:
94
- mcrz = RZGate(theta).control(len(ctrl_state) + j-1, ctrl_state = "1"*(j-1)+ctrl_state)
95
- qc.append(mcrz, range(0, n + j))
96
- else:
97
- mcrz = RZGate(theta).control(len(ctrl_state), ctrl_state = ctrl_state)
98
- qc.append(mcrz, range(0, n+j))
99
-
100
- if xgate and j>1:
101
- if isinstance(xgate, (list, tuple)): # selective application
102
- for idx, flag in enumerate(xgate):
103
- if flag: # only apply where flag == 1
104
- qc.x(n + idx)
105
- elif xgate is True: # apply to all
106
- qc.x(range(n, n+j-1))
107
-
108
- qc.h(n+ j-1)
109
- if lam != 0:
110
- qc.p(-lam, n + j-1)
111
- if j > 1:
112
- qc.cx(n + j-1, range(n, n +j-1))
113
-
114
- return qc.to_gate(label=name)
115
-
116
- def V1(nx, dt, name = "V1"):
117
- n = int(np.ceil(np.log2(nx)))
118
-
119
- derivatives = QuantumRegister(2*n)
120
- blocks = QuantumRegister(2)
121
-
122
- qc = QuantumCircuit(derivatives, blocks)
123
-
124
- W1 = Wj_block(2, n, "0"*n, -dt , 0, xgate=True)
125
- qc.append(W1, list(derivatives[0:n])+list(blocks[:]))
126
-
127
- # qc.barrier()
128
-
129
- W2 = Wj_block(3, n-1, "1"*(n-1), dt , 0, xgate=[0,1])
130
- qc.append(W2, list(derivatives[1:n])+[derivatives[0]]+list(blocks[:]))
131
-
132
- # qc.barrier()
133
-
134
- W3 = Wj_block(1, n+1, "0"*(n+1), dt , 0, xgate=False)
135
- qc.append(W3, list(derivatives[n:2*n])+list(blocks[:]))
136
-
137
- # qc.barrier()
138
-
139
- W4 = Wj_block(2, n, "0"+"1"*(n-1), -dt , 0, xgate=False)
140
- qc.append(W4, list(derivatives[n+1:2*n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
141
-
142
- return qc
143
-
144
- def V2(nx, dt, name = "V2"):
145
- n = int(np.ceil(np.log2(nx)))
146
-
147
- derivatives = QuantumRegister(2*n)
148
- blocks = QuantumRegister(2)
149
-
150
- qc = QuantumCircuit(derivatives, blocks)
151
-
152
- W1 = Wj_block(2, 0, "", -2*dt , -np.pi/2, xgate=True)
153
- qc.append(W1, list(blocks[:]))
154
-
155
- # qc.barrier()
156
-
157
- for j in range(1, n+1):
158
- W2 = Wj_block(2+j, 0, "", 2*dt , -np.pi/2, xgate=[1]*(j-1)+[0,1])
159
- qc.append(W2, list(derivatives[0:j])+list(blocks[:]))
160
-
161
- # qc.barrier()
162
-
163
- W3 = Wj_block(2, n, "0"*n, -dt , -np.pi/2, xgate=True)
164
- qc.append(W3, list(derivatives[0:n])+list(blocks[:]))
165
-
166
- # qc.barrier()
167
-
168
- W4 = Wj_block(2, n, "1"*n, 2*dt , -np.pi/2, xgate=True)
169
- qc.append(W4, list(derivatives[0:n])+list(blocks[:]))
170
-
171
- # qc.barrier()
172
-
173
- W5 = Wj_block(3, n-1, "1"*(n-1), dt , -np.pi/2, xgate=[0,1])
174
- qc.append(W5, list(derivatives[1:n])+[derivatives[0]]+list(blocks[:]))
175
-
176
- # qc.barrier()
177
-
178
- W6 = Wj_block(1, 1, "0", 2*dt , -np.pi/2, xgate=False)
179
- qc.append(W6, list(blocks[:]))
180
-
181
- # qc.barrier()
182
-
183
- for j in range(1, n+1):
184
- W7 = Wj_block(1+j, 1, "0", -2*dt , -np.pi/2, xgate=[1]*(j-1))
185
- qc.append(W7, [blocks[0]]+list(derivatives[n:n+j])+[blocks[1]])
186
-
187
- # qc.barrier()
188
-
189
- W8 = Wj_block(1, n+1, "0"*(n+1), dt , -np.pi/2, xgate=False)
190
- qc.append(W8, list(derivatives[n:2*n])+list(blocks[:]))
191
-
192
- # qc.barrier()
193
-
194
- W9 = Wj_block(1, n+1, "0"+"1"*(n), -2*dt , -np.pi/2, xgate=False)
195
- qc.append(W9, list(derivatives[n:2*n])+list(blocks[:]))
196
-
197
- # qc.barrier()
198
-
199
- W10 = Wj_block(2, n, "0"+"1"*(n-1), -dt , -np.pi/2, xgate=False)
200
- qc.append(W10, list(derivatives[n+1:2*n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
201
-
202
- # qc.barrier()
203
-
204
- return qc
205
-
206
- def schro(nx, na, R, dt, initial_state, steps):
207
-
208
- nq = int(np.ceil(np.log2(nx)))
209
-
210
- # warped phase transformation
211
- dp = 2 * R * np.pi / 2**na
212
- p = np.arange(- R * np.pi, R * np.pi, step=dp)
213
- fp = np.exp(-np.abs(p))
214
- norm1 = np.linalg.norm(fp[2**(na-1):]) # norm of p>=0
215
-
216
- # construct quantum circuit
217
- system = QuantumRegister(2*nq+2, name='system')
218
- ancilla = QuantumRegister(na, name='ancilla')
219
- qc = QuantumCircuit(system, ancilla)
220
-
221
- # initialization
222
- prep = StatePreparation(initial_state)
223
- anc_prep = StatePreparation(fp / np.linalg.norm(fp))
224
-
225
- qc.append(prep, system)
226
- # qc.append(anc_prep, ancilla)
227
- qc.initialize(fp / np.linalg.norm(fp), ancilla)
228
-
229
-
230
- # QFT
231
- qc.append(QFTGate(na), ancilla)
232
- qc.x(ancilla[-1])
233
-
234
- A1 = V1(nx, dt, name = "V1").to_gate()
235
- A2 = V2(nx, dt, name = "V2")
236
-
237
-
238
- # Hamiltonian simulation for Nt steps
239
- for i in range(steps):
240
- # circuit for one step
241
- for j in range(na):
242
- # repeat controlled H1 for 2**j times
243
- qc.append(A1.control().repeat(2**j), [ancilla[j]] + system[:])
244
-
245
- # qc.append(A1.inverse().control(ctrl_state = "0").repeat(2**(na-1)), [ancilla[na-1]] + system[:])
246
- qc.append(A1.inverse().repeat(2**(na-1)), system[:])
247
- qc.append(A2, system[:])
248
-
249
- # rearrange eta
250
- qc.x(ancilla[-1])
251
- qc.append(QFTGate(na).inverse(), ancilla)
252
-
253
- return qc
254
-
255
-
256
-
257
- def circ_for_magnitude(field, x, y, nx, na, R, dt, initial_state, steps):
258
-
259
- qc = schro(nx, na, R, dt, initial_state, steps)
260
- naimark = QuantumRegister(1, name='Naimark')
261
- qc.add_register(naimark)
262
-
263
- if field == 'Ez':
264
- index = nx * y + x
265
- elif field == 'Hx':
266
- index = 2*nx*nx + nx * y + x
267
- else:
268
- index = 3*nx*nx + nx * y + x
269
-
270
- index_bin = format(index, f'0{qc.num_qubits-2}b')
271
- ctrl_state = '1' + index_bin
272
- ctrl_qubits = qc.qubits[:-1]
273
- qc.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state)
274
-
275
- return qc
276
-
277
- def circuits_for_sign(field, x, y, nx, na, dt, R, initial_state, steps, xref, yref, field_ref = 'Ez'):
278
- qc = schro(nx, na, R, dt, initial_state, steps)
279
-
280
- naimark = QuantumRegister(1, name='Naimark')
281
- qc.add_register(naimark)
282
-
283
- if field == 'Ez':
284
- index = nx * y + x
285
- elif field == 'Hx':
286
- index = 2*nx*nx + nx * y + x
287
- else:
288
- index = 3*nx*nx + nx * y + x
289
-
290
- if field_ref == 'Ez':
291
- index_ref = nx * yref + xref
292
- elif field_ref == 'Hx':
293
- index_ref = 2*nx*nx + nx * yref + xref
294
- else:
295
- index_ref = 3*nx*nx + nx * yref + xref
296
-
297
- index_bin = [(index >> i) & 1 for i in range(qc.num_qubits-2)]
298
- index_ref_bin = [(index_ref >> i) & 1 for i in range(qc.num_qubits-2)]
299
- index_bin.append(1)
300
- index_ref_bin.append(1)
301
-
302
- #Convert reference bitstring to 00000
303
- for i, bit in enumerate(index_ref_bin):
304
- if bit == 1:
305
- qc.x(i)
306
-
307
- d_bits = [b ^ r for b, r in zip(index_ref_bin, index_bin)]
308
- control = d_bits.index(1)
309
-
310
- #Convert the other bitstring to 0001000
311
- for target, bit in enumerate(d_bits):
312
- if bit == 1 and target != control:
313
- qc.cx(control, target)
314
- qc.h(control)
315
-
316
- ctrl_state_sum = '0'*(qc.num_qubits-1)
317
- ctrl_state_diff = '0'*(qc.num_qubits-1-control-1)+'1'+'0'*(control)
318
-
319
- qcdiff = qc.copy()
320
-
321
- ctrl_qubits = qc.qubits[:-1]
322
-
323
- qc.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state_sum)
324
- qcdiff.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state_diff)
325
-
326
- return qc, qcdiff
327
-
328
- def get_absolute_field_value(qc, nq, na, offset, norm):
329
-
330
- pauli_label = 'Z'+'I'*(2*nq+2+na)
331
- observable = SparsePauliOp(Pauli(pauli_label))
332
- ########################################################################################
333
- estimator = StatevectorEstimator()
334
-
335
- # === Run Estimator (no parameters needed) ===
336
- pub = (qc, observable)
337
- job = estimator.run([pub])
338
- result = job.result()[0]
339
- z_exp = result.data.evs.item()
340
- #########################################################################################
341
- # === Compute projector expectation ===
342
- pi_expect = (1 - z_exp) / 2
343
-
344
- Absolute_value = norm*np.sqrt(pi_expect)-offset
345
-
346
- return Absolute_value
347
-
348
- def get_relative_sign(qc, qcdiff, nq, na):
349
-
350
- pauli_label = 'Z'+'I'*(2*nq+2+na)
351
- observable = SparsePauliOp(Pauli(pauli_label))
352
- ########################################################################################
353
- estimator = StatevectorEstimator()
354
-
355
- # === Run Estimator ===
356
- pub = (qc, observable)
357
- job = estimator.run([pub])
358
- result = job.result()[0]
359
- z_exp = result.data.evs.item()
360
-
361
- pub_diff = (qcdiff, observable)
362
- job_diff = estimator.run([pub_diff])
363
- result_diff = job_diff.result()[0]
364
- z_exp_diff = result_diff.data.evs.item()
365
- #########################################################################################
366
- # === Compute projector expectation ===
367
- pi_expect_sum = (1 - z_exp) / 2
368
- pi_expect_diff = (1 - z_exp_diff) / 2
369
-
370
- relative_sign = 'same' if pi_expect_sum >= pi_expect_diff else 'different'
371
-
372
- return relative_sign
373
-
374
- def Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref = 'Ez'):
375
- if steps < 31:
376
- offset = 1
377
- else :
378
- offset = 0.15
379
- deltastate = np.zeros(4*nx*nx)
380
- # deltastate[nx*nx//2+nx//2:nx*nx//2+nx//2+1] = 1
381
- deltastate[nx*yref+xref] = 1
382
- deltastate[0:nx*nx] = deltastate[0:nx*nx] + offset
383
- norm1 = np.linalg.norm(deltastate)
384
- initial_state = deltastate/norm1
385
-
386
- dp = 2 * R * np.pi / 2**na
387
- p = np.arange(- R * np.pi, R * np.pi, step=dp)
388
- fp = np.exp(-np.abs(p))
389
- norm2 = np.linalg.norm(fp)
390
- norm = norm1 * norm2
391
-
392
- qc = circ_for_magnitude(field_ref, xref, yref, nx, na, R, dt, initial_state, steps)
393
-
394
- Ezref = get_absolute_field_value(qc, nq, na, offset, norm)
395
-
396
- return Ezref
397
-
398
-
399
- def transpile_circ(circ, basis_gates=None):
400
- """
401
- Transpile the circuit to the specified basis gates.
402
- """
403
- if basis_gates is None:
404
- basis_gates = ['z', 'y', 'x', 'sdg', 's', 'h', 'rz', 'ry', 'rx', 'ecr', 'cz', 'cx']
405
-
406
- transpiled_circ = transpile(circ, basis_gates=basis_gates)
407
- return transpiled_circ
408
-
409
- def compute_fidelity(circ1, circ2):
410
-
411
- circ_1 = tensornetwork_from_circuit(transpile_circ(circ1), simulator_settings)
412
- circ_2 = tensornetwork_from_circuit(transpile_circ(circ2), simulator_settings)
413
- fidelity = abs(compute_overlap(circ_1, circ_2))**2
414
-
415
- return fidelity
416
-
417
- # def create_impulse_state(grid_dims, impulse_pos):
418
- # """
419
- # Creates an initial state vector with a single delta impulse at a specified grid position.
420
-
421
- # The 2D grid is flattened into a 1D vector in row-major order, and this
422
- # vector is then padded to match the full simulation state space size (4x).
423
-
424
- # Args:
425
- # grid_dims (tuple): A tuple (width, height) defining the simulation grid dimensions.
426
- # For your original code, this would be (nx, nx).
427
- # impulse_pos (tuple): A tuple (x, y) for the position of the impulse.
428
- # Coordinates are 0-indexed.
429
-
430
- # Returns:
431
- # numpy.ndarray: The full, padded initial state vector with a single 1.
432
-
433
- # Raises:
434
- # ValueError: If the impulse position is outside the grid dimensions.
435
- # """
436
- # grid_width, grid_height = grid_dims
437
- # impulse_x, impulse_y = impulse_pos
438
-
439
- # # --- Input Validation ---
440
- # # Ensure the requested impulse position is actually on the grid.
441
- # if not (0 <= impulse_x < grid_width and 0 <= impulse_y < grid_height):
442
- # raise ValueError(f"Impulse position ({impulse_x}, {impulse_y}) is outside the "
443
- # f"grid dimensions ({grid_width}x{grid_height}).")
444
-
445
- # # --- 1. Calculate the 1D Array Index ---
446
- # # Convert the (x, y) coordinate to a single index in a flattened 1D array.
447
- # # The formula for row-major order is: index = y_coord * width + x_coord
448
- # flat_index = impulse_y * grid_width + impulse_x
449
-
450
- # # --- 2. Create the Full, Padded State Vector ---
451
- # grid_size = grid_width * grid_height
452
- # total_size = 4 * grid_size # The simulation space is 4x the grid size.
453
- # initial_state = np.zeros(total_size)
454
-
455
- # # --- 3. Set the Delta Impulse ---
456
- # initial_state[flat_index] = 1
457
-
458
- # return initial_state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/delta_impulse_generator.py DELETED
@@ -1,493 +0,0 @@
1
- import numpy as np
2
- import math
3
- from qiskit.circuit import QuantumCircuit, QuantumRegister
4
- from qiskit.circuit.library import StatePreparation, QFTGate, RZGate
5
- from qiskit.quantum_info import Statevector
6
- import pyvista as pv
7
-
8
- def create_impulse_state(grid_dims, impulse_pos):
9
- """
10
- Creates an initial state vector with a single delta impulse at a specified grid position.
11
-
12
- The 2D grid is flattened into a 1D vector in row-major order, and this
13
- vector is then padded to match the full simulation state space size (4x).
14
-
15
- Args:
16
- grid_dims (tuple): A tuple (width, height) defining the simulation grid dimensions.
17
- For your original code, this would be (nx, nx).
18
- impulse_pos (tuple): A tuple (x, y) for the position of the impulse.
19
- Coordinates are 0-indexed.
20
-
21
- Returns:
22
- numpy.ndarray: The full, padded initial state vector with a single 1.
23
-
24
- Raises:
25
- ValueError: If the impulse position is outside the grid dimensions.
26
- """
27
- grid_width, grid_height = grid_dims
28
- impulse_x, impulse_y = impulse_pos
29
-
30
- # --- Input Validation ---
31
- # Ensure the requested impulse position is actually on the grid.
32
- if not (0 <= impulse_x < grid_width and 0 <= impulse_y < grid_height):
33
- raise ValueError(f"Impulse position ({impulse_x}, {impulse_y}) is outside the "
34
- f"grid dimensions ({grid_width}x{grid_height}).")
35
-
36
- # --- 1. Calculate the 1D Array Index ---
37
- # Convert the (x, y) coordinate to a single index in a flattened 1D array.
38
- # The formula for row-major order is: index = y_coord * width + x_coord
39
- flat_index = impulse_y * grid_width + impulse_x
40
-
41
- # --- 2. Create the Full, Padded State Vector ---
42
- grid_size = grid_width * grid_height
43
- total_size = 4 * grid_size # The simulation space is 4x the grid size.
44
- initial_state = np.zeros(total_size)
45
-
46
- # --- 3. Set the Delta Impulse ---
47
- initial_state[flat_index] = 1
48
-
49
- return initial_state
50
-
51
- def create_gaussian_state(grid_dims, mu, sigma):
52
- """
53
- Creates an initial state vector with a 2D Gaussian distribution.
54
-
55
- The state is normalized and padded to match the full simulation state space size (4x).
56
-
57
- Args:
58
- grid_dims (tuple): A tuple (width, height) defining the grid dimensions.
59
- mu (tuple): A tuple (mu_x, mu_y) for the center (mean) of the Gaussian.
60
- sigma (tuple): A tuple (sigma_x, sigma_y) for the standard deviation (spread).
61
-
62
- Returns:
63
- numpy.ndarray: The full, padded initial state vector for the Gaussian state.
64
-
65
- Raises:
66
- ValueError: If sigma values are not positive.
67
- """
68
- grid_width, grid_height = grid_dims
69
- mu_x, mu_y = mu
70
- sigma_x, sigma_y = sigma
71
-
72
- if sigma_x <= 0 or sigma_y <= 0:
73
- raise ValueError("Sigma values (spread) must be positive.")
74
-
75
- # --- 1. Create a Coordinate Grid ---
76
- x = np.arange(0, grid_width)
77
- y = np.arange(0, grid_height)
78
- X, Y = np.meshgrid(x, y)
79
-
80
- # --- 2. Calculate the 2D Gaussian Function ---
81
- gaussian_2d = np.exp(-((X - mu_x)**2 / (2 * sigma_x**2)) -
82
- ((Y - mu_y)**2 / (2 * sigma_y**2)))
83
-
84
- # --- 3. Normalize the State Vector ---
85
- # For a valid quantum state, the L2 norm (sum of squares of amplitudes) must be 1.
86
- norm = np.linalg.norm(gaussian_2d)
87
- if norm > 0:
88
- gaussian_2d = gaussian_2d / norm
89
-
90
- # --- 4. Flatten and Pad the Vector ---
91
- gaussian_flat = gaussian_2d.flatten()
92
- grid_size = grid_width * grid_height
93
- total_size = 4 * grid_size
94
- initial_state = np.pad(gaussian_flat, (0, total_size - grid_size), mode='constant')
95
-
96
- return initial_state
97
-
98
-
99
-
100
-
101
-
102
- # --- New: Continuous-position helpers for excitation before meshing ---
103
- def _normalize_to_unit(vec: np.ndarray) -> np.ndarray:
104
- n = np.linalg.norm(vec)
105
- return vec / n if n > 0 else vec
106
-
107
-
108
-
109
-
110
- def create_impulse_state_from_pos(grid_dims, pos01):
111
- """
112
- Create a delta-like initial state from continuous position pos01=(x,y) in [0,1].
113
-
114
- Why grid_dims?
115
- - Simulation runs on a discrete nx×ny lattice; the continuous position must be
116
- discretized onto that grid to produce the state vector fed into the solver.
117
- - grid_dims provides (nx, ny) so we can map (x,y)∈[0,1]→grid coordinates via
118
- gx = x*(nx-1), gy = y*(ny-1), then distribute amplitude bilinearly to the 4
119
- neighboring nodes. This is required only for the simulation state, not the preview.
120
-
121
- The preview uses create_impulse_preview_state(), which renders a smooth bump on a
122
- fixed unit-square grid independent of nx for visualization.
123
- """
124
- grid_width, grid_height = grid_dims
125
- px, py = pos01
126
- px = float(max(0.0, min(1.0, px)))
127
- py = float(max(0.0, min(1.0, py)))
128
-
129
- gx = px * (grid_width - 1)
130
- gy = py * (grid_height - 1)
131
- i0, j0 = int(np.floor(gx)), int(np.floor(gy))
132
- i1, j1 = min(i0 + 1, grid_width - 1), min(j0 + 1, grid_height - 1)
133
- dx, dy = gx - i0, gy - j0
134
-
135
- w00 = (1 - dx) * (1 - dy)
136
- w10 = dx * (1 - dy)
137
- w01 = (1 - dx) * dy
138
- w11 = dx * dy
139
-
140
- grid_size = grid_width * grid_height
141
- total_size = 4 * grid_size
142
- field = np.zeros(grid_size)
143
- field[j0 * grid_width + i0] += w00
144
- field[j0 * grid_width + i1] += w10
145
- field[j1 * grid_width + i0] += w01
146
- field[j1 * grid_width + i1] += w11
147
- field = _normalize_to_unit(field)
148
-
149
- initial_state = np.zeros(total_size)
150
- initial_state[:grid_size] = field
151
- return initial_state
152
-
153
-
154
- def create_gaussian_state_from_pos(grid_dims, mu01, sigma01):
155
- """
156
- Create a Gaussian initial state with center mu01=(x,y) and spreads sigma01=(sx,sy)
157
- in [0,1] of the domain, then discretize to the solver grid given by grid_dims.
158
-
159
- Why grid_dims?
160
- - The quantum solver expects a vector aligned to the chosen nx×ny simulation grid.
161
- We convert normalized μ and σ (fractions of the domain) into grid units using
162
- (nx-1) and (ny-1). This step is necessary for the simulation, not for the preview.
163
-
164
- For preview-only rendering, use create_impulse_preview_state() to keep the visuals
165
- continuous and independent of nx.
166
- """
167
- grid_width, grid_height = grid_dims
168
- mu_x01, mu_y01 = mu01
169
- sig_x01, sig_y01 = sigma01
170
-
171
- mu_x01 = float(max(0.0, min(1.0, mu_x01)))
172
- mu_y01 = float(max(0.0, min(1.0, mu_y01)))
173
- sig_x01 = float(sig_x01)
174
- sig_y01 = float(sig_y01)
175
- if sig_x01 <= 0 or sig_y01 <= 0:
176
- raise ValueError("Sigma values (spread) must be positive.")
177
-
178
- mu_x = mu_x01 * (grid_width - 1)
179
- mu_y = mu_y01 * (grid_height - 1)
180
- sigma_x = sig_x01 * (grid_width - 1)
181
- sigma_y = sig_y01 * (grid_height - 1)
182
-
183
- x = np.arange(0, grid_width)
184
- y = np.arange(0, grid_height)
185
- X, Y = np.meshgrid(x, y)
186
- gaussian_2d = np.exp(-((X - mu_x) ** 2) / (2 * sigma_x ** 2) - ((Y - mu_y) ** 2) / (2 * sigma_y ** 2))
187
-
188
- field = _normalize_to_unit(gaussian_2d.ravel())
189
- grid_size = grid_width * grid_height
190
- total_size = 4 * grid_size
191
- initial_state = np.zeros(total_size)
192
- initial_state[:grid_size] = field
193
- return initial_state
194
-
195
- # --- Simulation Code (from previous context) ---
196
- def Wj_block(j, n, ctrl_state, theta, lam, name='Wj_block', xgate=False):
197
- qc = QuantumCircuit(n + j, name=name)
198
- if j > 1: qc.cx(n + j - 1, range(n, n + j - 1))
199
- if lam != 0: qc.p(lam, n + j - 1)
200
- qc.h(n + j - 1)
201
- if xgate and j > 1:
202
- if isinstance(xgate, (list, tuple)):
203
- for idx, flag in enumerate(xgate):
204
- if flag: qc.x(n + idx)
205
- elif xgate is True: qc.x(range(n, n + j - 1))
206
- if j > 1:
207
- mcrz = RZGate(theta).control(len(ctrl_state) + j - 1, ctrl_state="1" * (j - 1) + ctrl_state)
208
- qc.append(mcrz, range(0, n + j))
209
- else:
210
- mcrz = RZGate(theta).control(len(ctrl_state), ctrl_state=ctrl_state)
211
- qc.append(mcrz, range(0, n + j))
212
- if xgate and j > 1:
213
- if isinstance(xgate, (list, tuple)):
214
- for idx, flag in enumerate(xgate):
215
- if flag: qc.x(n + idx)
216
- elif xgate is True: qc.x(range(n, n + j - 1))
217
- qc.h(n + j - 1)
218
- if lam != 0: qc.p(-lam, n + j - 1)
219
- if j > 1: qc.cx(n + j - 1, range(n, n + j - 1))
220
- return qc.to_gate(label=name)
221
-
222
- def V1(nx, dt):
223
- n = int(np.ceil(np.log2(nx)))
224
- derivatives, blocks = QuantumRegister(2 * n), QuantumRegister(2)
225
- qc = QuantumCircuit(derivatives, blocks)
226
- qc.append(Wj_block(2, n, "0" * n, -dt, 0, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
227
- qc.append(Wj_block(3, n - 1, "1" * (n - 1), dt, 0, xgate=[0, 1]), list(derivatives[1:n]) + [derivatives[0]] + list(blocks[:]))
228
- qc.append(Wj_block(1, n + 1, "0" * (n + 1), dt, 0, xgate=True), list(derivatives[n:2 * n]) + list(blocks[:]))
229
- qc.append(Wj_block(2, n, "0" + "1" * (n - 1), -dt, 0, xgate=False), list(derivatives[n + 1:2 * n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
230
- return qc
231
-
232
- def V2(nx, dt):
233
- n = int(np.ceil(np.log2(nx)))
234
- derivatives, blocks = QuantumRegister(2 * n), QuantumRegister(2)
235
- qc = QuantumCircuit(derivatives, blocks)
236
- qc.append(Wj_block(2, 0, "", -2 * dt, -np.pi / 2, xgate=True), blocks[:])
237
- for j in range(1, n + 1): qc.append(Wj_block(2 + j, 0, "", 2 * dt, -np.pi / 2, xgate=[1] * (j - 1) + [0, 1]), list(derivatives[0:j]) + list(blocks[:]))
238
- qc.append(Wj_block(2, n, "0" * n, -dt, -np.pi / 2, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
239
- qc.append(Wj_block(2, n, "1" * n, 2 * dt, -np.pi / 2, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
240
- qc.append(Wj_block(3, n - 1, "1" * (n - 1), dt, -np.pi / 2, xgate=[0, 1]), list(derivatives[1:n]) + [derivatives[0]] + list(blocks[:]))
241
- qc.append(Wj_block(1, 1, "0", 2 * dt, -np.pi / 2, xgate=False), blocks[:])
242
- for j in range(1, n + 1): qc.append(Wj_block(1 + j, 1, "0", -2 * dt, -np.pi / 2, xgate=[1] * (j - 1)), [blocks[0]] + list(derivatives[n:n + j]) + [blocks[1]])
243
- qc.append(Wj_block(1, n + 1, "0" * (n + 1), dt, -np.pi / 2, xgate=False), list(derivatives[n:2 * n]) + list(blocks[:]))
244
- qc.append(Wj_block(1, n + 1, "0" + "1" * n, -2 * dt, -np.pi / 2, xgate=False), list(derivatives[n:2 * n]) + list(blocks[:]))
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],
252
- always including t=0 and the final solver-aligned T (T_eff = floor(T/dt)*dt).
253
-
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:
266
- T_val = float(T)
267
- except Exception:
268
- return np.array([]), np.array([])
269
- if T_val <= 0:
270
- return np.array([]), np.array([])
271
-
272
- steps = int(np.floor(T_val / dt))
273
- if steps <= 0:
274
- return np.array([]), np.array([])
275
- T_eff = steps * dt
276
-
277
- # Determine snapshot Δt on solver grid
278
- tol = 1e-12
279
- if snapshot_dt is None:
280
- snapshot_dt_val = dt
281
- else:
282
- try:
283
- snapshot_dt_val = float(snapshot_dt)
284
- except Exception:
285
- snapshot_dt_val = dt
286
- if snapshot_dt_val < dt - tol:
287
- snapshot_dt_val = dt
288
- k = max(1, int(round(snapshot_dt_val / dt)))
289
- snapshot_dt_eff = k * dt
290
-
291
- # Build requested snapshot times on solver grid
292
- target_times = [0.0]
293
- t = 0.0
294
- while t + snapshot_dt_eff <= T_eff + tol:
295
- t = round(t + snapshot_dt_eff, 12)
296
- if t <= T_eff + tol:
297
- target_times.append(min(t, T_eff))
298
- if abs(target_times[-1] - T_eff) > tol:
299
- target_times.append(T_eff)
300
-
301
- # Setup circuit
302
- nq = int(np.ceil(np.log2(nx)))
303
- dp = 2 * R * np.pi / 2 ** na
304
- p = np.arange(-R * np.pi, R * np.pi, step=dp)
305
- fp = np.exp(-np.abs(p))
306
- system, ancilla = QuantumRegister(2 * nq + 2), QuantumRegister(na)
307
- qc = QuantumCircuit(system, ancilla)
308
- qc.append(StatePreparation(initial_state), system)
309
- qc.append(StatePreparation(fp / np.linalg.norm(fp)), ancilla)
310
- expA1 = V1(nx, dt).to_gate()
311
- expA2 = V2(nx, dt)
312
-
313
- frames = []
314
- # Capture initial frame at t=0
315
- sv0 = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))
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)
327
- qc.x(ancilla[-1])
328
- for j in range(na - 1):
329
- qc.append(expA1.control().repeat(2 ** j), [ancilla[j]] + system[:])
330
- qc.append(expA1.inverse().control(ctrl_state="0").repeat(2 ** (na - 1)), [ancilla[na - 1]] + system[:])
331
- qc.append(expA2, system[:])
332
- qc.x(ancilla[-1])
333
- qc.append(QFTGate(na).inverse(), ancilla)
334
-
335
- current_time = (i + 1) * dt
336
- if next_idx < len(target_times) and abs(current_time - target_times[next_idx]) <= tol:
337
- u = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))
338
- frames.append(u[2 ** (na - 1)])
339
- next_idx += 1
340
-
341
- if progress_callback:
342
- try:
343
- progress = ((i + 1) / steps) * 100
344
- progress_callback(progress)
345
- except Exception:
346
- pass
347
-
348
- if progress_callback:
349
- try:
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)
358
- times_arr = np.asarray(target_times[: len(frames_arr)])
359
- return frames_arr, times_arr
360
-
361
- def create_impulse_preview_state(preview_n: int, pos01, sigma01: float = 0.02):
362
- """
363
- Smooth delta-like preview on a unit square using a narrow Gaussian (sigma in [0,1]).
364
- Preview-only helper, independent of simulation grid size (nx). Use this for the
365
- Excitation preview; use the *_from_pos() variants for the actual simulation.
366
- """
367
- try:
368
- sx = float(sigma01) if sigma01 and sigma01 > 0 else 0.02
369
- except Exception:
370
- sx = 0.02
371
- return create_gaussian_state_from_pos((int(preview_n), int(preview_n)), (float(pos01[0]), float(pos01[1])), (sx, sx))
372
-
373
-
374
-
375
-
376
-
377
-
378
- ##### Statevector Estimator Simulation Code Below #####
379
-
380
- from .base_functions import *
381
-
382
- def create_time_frames(total_time, snapshot_interval):
383
- dt = 0.1
384
- tol = 1e-9
385
- try:
386
- T_val = float(total_time)
387
- except (ValueError, TypeError):
388
- return []
389
- if T_val <= 0:
390
- return []
391
- steps = int(np.floor(T_val / dt))
392
- if steps <= 0:
393
- return [0.0]
394
- T_eff = steps * dt
395
- try:
396
- snapshot_dt_val = float(snapshot_interval)
397
- except (ValueError, TypeError):
398
- snapshot_dt_val = dt
399
- if snapshot_dt_val < dt:
400
- snapshot_dt_val = dt
401
- k = max(1, int(round(snapshot_dt_val / dt)))
402
- snapshot_dt_eff = k * dt
403
- times = np.arange(0, T_eff + tol, snapshot_dt_eff)
404
- if abs(times[-1] - T_eff) > tol:
405
- times = np.append(times, T_eff)
406
- times = np.round(times, 12)
407
- unique_times = []
408
- for t in times:
409
- if not unique_times or abs(t - unique_times[-1]) > tol:
410
- unique_times.append(float(t))
411
- return unique_times
412
-
413
-
414
-
415
- def run_sve(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos, progress_callback=None, print_callback=None):
416
- """Statevector Estimator for time-series field values.
417
-
418
- Supports both single-point and multi-point modes.
419
-
420
- - Single-point (backward compatible): x, y are integers; returns list[float].
421
- - Multi-point: x is a list/tuple of (ix, iy) integer pairs and y is None; returns dict[(ix,iy) -> list[float]].
422
- """
423
- def _log(msg):
424
- if print_callback:
425
- print_callback(msg)
426
- else:
427
- print(msg)
428
-
429
- na = 1
430
- dt = 0.1
431
- R = 4
432
- nq = int(np.ceil(np.log2(nx)))
433
-
434
- # Normalize monitor points input
435
- if isinstance(x, (list, tuple)) and y is None:
436
- points = [tuple(map(int, pt)) for pt in x]
437
- multi = True
438
- else:
439
- points = [(int(x), int(y))]
440
- multi = False
441
-
442
- xref, yref = impulse_pos
443
-
444
- offset = 0
445
- grid_dims = (nx, nx)
446
- initial_state = create_impulse_state(grid_dims, impulse_pos)
447
-
448
- dp = 2 * R * np.pi / 2**na
449
- p = np.arange(- R * np.pi, R * np.pi, step=dp)
450
- fp = np.exp(-np.abs(p))
451
- norm = np.linalg.norm(fp)
452
-
453
- time_frames = create_time_frames(T, snapshot_time)
454
- total_frames = len(time_frames)
455
-
456
- _log(f"Starting QPU simulation: T={T}s, frames={total_frames}, points={len(points)}")
457
-
458
- # Prepare outputs
459
- if multi:
460
- series_by_point = { (px, py): [] for (px, py) in points }
461
- else:
462
- series_single = []
463
-
464
- for idx, time in enumerate(time_frames):
465
- steps = int(math.ceil(time / dt))
466
- # Reference Ez field at impulse location for sign
467
- Eref = Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref='Ez')
468
-
469
- for (px, py) in points:
470
- circ_magnitude = circ_for_magnitude(field, px, py, nx, na, R, dt, initial_state, steps)
471
- magnitude = get_absolute_field_value(circ_magnitude, nq, na, offset, norm)
472
-
473
- if field == 'Ez' and px == xref and py == yref:
474
- Field_value = -magnitude if Eref < 0 else magnitude
475
- else:
476
- circsum, circdiff = circuits_for_sign(field, px, py, nx, na, dt, R, initial_state, steps, xref, yref, field_ref='Ez')
477
- sign = get_relative_sign(circsum, circdiff, nq, na)
478
- if (sign == 'same' and Eref > 0) or (sign == 'different' and Eref < 0):
479
- Field_value = magnitude
480
- else:
481
- Field_value = -magnitude
482
-
483
- if multi:
484
- series_by_point[(px, py)].append(Field_value)
485
- else:
486
- series_single.append(Field_value)
487
-
488
- if progress_callback:
489
- progress_callback((idx + 1) / total_frames * 100)
490
-
491
- _log("Statevector Estimator simulation completed.")
492
-
493
- return series_by_point if multi else series_single