A19grey commited on
Commit
0f5f6d3
·
0 Parent(s):

Updating to new 3D visualizer molgallery3D and need to restart space to install

Browse files
.cursorrules ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Write out code in a way that is easy to understand and follow.
2
+
3
+ For each large task update make an update in @history.md file with the date and the changes made.
4
+ New Entries should be added to the top of the file. This will provide a history of the changes made to the code.
5
+
6
+ If things are unclear ask for clarification.
7
+
8
+ If you are unsure about the best way to do something, propose a solution and ask for clarification.
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ env/
26
+ ENV/
27
+ .env
28
+ .venv
29
+ env.bak/
30
+ venv.bak/
31
+
32
+ # IDE specific files
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+ .project
38
+ .pydevproject
39
+ .settings/
40
+
41
+ # Jupyter Notebook
42
+ .ipynb_checkpoints
43
+ *.ipynb_checkpoints/
44
+
45
+ # Distribution / packaging
46
+ .Python
47
+ *.manifest
48
+ *.spec
49
+
50
+ # Logs and databases
51
+ *.log
52
+ *.sqlite
53
+ *.db
54
+
55
+ # OS generated files
56
+ .DS_Store
57
+ .DS_Store?
58
+ ._*
59
+ .Spotlight-V100
60
+ .Trashes
61
+ ehthumbs.db
62
+ Thumbs.db
63
+
64
+ # Testing
65
+ .coverage
66
+ .tox/
67
+ .pytest_cache/
68
+ htmlcov/
69
+ .coverage.*
70
+ coverage.xml
71
+ *.cover
72
+
73
+ # Documentation
74
+ docs/_build/
75
+ docs/generated/
76
+
77
+ # Local development settings
78
+ local_settings.py
79
+ *.env
README.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Cuda Quantum Molecular Playground
3
+ emoji: 📈
4
+ colorFrom: pink
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 5.9.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ suggested_hardware: t4-small
12
+ short_description: Nvidia Cuda Quantum accelerator to demo to simulate bonds
13
+ ---
14
+
15
+ # CUDA Quantum Molecular Playground
16
+
17
+ An interactive web demo using NVIDIA's CUDA Quantum Library (CUDA-Q) for quantum computations. This project focuses on the Variational Quantum Eigensolver (VQE) algorithm to estimate molecular energy, providing an educational interface for quantum control and simulation.
18
+
19
+ ## Interface Features
20
+
21
+ 1. **Molecule Selection & Parameters**
22
+ - Choose from available molecules
23
+ - View detailed molecular parameters (formula, orbitals, charge, basis)
24
+ - Adjust bond length using an interactive scale factor slider
25
+
26
+ 2. **Simulation Controls**
27
+ - Run VQE simulations with real-time visualization
28
+ - View simulation results including:
29
+ - Final energy
30
+ - Parameter count
31
+ - Hamiltonian terms
32
+ - Optimization status
33
+ - Iteration count
34
+
35
+ 3. **Visualization**
36
+ - Interactive 3D molecular structure viewer
37
+ - Real-time energy convergence plots
38
+ - Detailed simulation results display
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ ```
45
+
46
+ ## Running the Demo
47
+
48
+ 1. Launch the application:
49
+ ```bash
50
+ python app.py
51
+ ```
52
+
53
+ 2. Open your browser to the local URL displayed in the terminal (typically http://localhost:7860)
54
+
55
+ ## Key Technologies
56
+
57
+ - **CUDA-Q**: Quantum computations and hardware acceleration
58
+ - **OpenFermion**: Quantum simulations and electronic structure calculations
59
+ - **PySCF**: Molecular Hamiltonian preparation and quantum chemistry simulations
60
+ - **Gradio**: Interactive web interface
61
+ - **Plotly**: Data visualization
62
+ - **py3Dmol**: Molecular structure visualization
63
+
64
+ ## Hardware Requirements
65
+
66
+ The demo automatically detects available hardware:
67
+ - With NVIDIA GPU: Uses CUDA-Q's nvidia backend for acceleration
68
+ - Without GPU: Falls back to CPU-based quantum simulation
app.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NVIDIA CUDA-Q VQE Demo
3
+ =====================
4
+
5
+ This Gradio application demonstrates the Variational Quantum Eigensolver (VQE)
6
+ algorithm using NVIDIA's CUDA Quantum library. The demo allows users to:
7
+
8
+ 1. Select from available molecules
9
+ 2. Adjust bond length parameters
10
+ 3. Run VQE simulations with real-time visualization
11
+ 4. View energy convergence and molecular structure in 3D
12
+
13
+ Running the Demo
14
+ ---------------
15
+ 1. Ensure you have all dependencies installed:
16
+ ```bash
17
+ pip install -r requirements.txt
18
+ ```
19
+
20
+ 2. Launch the application:
21
+ ```bash
22
+ python app.py
23
+ ```
24
+
25
+ 3. Open your browser to the local URL displayed in the terminal
26
+ (typically http://localhost:7860)
27
+
28
+ Using the Interface
29
+ -----------------
30
+ - Select a molecule from the dropdown
31
+ - Adjust bond length using the slider (0.5 to 2.5 Å)
32
+ - Click "Run VQE Simulation" or watch real-time updates as you adjust parameters
33
+ - View results in three panels:
34
+ * Simulation Results: Shows final energy and optimization status
35
+ * VQE Convergence: Plots energy vs. iteration
36
+ * Molecule Visualization: 3D representation of the molecule
37
+
38
+ GPU Acceleration
39
+ --------------
40
+ The app automatically detects if NVIDIA GPUs are available:
41
+ - With GPU: Uses CUDA-Q's nvidia backend for acceleration
42
+ - Without GPU: Falls back to CPU-based quantum simulation
43
+
44
+ References
45
+ ---------
46
+ - Installation: See Cuda-Q_install.md
47
+ - VQE Implementation: See VQE_Example.md
48
+ - Project Structure: See quantum-demo-blueprint.md
49
+ """
50
+
51
+ import subprocess
52
+ import sys
53
+ import json
54
+ import os
55
+
56
+ # Load molecule definitions
57
+ def load_molecules():
58
+ """Load molecule definitions from JSON file."""
59
+ try:
60
+ with open('molecules.json', 'r') as f:
61
+ return json.load(f)
62
+ except Exception as e:
63
+ print(f"Error loading molecules.json: {e}", file=sys.stderr)
64
+ return {}
65
+
66
+ # Test if cudaq is already installed
67
+ try:
68
+ import cudaq
69
+ print("CUDA-Q already installed, skipping dependency installation")
70
+ except ImportError:
71
+ print("CUDA-Q not found, installing dependencies...")
72
+ # Run installation before any other imports
73
+ install_dependencies()
74
+
75
+ # Now import the rest of the modules
76
+ import gradio as gr
77
+ import numpy as np
78
+ from quantum_utils import run_vqe_simulation
79
+ from visualization import plot_convergence, create_molecule_viewer, format_molecule_params
80
+ import spaces
81
+
82
+ # Add immediate logging
83
+ print("Imported all modules successfully", file=sys.stderr, flush=True)
84
+
85
+ def update_simulation(molecule_choice: str, scale_factor: float) -> tuple:
86
+ """
87
+ Run VQE simulation and update visualizations.
88
+
89
+ Args:
90
+ molecule_choice: Selected molecule from available options
91
+ scale_factor: Factor to scale the molecule geometry by (1.0 = original size)
92
+ Returns:
93
+ Tuple of (energy text, convergence plot, molecule HTML)
94
+ """
95
+ print("UPDATE_SIMULATION CALLED", file=sys.stderr, flush=True)
96
+ try:
97
+ # Get molecule data
98
+ molecules = load_molecules()
99
+ if molecule_choice not in molecules:
100
+ raise ValueError(f"Unknown molecule: {molecule_choice}")
101
+
102
+ molecule_data = molecules[molecule_choice]
103
+ print(f"Starting simulation with molecule: {molecule_choice}, scale: {scale_factor}",
104
+ file=sys.stderr, flush=True)
105
+
106
+ # Run VQE simulation
107
+ results = run_vqe_simulation(
108
+ molecule_data=molecule_data,
109
+ scale_factor=scale_factor
110
+ )
111
+
112
+ print(f"VQE simulation completed with results: {results}",
113
+ file=sys.stderr, flush=True)
114
+
115
+ # Create plots
116
+ print("Creating convergence plot...", file=sys.stderr)
117
+ convergence_plot = plot_convergence(results)
118
+ print("Convergence plot created", file=sys.stderr)
119
+
120
+ print("Creating molecule visualization...", file=sys.stderr)
121
+ molecule_html = create_molecule_viewer(molecule_choice, scale_factor)
122
+ print("Molecule visualization created", file=sys.stderr)
123
+
124
+ # Format energy output
125
+ energy_text = f"""
126
+ Final Energy: {results['final_energy']:.6f} Hartree
127
+ Optimization Status: {'Success' if results['success'] else 'Failed'}
128
+ Message: {results['message']}
129
+ Number of Iterations: {len(results['history'])}
130
+ Scale Factor: {scale_factor:.2f}
131
+ """
132
+
133
+ print("Returning results...", file=sys.stderr)
134
+ return energy_text, convergence_plot, molecule_html
135
+
136
+ except Exception as e:
137
+ import traceback
138
+ error_msg = f"Error in simulation:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
139
+ print(error_msg, file=sys.stderr) # This will show in the server logs
140
+ return error_msg, None, None
141
+
142
+ def create_interface():
143
+ """Create the Gradio interface for the VQE demo."""
144
+
145
+ # Load available molecules
146
+ molecules = load_molecules()
147
+ if not molecules:
148
+ raise ValueError("No molecules found in molecules.json")
149
+
150
+ with gr.Blocks(title="CUDA-Q VQE Demo") as demo:
151
+ gr.Markdown("# NVIDIA CUDA-Q VQE Demo")
152
+ gr.Markdown("""
153
+ This demo shows a Variational Quantum Eigensolver (VQE) simulation
154
+ using NVIDIA's CUDA Quantum library.
155
+ """)
156
+
157
+ state = gr.State({}) # Store current molecule data
158
+
159
+ with gr.Row():
160
+ with gr.Column(scale=1):
161
+ # Molecule selection controls
162
+ molecule_choice = gr.Dropdown(
163
+ choices=list(molecules.keys()),
164
+ value=list(molecules.keys())[0],
165
+ label="Select Molecule"
166
+ )
167
+
168
+ # Get scale range from selected molecule
169
+ default_molecule = molecules[list(molecules.keys())[0]]
170
+ scale_factor = gr.Slider(
171
+ minimum=default_molecule['scale_range']['min'],
172
+ maximum=default_molecule['scale_range']['max'],
173
+ value=default_molecule['default_scale'],
174
+ step=default_molecule['scale_range']['step'],
175
+ label="Scale Factor (1.0 = original size)",
176
+ interactive=True
177
+ )
178
+
179
+ run_button = gr.Button("Run VQE Simulation")
180
+
181
+ with gr.Column(scale=1):
182
+ # Molecule parameters display
183
+ params_display = gr.HTML(
184
+ label="Molecule Parameters",
185
+ value=format_molecule_params(default_molecule)
186
+ )
187
+
188
+ # Simulation results box below controls
189
+ with gr.Row():
190
+ simulation_results = gr.HTML(
191
+ label="Simulation Results",
192
+ value="<div style='padding: 10px; background-color: #f5f5f5; border-radius: 5px;'>" +
193
+ "Click 'Run VQE Simulation' to start the calculation.</div>"
194
+ )
195
+
196
+ # Plots and visualization
197
+ with gr.Row():
198
+ convergence_plot = gr.Plot(
199
+ label="VQE Convergence",
200
+ show_label=True
201
+ )
202
+ # Create MolGallery3D component
203
+ molecule_viz = MolGallery3D(
204
+ columns=1,
205
+ height=400,
206
+ label="3D Molecule Viewer",
207
+ automatic_rotation=True
208
+ )
209
+
210
+ def update_molecule_info(molecule_choice):
211
+ """Update description and scale range when molecule is selected."""
212
+ try:
213
+ mol_data = molecules[molecule_choice]
214
+
215
+ # Format parameters display
216
+ params_html = format_molecule_params(mol_data)
217
+
218
+ # Get scale range values with defaults
219
+ min_scale = mol_data.get('scale_range', {}).get('min', 0.1)
220
+ max_scale = mol_data.get('scale_range', {}).get('max', 3.0)
221
+ default_scale = mol_data.get('default_scale', 1.0)
222
+ step_size = mol_data.get('scale_range', {}).get('step', 0.05)
223
+
224
+ # Create molecule visualization
225
+ mol_list = create_molecule_viewer(molecule_choice, default_scale)
226
+ if mol_list is None:
227
+ mol_list = [] # Empty list if molecule creation failed
228
+
229
+ return [
230
+ params_html, # Update params display
231
+ gr.update( # Update slider
232
+ minimum=min_scale,
233
+ maximum=max_scale,
234
+ value=default_scale,
235
+ step=step_size
236
+ ),
237
+ mol_list # Update 3D view
238
+ ]
239
+ except Exception as e:
240
+ print(f"Error updating molecule info: {e}", file=sys.stderr)
241
+ return [
242
+ "<div>Error loading molecule parameters</div>",
243
+ gr.update(
244
+ minimum=0.1,
245
+ maximum=3.0,
246
+ value=1.0,
247
+ step=0.05
248
+ ),
249
+ [] # Empty list for molecule visualization
250
+ ]
251
+
252
+ def update_simulation_with_details(molecule_choice: str, scale_factor: float) -> tuple:
253
+ """Enhanced simulation update with parameter count and Hamiltonian terms."""
254
+ try:
255
+ # Get molecule data
256
+ molecule_data = molecules[molecule_choice]
257
+
258
+ # Run VQE simulation
259
+ results = run_vqe_simulation(
260
+ molecule_data=molecule_data,
261
+ scale_factor=scale_factor
262
+ )
263
+
264
+ # Create plots
265
+ convergence_plot = plot_convergence(results)
266
+
267
+ # Format simulation results with additional details
268
+ simulation_html = f"""
269
+ <div style='padding: 10px; background-color: #f5f5f5; border-radius: 5px;'>
270
+ <h3>Simulation Results:</h3>
271
+ <ul style='list-style-type: none; padding-left: 0;'>
272
+ <li><b>Final Energy:</b> {results['final_energy']:.6f} Hartree</li>
273
+ <li><b>Parameter Count:</b> {results.get('parameter_count', 'N/A')}</li>
274
+ <li><b>Hamiltonian Terms:</b> {results.get('hamiltonian_terms', 'N/A')}</li>
275
+ <li><b>Optimization Status:</b> {'Success' if results.get('success', False) else 'Failed'}</li>
276
+ <li><b>Iterations:</b> {len(results['history'])}</li>
277
+ <li><b>Scale Factor:</b> {scale_factor:.2f}</li>
278
+ </ul>
279
+ <p><i>{results.get('message', '')}</i></p>
280
+ </div>
281
+ """
282
+
283
+ return simulation_html, convergence_plot
284
+
285
+ except Exception as e:
286
+ import traceback
287
+ error_msg = f"<div style='color: red;'>Error in simulation:<br>{str(e)}</div>"
288
+ return error_msg, None
289
+
290
+ # Update parameters and 3D view when molecule changes
291
+ molecule_choice.change(
292
+ fn=update_molecule_info,
293
+ inputs=[molecule_choice],
294
+ outputs=[params_display, scale_factor, molecule_viz]
295
+ )
296
+
297
+ # Run simulation when button is clicked
298
+ run_button.click(
299
+ fn=update_simulation_with_details,
300
+ inputs=[molecule_choice, scale_factor],
301
+ outputs=[simulation_results, convergence_plot]
302
+ )
303
+
304
+ return demo
305
+
306
+ if __name__ == "__main__":
307
+ demo = create_interface()
308
+ demo.launch(share=False)
docref/Example_cudaq_logical_aim_agk.py ADDED
@@ -0,0 +1,712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """Copy of cudaq_logical_aim_agk.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1Jsf9Le8eoCJgddNzYvfQO4E7g--N-DyC
8
+
9
+ # AIM: Logical Anderson Impurity Model on Sqale using CUDA-Q
10
+
11
+ [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Infleqtion/client-superstaq/blob/main/docs/source/apps/cudaq_logical_aim.ipynb) [![Launch Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Infleqtion/client-superstaq/HEAD?labpath=docs/source/apps/cudaq_logical_aim.ipynb)
12
+
13
+ Ground state quantum chemistry—computing total energies of molecular configurations to within chemical accuracy—is perhaps the most highly-touted industrial application of fault-tolerant quantum computers. Strongly correlated materials, for example, are particularly interesting, and tools like dynamical mean-field theory (DMFT) allow one to account for the effect of their strong, localized electronic correlations. These DMFT models help predict material properties by approximating the system as a single site impurity inside a “bath” that encompasses the rest of the system. Simulating such dynamics can be a tough task using classical methods, but can be done efficiently on a quantum computer via quantum simulation.
14
+
15
+ In this notebook, we showcase a workflow for preparing the ground state of the minimal single-impurity Anderson model (SIAM) using the Hamiltonian Variational Ansatz for a range of realistic parameters. As a first step towards running DMFT on a fault-tolerant quantum computer, we will use logical qubits encoded in the `[[4, 2, 2]]` code. Using this workflow, we will obtain the ground state energy estimates via noisy simulation, and then also execute the corresponding optimized circuits on Infleqtion's gate-based neutral-atom quantum computer, making the benefits of logical qubits apparent. More details can be found in our [paper](https://arxiv.org/abs/2412.07670).
16
+
17
+ [agk] - 2024-12-23 Reference chats to understand AIM model - https://gemini.google.com/app/fce0538c8dc2b6ac and https://x.com/i/grok/share/AV9UkWb4LNtfqm1HfLPEQ78HG
18
+
19
+ This demo notebook uses CUDA-Q (`cudaq`) and a CUDA-QX library, `cudaq-solvers`; let us first begin by importing (and installing as needed) these packages:
20
+ """
21
+
22
+ # Commented out IPython magic to ensure Python compatibility.
23
+ try:
24
+ import cudaq_solvers as solvers
25
+ import cudaq
26
+ import matplotlib.pyplot as plt
27
+ except ImportError:
28
+ print("Installing required packages...")
29
+ # %pip install --quiet 'cudaq-solvers' 'matplotlib'
30
+ print("Installed `cudaq`, `cudaq-solvers`, and `matplotlib` packages.")
31
+ print("You may need to restart the kernel to import newly installed packages.")
32
+ import cudaq_solvers as solvers
33
+ import cudaq
34
+ import matplotlib.pyplot as plt
35
+
36
+ from collections.abc import Mapping, Sequence
37
+ import numpy as np
38
+ from scipy.optimize import minimize
39
+ import os
40
+
41
+ from IPython.core.display import HTML
42
+ print("restarting Kernel")
43
+ HTML("<script>Jupyter.notebook.kernel.restart()</script>")
44
+ print("Kernel restarted. Probably, I don't know... I'm not magic")
45
+
46
+ """## Performing logical Variational Quantum Eigensolver (VQE) with CUDA-QX
47
+
48
+ To prepare our ground state quantum Anderson impurity model circuits (referred to as AIM circuits in this notebook for short), we use VQE to train an ansatz to minimize a Hamiltonian and obtain optimal angles that can be used to set the AIM circuits. As described in our [paper](https://arxiv.org/abs/2412.07670), the associated restricted Hamiltonian for our SIAM can be reduced to,
49
+ $$
50
+ \begin{equation}
51
+ H_{(U, V)} = U (Z_0 Z_2 - 1) / 4 + V (X_0 + X_2),
52
+ \end{equation}
53
+ $$
54
+ where $U$ is the Coulomb interaction and $V$ the hybridization strength. In this notebook workflow, we will optimize over a 2-dimensional grid of Hamiltonian parameter values, namely $U\in \{1, 5, 9\}$ and $V\in \{-9, -1, 7\}$ (with all values assumed to be in units of eV), to ensure that the ansatz is generally trainable and expressive, and obtain 9 different circuit layers identified by the key $(U, V)$. We will simulate the VQE on GPU (or optionally on CPU if you do not have GPU access), enabled by CUDA-Q, in the absence of noise:
55
+ """
56
+
57
+ if cudaq.num_available_gpus() == 0:
58
+ cudaq.set_target("qpp-cpu", option="fp64")
59
+
60
+ print("Using CPU no Cuda gpu found")
61
+ else:
62
+ cudaq.set_target("nvidia", option="fp64")
63
+ print("Using GPU")
64
+
65
+ """This workflow can be easily defined in CUDA-Q as shown in the cell below, using the CUDA-QX Solvers library (which accelerates quantum algorithms like the VQE):"""
66
+
67
+ def ansatz(n_qubits: int) -> cudaq.Kernel:
68
+ # Create a CUDA-Q parameterized kernel
69
+ paramterized_ansatz, variational_angles = cudaq.make_kernel(list)
70
+ qubits = paramterized_ansatz.qalloc(n_qubits)
71
+
72
+ # Using |+> as the initial state:
73
+ paramterized_ansatz.h(qubits[0])
74
+ paramterized_ansatz.cx(qubits[0], qubits[1])
75
+
76
+ paramterized_ansatz.rx(variational_angles[0], qubits[0])
77
+ paramterized_ansatz.cx(qubits[0], qubits[1])
78
+ paramterized_ansatz.rz(variational_angles[1], qubits[1])
79
+ paramterized_ansatz.cx(qubits[0], qubits[1])
80
+ return paramterized_ansatz
81
+
82
+
83
+ def run_logical_vqe(cudaq_hamiltonian: cudaq.SpinOperator) -> tuple[float, list[float]]:
84
+ # Set seed for easier reproduction
85
+ np.random.seed(42)
86
+
87
+ # Initial angles for the optimizer
88
+ init_angles = np.random.random(2) * 1e-1
89
+
90
+ # Obtain CUDA-Q Ansatz
91
+ num_qubits = cudaq_hamiltonian.get_qubit_count()
92
+ variational_kernel = ansatz(num_qubits)
93
+
94
+ # Perform VQE optimization
95
+ energy, params, _ = solvers.vqe(
96
+ variational_kernel,
97
+ cudaq_hamiltonian,
98
+ init_angles,
99
+ optimizer=minimize,
100
+ method="SLSQP",
101
+ tol=1e-10,
102
+ )
103
+ return energy, params
104
+
105
+ """## Constructing circuits in the `[[4,2,2]]` encoding
106
+
107
+ The `[[4,2,2]]` code is a quantum error detection code that uses four physical qubits to encode two logical qubits. In this notebook, we will construct two variants of quantum circuits: physical (bare, unencoded) and logical (encoded). These circuits will be informed by the Hamiltonian Variational Ansatz described earlier. To measure all the terms in our Hamiltonian, we will measure the data qubits in both the $Z$- and $X$-basis, as allowed by the `[[4,2,2]]` logical gateset. Full details on the circuit constructions are outlined in our [paper](https://arxiv.org/abs/2412.07670).
108
+
109
+ Below, we create functions to build our CUDA-Q AIM circuits, both physical and logical versions. As we consider noisy simulation in this notebook, we will include some noisy gates. Here, for simplicity, we will just register a custom identity gate -- to be later used as a noisy operation to model readout error:
110
+ """
111
+
112
+ cudaq.register_operation("meas_id", np.identity(2))
113
+
114
+ def aim_physical_circuit(
115
+ angles: list[float], basis: str, *, ignore_meas_id: bool = False
116
+ ) -> cudaq.Kernel:
117
+ kernel = cudaq.make_kernel()
118
+ qubits = kernel.qalloc(2)
119
+
120
+ # Bell state prep
121
+ kernel.h(qubits[0])
122
+ kernel.cx(qubits[0], qubits[1])
123
+
124
+ # Rx Gate
125
+ kernel.rx(angles[0], qubits[0])
126
+
127
+ # ZZ rotation
128
+ kernel.cx(qubits[0], qubits[1])
129
+ kernel.rz(angles[1], qubits[1])
130
+ kernel.cx(qubits[0], qubits[1])
131
+
132
+ if basis == "z_basis":
133
+ if not ignore_meas_id:
134
+ kernel.for_loop(
135
+ start=0, stop=2, function=lambda q_idx: getattr(kernel, "meas_id")(qubits[q_idx])
136
+ )
137
+ kernel.mz(qubits)
138
+ elif basis == "x_basis":
139
+ kernel.h(qubits)
140
+ if not ignore_meas_id:
141
+ kernel.for_loop(
142
+ start=0, stop=2, function=lambda q_idx: getattr(kernel, "meas_id")(qubits[q_idx])
143
+ )
144
+ kernel.mz(qubits)
145
+ else:
146
+ raise ValueError("Unsupported basis provided:", basis)
147
+ return kernel
148
+
149
+ def aim_logical_circuit(
150
+ angles: list[float], basis: str, *, ignore_meas_id: bool = False
151
+ ) -> cudaq.Kernel:
152
+ kernel = cudaq.make_kernel()
153
+ qubits = kernel.qalloc(6)
154
+
155
+ kernel.for_loop(start=0, stop=3, function=lambda idx: kernel.h(qubits[idx]))
156
+ kernel.cx(qubits[1], qubits[4])
157
+ kernel.cx(qubits[2], qubits[3])
158
+ kernel.cx(qubits[0], qubits[1])
159
+ kernel.cx(qubits[0], qubits[3])
160
+
161
+ # Rx teleportation
162
+ kernel.rx(angles[0], qubits[0])
163
+
164
+ kernel.cx(qubits[0], qubits[1])
165
+ kernel.cx(qubits[0], qubits[3])
166
+ kernel.h(qubits[0])
167
+
168
+ if basis == "z_basis":
169
+ if not ignore_meas_id:
170
+ kernel.for_loop(
171
+ start=0, stop=5, function=lambda idx: getattr(kernel, "meas_id")(qubits[idx])
172
+ )
173
+ kernel.mz(qubits)
174
+ elif basis == "x_basis":
175
+ # ZZ rotation and teleportation
176
+ kernel.cx(qubits[3], qubits[5])
177
+ kernel.cx(qubits[2], qubits[5])
178
+ kernel.rz(angles[1], qubits[5])
179
+ kernel.cx(qubits[1], qubits[5])
180
+ kernel.cx(qubits[4], qubits[5])
181
+ kernel.for_loop(start=1, stop=5, function=lambda idx: kernel.h(qubits[idx]))
182
+ if not ignore_meas_id:
183
+ kernel.for_loop(
184
+ start=0, stop=6, function=lambda idx: getattr(kernel, "meas_id")(qubits[idx])
185
+ )
186
+ kernel.mz(qubits)
187
+ else:
188
+ raise ValueError("Unsupported basis provided:", basis)
189
+ return kernel
190
+
191
+ """With the circuit definitions above, we can now define a function that automatically runs the VQE and constructs a dictionary containing all the AIM circuits we want to submit to hardware (or noisily simulate):"""
192
+
193
+ def generate_circuit_set(ignore_meas_id: bool = False) -> object:
194
+ u_vals = [1, 5, 9]
195
+ v_vals = [-9, -1, 7]
196
+ circuit_dict = {}
197
+ for u in u_vals:
198
+ for v in v_vals:
199
+ qubit_hamiltonian = (
200
+ 0.25 * u * cudaq.spin.z(0) * cudaq.spin.z(1)
201
+ - 0.25 * u
202
+ + v * cudaq.spin.x(0)
203
+ + v * cudaq.spin.x(1)
204
+ )
205
+ _, opt_params = run_logical_vqe(qubit_hamiltonian)
206
+ angles = [float(angle) for angle in opt_params]
207
+ print(f"Computed optimal angles={angles} for U={u}, V={v}")
208
+
209
+ tmp_physical_dict = {}
210
+ tmp_logical_dict = {}
211
+ for basis in ("z_basis", "x_basis"):
212
+ tmp_physical_dict[basis] = aim_physical_circuit(
213
+ angles, basis, ignore_meas_id=ignore_meas_id
214
+ )
215
+ tmp_logical_dict[basis] = aim_logical_circuit(
216
+ angles, basis, ignore_meas_id=ignore_meas_id
217
+ )
218
+
219
+ circuit_dict[f"{u}:{v}"] = {
220
+ "physical": tmp_physical_dict,
221
+ "logical": tmp_logical_dict,
222
+ }
223
+ print("\nFinished building optimized circuits!")
224
+ return circuit_dict
225
+
226
+ sim_circuit_dict = generate_circuit_set()
227
+ circuit_layers = sim_circuit_dict.keys()
228
+
229
+ """## Setting up submission and decoding workflow
230
+
231
+ In this section, we define various helper functions that will play a role in generating the associated energies of the AIM circuits based on the circuit samples (in the different bases), as well as decode the logical circuits with post-selection informed by the `[[4,2,2]]` code:
232
+ """
233
+
234
+ def _num_qubits(counts: Mapping[str, float]) -> int:
235
+ for key in counts:
236
+ if key.isdecimal():
237
+ return len(key)
238
+ return 0
239
+
240
+
241
+ def process_counts(
242
+ counts: Mapping[str, float],
243
+ data_qubits: Sequence[int],
244
+ flag_qubits: Sequence[int] = (),
245
+ ) -> dict[str, float]:
246
+ new_data: dict[str, float] = {}
247
+ for key, val in counts.items():
248
+ if not all(key[i] == "0" for i in flag_qubits):
249
+ continue
250
+
251
+ new_key = "".join(key[i] for i in data_qubits)
252
+
253
+ if not set("01").issuperset(new_key):
254
+ continue
255
+
256
+ new_data.setdefault(new_key, 0)
257
+ new_data[new_key] += val
258
+
259
+ return new_data
260
+
261
+
262
+ def decode(counts: Mapping[str, float]) -> dict[str, float]:
263
+ """Decode physical counts into logical counts. Should be called after `process_counts`."""
264
+
265
+ if not counts:
266
+ return {}
267
+
268
+ num_qubits = _num_qubits(counts)
269
+ assert num_qubits % 4 == 0
270
+
271
+ physical_to_logical = {
272
+ "0000": "00",
273
+ "1111": "00",
274
+ "0011": "01",
275
+ "1100": "01",
276
+ "0101": "10",
277
+ "1010": "10",
278
+ "0110": "11",
279
+ "1001": "11",
280
+ }
281
+
282
+ new_data: dict[str, float] = {}
283
+ for key, val in counts.items():
284
+ physical_keys = [key[i : i + 4] for i in range(0, num_qubits, 4)]
285
+ logical_keys = [physical_to_logical.get(physical_key) for physical_key in physical_keys]
286
+ if None not in logical_keys:
287
+ new_key = "".join(logical_keys)
288
+ new_data.setdefault(new_key, 0)
289
+ new_data[new_key] += val
290
+
291
+ return new_data
292
+
293
+
294
+ def ev_x(counts: Mapping[str, float]) -> float:
295
+ ev = 0.0
296
+
297
+ for k, val in counts.items():
298
+ ev += val * ((-1) ** int(k[0]) + (-1) ** int(k[1]))
299
+
300
+ total = sum(counts.values())
301
+ ev /= total
302
+ return ev
303
+
304
+
305
+ def ev_xx(counts: Mapping[str, float]) -> float:
306
+ ev = 0.0
307
+
308
+ for k, val in counts.items():
309
+ ev += val * (-1) ** k.count("1")
310
+
311
+ total = sum(counts.values())
312
+ ev /= total
313
+ return ev
314
+
315
+
316
+ def ev_zz(counts: Mapping[str, float]) -> float:
317
+ ev = 0.0
318
+
319
+ for k, val in counts.items():
320
+ ev += val * (-1) ** k.count("1")
321
+
322
+ total = sum(counts.values())
323
+ ev /= total
324
+ return ev
325
+
326
+
327
+ def aim_logical_energies(
328
+ data_ordering: object, counts_list: Sequence[dict[str, float]]
329
+ ) -> tuple[dict[tuple[int, int], float], dict[tuple[int, int], float]]:
330
+ counts_data = {
331
+ data_ordering[i]: decode(
332
+ process_counts(
333
+ counts,
334
+ data_qubits=[1, 2, 3, 4],
335
+ flag_qubits=[0, 5],
336
+ )
337
+ )
338
+ for i, counts in enumerate(counts_list)
339
+ }
340
+ return _aim_energies(counts_data)
341
+
342
+
343
+ def aim_physical_energies(
344
+ data_ordering: object, counts_list: Sequence[dict[str, float]]
345
+ ) -> tuple[dict[tuple[int, int], float], dict[tuple[int, int], float]]:
346
+ counts_data = {
347
+ data_ordering[i]: process_counts(
348
+ counts,
349
+ data_qubits=[0, 1],
350
+ )
351
+ for i, counts in enumerate(counts_list)
352
+ }
353
+ return _aim_energies(counts_data)
354
+
355
+
356
+ def _aim_energies(
357
+ counts_data: Mapping[tuple[int, int, str], dict[str, float]],
358
+ ) -> tuple[dict[tuple[int, int], float], dict[tuple[int, int], float]]:
359
+ evxs: dict[tuple[int, int], float] = {}
360
+ evxxs: dict[tuple[int, int], float] = {}
361
+ evzzs: dict[tuple[int, int], float] = {}
362
+ totals: dict[tuple[int, int], float] = {}
363
+
364
+ for key, counts in counts_data.items():
365
+ h_params, basis = key
366
+ key_a, key_b = h_params.split(":")
367
+ u, v = int(key_a), int(key_b)
368
+ if basis.startswith("x"):
369
+ evxs[u, v] = ev_x(counts)
370
+ evxxs[u, v] = ev_xx(counts)
371
+ else:
372
+ evzzs[u, v] = ev_zz(counts)
373
+
374
+ totals.setdefault((u, v), 0)
375
+ totals[u, v] += sum(counts.values())
376
+
377
+ energies = {}
378
+ uncertainties = {}
379
+ for u, v in evxs.keys() & evzzs.keys():
380
+ string_key = f"{u}:{v}"
381
+ energies[string_key] = u * (evzzs[u, v] - 1) / 4 + v * evxs[u, v]
382
+
383
+ uncertainty_xx = 2 * v**2 * (1 + evxxs[u, v]) - u * v * evxs[u, v] / 2
384
+ uncertainty_zz = u**2 * (1 - evzzs[u, v]) / 2
385
+
386
+ uncertainties[string_key] = np.sqrt(
387
+ (uncertainty_zz + uncertainty_xx - energies[string_key] ** 2) / (totals[u, v] / 2)
388
+ )
389
+
390
+ return energies, uncertainties
391
+
392
+
393
+ def _get_energy_diff(
394
+ bf_energies: dict[str, float],
395
+ physical_energies: dict[str, float],
396
+ logical_energies: dict[str, float],
397
+ ) -> tuple[list[float], list[float]]:
398
+ physical_energy_diff = []
399
+ logical_energy_diff = []
400
+
401
+ # Data ordering following `bf_energies` keys
402
+ for layer in bf_energies.keys():
403
+ physical_sim_energy = physical_energies[layer]
404
+ logical_sim_energy = logical_energies[layer]
405
+ true_energy = bf_energies[layer]
406
+ u, v = layer.split(":")
407
+ print(f"Layer=({u}, {v}) has brute-force energy of: {true_energy}")
408
+ print(f"Physical circuit of layer=({u}, {v}) got an energy of: {physical_sim_energy}")
409
+ print(f"Logical circuit of layer=({u}, {v}) got an energy of: {logical_sim_energy}")
410
+ print("-" * 72)
411
+
412
+ if logical_sim_energy < physical_sim_energy:
413
+ print("Logical circuit achieved the lower energy!")
414
+ else:
415
+ print("Physical circuit achieved the lower energy")
416
+ print("-" * 72, "\n")
417
+
418
+ physical_energy_diff.append(
419
+ -1 * (true_energy - physical_sim_energy)
420
+ ) # Multiply by -1 since negative energies
421
+ logical_energy_diff.append(-1 * (true_energy - logical_sim_energy))
422
+ return physical_energy_diff, logical_energy_diff
423
+
424
+ def submit_aim_circuits(
425
+ circuit_dict: object,
426
+ *,
427
+ folder_path: str = "future_aim_results",
428
+ shots_count: int = 1000,
429
+ noise_model: cudaq.mlir._mlir_libs._quakeDialects.cudaq_runtime.NoiseModel | None = None,
430
+ run_async: bool = False,
431
+ ) -> dict[str, list[dict[str, int]]] | None:
432
+ if run_async:
433
+ os.makedirs(folder_path, exist_ok=True)
434
+ else:
435
+ aim_results = {"physical": [], "logical": []}
436
+
437
+ for layer in circuit_dict.keys():
438
+ if run_async:
439
+ print(f"Posting circuits associated with layer=('{layer}')")
440
+ else:
441
+ print(f"Running circuits associated with layer=('{layer}')")
442
+
443
+ for basis in ("z_basis", "x_basis"):
444
+ if run_async:
445
+ u, v = layer.split(":")
446
+
447
+ tmp_physical_results = cudaq.sample_async(
448
+ circuit_dict[layer]["physical"][basis], shots_count=shots_count
449
+ )
450
+ file = open(f"{folder_path}/physical_{basis}_job_u={u}_v={v}_result.txt", "w")
451
+ file.write(str(tmp_physical_results))
452
+ file.close()
453
+
454
+ tmp_logical_results = cudaq.sample_async(
455
+ circuit_dict[layer]["logical"][basis], shots_count=shots_count
456
+ )
457
+ file = open(f"{folder_path}/logical_{basis}_job_u={u}_v={v}_result.txt", "w")
458
+ file.write(str(tmp_logical_results))
459
+ file.close()
460
+ else:
461
+ tmp_physical_results = cudaq.sample(
462
+ circuit_dict[layer]["physical"][basis],
463
+ shots_count=shots_count,
464
+ noise_model=noise_model,
465
+ )
466
+ tmp_logical_results = cudaq.sample(
467
+ circuit_dict[layer]["logical"][basis],
468
+ shots_count=shots_count,
469
+ noise_model=noise_model,
470
+ )
471
+ aim_results["physical"].append({k: v for k, v in tmp_physical_results.items()})
472
+ aim_results["logical"].append({k: v for k, v in tmp_logical_results.items()})
473
+ if not run_async:
474
+ print("\nCompleted all circuit sampling!")
475
+ return aim_results
476
+ else:
477
+ print("\nAll circuits submitted for async sampling!")
478
+
479
+ def _get_async_results(
480
+ layers: object, *, folder_path: str = "future_aim_results"
481
+ ) -> dict[str, list[dict[str, int]]]:
482
+ aim_results = {"physical": [], "logical": []}
483
+ for layer in layers:
484
+ print(f"Retrieving all circuits counts associated with layer=('{layer}')")
485
+ u, v = layer.split(":")
486
+ for basis in ("z_basis", "x_basis"):
487
+ file = open(f"{folder_path}/physical_{basis}_job_u={u}_v={v}_result.txt", "r")
488
+ tmp_physical_results = cudaq.AsyncSampleResult(str(file.read()))
489
+ physical_counts = tmp_physical_results.get()
490
+
491
+ file = open(f"{folder_path}/logical_{basis}_job_u={u}_v={v}_result.txt", "r")
492
+ tmp_logical_results = cudaq.AsyncSampleResult(str(file.read()))
493
+ logical_counts = tmp_logical_results.get()
494
+
495
+ aim_results["physical"].append({k: v for k, v in physical_counts.items()})
496
+ aim_results["logical"].append({k: v for k, v in logical_counts.items()})
497
+
498
+ print("\nObtained all circuit samples!")
499
+ return aim_results
500
+
501
+ """## Running a CUDA-Q noisy simulation
502
+
503
+ In this section, we will first explore the performance of the physical and logical circuits under the influence of a device noise model. This will help us predict experimental results, as well as understand the dominant error sources at play. Such a simulation can be achieved via CUDA-Q's density matrix simulator:
504
+ """
505
+
506
+ cudaq.reset_target()
507
+ cudaq.set_target("density-matrix-cpu")
508
+
509
+
510
+
511
+ def get_device_noise(
512
+ depolar_prob_1q: float,
513
+ depolar_prob_2q: float,
514
+ *,
515
+ readout_error_prob: float | None = None,
516
+ custom_gates: list[str] | None = None,
517
+ ) -> cudaq.mlir._mlir_libs._quakeDialects.cudaq_runtime.NoiseModel:
518
+ noise = cudaq.NoiseModel()
519
+ depolar_noise = cudaq.DepolarizationChannel(depolar_prob_1q)
520
+
521
+ noisy_ops = ["z", "s", "x", "h", "rx", "rz"]
522
+ for op in noisy_ops:
523
+ noise.add_all_qubit_channel(op, depolar_noise)
524
+
525
+ if custom_gates:
526
+ custom_depolar_channel = cudaq.DepolarizationChannel(depolar_prob_1q)
527
+ for op in custom_gates:
528
+ noise.add_all_qubit_channel(op, custom_depolar_channel)
529
+
530
+ # Two qubit depolarization error
531
+ p_0 = 1 - depolar_prob_2q
532
+ p_1 = np.sqrt((1 - p_0**2) / 3)
533
+
534
+ k0 = np.array(
535
+ [[p_0, 0.0, 0.0, 0.0], [0.0, p_0, 0.0, 0.0], [0.0, 0.0, p_0, 0.0], [0.0, 0.0, 0.0, p_0]],
536
+ dtype=np.complex128,
537
+ )
538
+ k1 = np.array(
539
+ [[0.0, 0.0, p_1, 0.0], [0.0, 0.0, 0.0, p_1], [p_1, 0.0, 0.0, 0.0], [0.0, p_1, 0.0, 0.0]],
540
+ dtype=np.complex128,
541
+ )
542
+ k2 = np.array(
543
+ [
544
+ [0.0, 0.0, -1j * p_1, 0.0],
545
+ [0.0, 0.0, 0.0, -1j * p_1],
546
+ [1j * p_1, 0.0, 0.0, 0.0],
547
+ [0.0, 1j * p_1, 0.0, 0.0],
548
+ ],
549
+ dtype=np.complex128,
550
+ )
551
+ k3 = np.array(
552
+ [[p_1, 0.0, 0.0, 0.0], [0.0, p_1, 0.0, 0.0], [0.0, 0.0, -p_1, 0.0], [0.0, 0.0, 0.0, -p_1]],
553
+ dtype=np.complex128,
554
+ )
555
+ kraus_channel = cudaq.KrausChannel([k0, k1, k2, k3])
556
+
557
+ noise.add_all_qubit_channel("cz", kraus_channel)
558
+ noise.add_all_qubit_channel("cx", kraus_channel)
559
+
560
+ if readout_error_prob is not None:
561
+ # Readout error modeled with a Bit flip channel on identity before measurement
562
+ bit_flip = cudaq.BitFlipChannel(readout_error_prob)
563
+ noise.add_all_qubit_channel("meas_id", bit_flip)
564
+ return noise
565
+
566
+ """Finally, with our example noise model defined above, we can synchronously & noisily sample all of our AIM circuits by passing `noise_model=cudaq_noise_model` to the workflow containing function `submit_aim_circuits()`:"""
567
+
568
+ # Example parameters that can model execution on hardware at the high, simulation, level:
569
+ # Take single-qubit gate depolarization rate: ~0.2% or better (fidelity ≥99.8%)
570
+ # Take two-qubit gate depolarization rate: ~1–2% (fidelity ~98–99%)
571
+ cudaq_noise_model = get_device_noise(0.002, 0.02, readout_error_prob=0.02)
572
+
573
+
574
+
575
+ aim_sim_data = submit_aim_circuits(sim_circuit_dict, noise_model=cudaq_noise_model)
576
+
577
+ data_ordering = []
578
+ for key in circuit_layers:
579
+ for basis in ("z_basis", "x_basis"):
580
+ data_ordering.append((key, basis))
581
+
582
+ sim_physical_energies, sim_physical_uncertainties = aim_physical_energies(
583
+ data_ordering, aim_sim_data["physical"]
584
+ )
585
+
586
+ sim_logical_energies, sim_logical_uncertainties = aim_logical_energies(
587
+ data_ordering, aim_sim_data["logical"]
588
+ )
589
+
590
+ """To analyze our simulated energy results in the above cells, we will compare them to the brute-force computed exact ground state energies for the AIM Hamiltonian. For simplicity, these are already stored in the dictionary `bf_energies` below:"""
591
+
592
+ bf_energies = {
593
+ "1:-9": -18.251736027394713,
594
+ "1:-1": -2.265564437074638,
595
+ "1:7": -14.252231964940428,
596
+ "5:-9": -19.293350575766127,
597
+ "5:-1": -3.608495283014149,
598
+ "5:7": -15.305692796870582,
599
+ "9:-9": -20.39007993367173,
600
+ "9:-1": -5.260398644698076,
601
+ "9:7": -16.429650912487233,
602
+ }
603
+
604
+ """With the above metric, we can assess the performance of the logical circuits against the physical circuits by considering how far away the respective energies are from the brute-force expected energies. The cell below computes these energy deviations:"""
605
+
606
+ sim_physical_energy_diff, sim_logical_energy_diff = _get_energy_diff(
607
+ bf_energies, sim_physical_energies, sim_logical_energies
608
+ )
609
+
610
+ """Both physical and logical circuits were subject to the same noise model, but the `[[4,2,2]]` provides additional information that can help overcome some errors. Visualizing the computed energy differences from the above the cell, our noisy simulation provides a preview of the benefits logical qubits can offer:"""
611
+
612
+ # Diagnostic block because next code block was getting stuck and throwing many errors
613
+ print("=== Creating layer labels ===")
614
+ layer_labels = [(int(key.split(":")[0]), int(key.split(":")[1])) for key in bf_energies.keys()]
615
+ print("=== ge nerating strings ===")
616
+ plot_labels = [str(item) for item in layer_labels]
617
+
618
+ print("=== Variable Type Check ===")
619
+ print(f"Type of sim_physical_energy_diff: {type(sim_physical_energy_diff)}")
620
+ print(f"Type of sim_physical_uncertainties: {type(sim_physical_uncertainties)}")
621
+
622
+ print("\n=== Values Preview ===")
623
+ print(f"bf_energies keys: {list(bf_energies.keys())[:5]}...") # First 5 keys
624
+ print(f"layer_labels: {layer_labels[:5]}...") # First 5 labels
625
+
626
+ print("\n=== Shape/Length Check ===")
627
+ print(f"Length of plot_labels: {len(plot_labels)}")
628
+
629
+ print(f"Length of sim_physical_energy_diff: {len(sim_physical_energy_diff) if hasattr(sim_physical_energy_diff, 'len') else 'N/A'}")
630
+
631
+ print(f"Number of uncertainties: {len(sim_physical_uncertainties.values()) if hasattr(sim_physical_uncertainties.values(), 'len') else 'N/A'}")
632
+ sim_physical_energy_diff
633
+
634
+ print("=== Examining sim_physical_uncertainties ===")
635
+ print(f"Type: {type(sim_physical_uncertainties)}")
636
+ print("\nFirst few key-value pairs:")
637
+ for i, (key, value) in enumerate(sim_physical_uncertainties.items()):
638
+ if i < 5: # Show first 5 items
639
+ print(f"{key}: {value}")
640
+ else:
641
+ break
642
+
643
+ print("\n=== Examining sim_physical_energy_diff ===")
644
+ print(f"Type: {type(sim_physical_energy_diff)}")
645
+ if hasattr(sim_physical_energy_diff, '__iter__'):
646
+ print("First few values:")
647
+ try:
648
+ print(list(sim_physical_energy_diff)[:5])
649
+ except:
650
+ print("Could not convert to list")
651
+
652
+ # Convert uncertainties to a list of values
653
+ physical_uncertainties = list(sim_physical_uncertainties.values())
654
+ logical_uncertainties = list(sim_logical_uncertainties.values())
655
+
656
+ # Convert energy differences if needed
657
+ if isinstance(sim_physical_energy_diff, dict):
658
+ energy_diff = list(sim_physical_energy_diff.values())
659
+ else:
660
+ energy_diff = sim_physical_energy_diff
661
+
662
+ fig, ax = plt.subplots(figsize=(11, 7), dpi=200)
663
+ print("1")
664
+ layer_labels = [(int(key.split(":")[0]), int(key.split(":")[1])) for key in bf_energies.keys()]
665
+ plot_labels = [str(item) for item in layer_labels]
666
+ print("here")
667
+ plt.errorbar(
668
+ plot_labels,
669
+ sim_physical_energy_diff,
670
+ yerr=physical_uncertainties,
671
+ ecolor=(20 / 255.0, 26 / 255.0, 94 / 255.0),
672
+ color=(20 / 255.0, 26 / 255.0, 94 / 255.0),
673
+ capsize=4,
674
+ elinewidth=1.5,
675
+ fmt="o",
676
+ markersize=8,
677
+ markeredgewidth=1,
678
+ label="Physical",
679
+ )
680
+ print("2")
681
+
682
+ plt.errorbar(
683
+ plot_labels,
684
+ sim_logical_energy_diff,
685
+ yerr=logical_uncertainties,
686
+ color=(0, 177 / 255.0, 152 / 255.0),
687
+ ecolor=(0, 177 / 255.0, 152 / 255.0),
688
+ capsize=4,
689
+ elinewidth=1.5,
690
+ fmt="o",
691
+ markersize=8,
692
+ markeredgewidth=1,
693
+ label="Logical",
694
+ )
695
+ print("3")
696
+
697
+ ax.set_xlabel("Hamiltonian Parameters (U, V)", fontsize=18)
698
+ ax.set_ylabel("Energy above true ground state (in eV)", fontsize=18)
699
+ ax.set_title("CUDA-Q AIM Circuits Simulation (lower is better)", fontsize=20)
700
+ ax.legend(loc="upper right", fontsize=18.5)
701
+ plt.xticks(fontsize=16)
702
+ plt.yticks(fontsize=16)
703
+
704
+ print("4")
705
+
706
+ ax.axhline(y=0, color="black", linestyle="--", linewidth=2)
707
+ plt.ylim(
708
+ top=max(sim_physical_energy_diff) + max(sim_physical_uncertainties.values()) + 0.2, bottom=-0.2
709
+ )
710
+ print("5")
711
+ plt.tight_layout()
712
+ plt.show()
docref/Example_plotly_code.md ADDED
File without changes
docref/H2example.md ADDED
File without changes
docref/VQE_Example.md ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ VQE with gradients, active spaces, and gate fusion¶
2
+ This tutorial will explore the Variational Quantum Eigensolver, a hybrid quantum classical algorithm for determining the ground state energy of molecules. The first part of this tutorial will walk through the key aspects of the VQE algorithm and how to implement it with CUDA-Q. The following sections explore advanced topics: parallel gradients, active spaces, and gate fusion
3
+
4
+ The Basics of VQE¶
5
+ The VQE algorithm is hybrid quantum-classical algorithm, meaning some subroutines run on a quantum computer or quantum simulator and others run on a traditional (super)computer.
6
+
7
+ The goal is to take a parameterized quantum circuit and a qubit form of the molecular Hamiltonian, measure an expectation value that corresponds to the ground state energy of the molecule, and then repeat the process to variationally minimize the energy with respect to the parameters in the quantum circuit. The optimization is performed on a classical device while the expectation values are determined on a quantum device. See the figure below.
8
+
9
+ VQE.png
10
+
11
+ The next few cells will elaborate on each part of the VQE procedure and show you how to build a VQE simulation to compute the ground state energy of the water molecule.
12
+
13
+ Installing/Loading Relevant Packages¶
14
+ # Install the relevant packages.
15
+ !pip install pyscf==2.6.2 openfermionpyscf==0.5 matplotlib==3.8.4 openfermion==1.6.1 -q
16
+ import openfermion
17
+ import openfermionpyscf
18
+ from openfermion.transforms import jordan_wigner, get_fermion_operator
19
+
20
+ import os
21
+ import timeit
22
+
23
+ import cudaq
24
+ import matplotlib.pyplot as plt
25
+ from scipy.optimize import minimize
26
+ import numpy as np
27
+ Implementing VQE in CUDA-Q¶
28
+ Like most quantum chemistry programs, the first step is to specify a molecular geometry, basis set, charge, and multiplicity.
29
+
30
+ geometry = [('O', (0.1173, 0.0, 0.0)), ('H', (-0.4691, 0.7570, 0.0)),
31
+ ('H', (-0.4691, -0.7570, 0.0))]
32
+ basis = 'sto3g'
33
+ multiplicity = 1
34
+ charge = 0
35
+ The VQE procedure requires some classical preprocessing. The code below uses the PySCF package and OpenFermion to compute the Hartree Fock reference state and compute the integrals required for the Hamiltonian.
36
+
37
+ molecule = openfermionpyscf.run_pyscf(
38
+ openfermion.MolecularData(geometry, basis, multiplicity, charge))
39
+ Next, the Hamiltonian is built using get_molecular_hamiltonian. The Hamiltonian must then be converted to a qubit Hamiltonian consisting of qubit operators. The standard Jordan-Wigner transformation is used to perform this mapping.
40
+
41
+ Finally, the Jordan-Wigner qubit Hamiltonian is converted into a CUDA-Q spin operator which can be used to evaluate an expectation value given a quantum circuit.
42
+
43
+ molecular_hamiltonian = molecule.get_molecular_hamiltonian()
44
+
45
+ fermion_hamiltonian = get_fermion_operator(molecular_hamiltonian)
46
+
47
+ qubit_hamiltonian = jordan_wigner(fermion_hamiltonian)
48
+
49
+ spin_ham = cudaq.SpinOperator(qubit_hamiltonian)
50
+ Next, the quantum circuit needs to be defined, which models the wavefunction. This is done in CUDA-Q by specifying a CUDA-Q kernel. The kernel takes as an input the number of qubits, the number of electrons, and the parameters of the circuit ansatz (form of the wavefunction) yet to be defined.
51
+
52
+ The number of qubits corresponds to the potential positions of electrons and is therefore twice the number of spatial orbitals constructed with the chosen basis set, as each can be occupied by two electrons.
53
+
54
+ The Hartree-Fock reference is constructed by applying
55
+ bitflip operations to each of the first
56
+ qubits where
57
+ is the number of electrons. Next, a parameterized ansatz is chosen. Theoretically, any set of operations can work as an ansatz, however, it is good practice to use an ansatz that captures the underlying physics of the problem. The most common choice for chemistry is the Unitary Coupled Cluster Ansatz with Single and Double excitations (UCCSD). This UCCSD hate operations are automatically added to the kernel with the cudaq.kernels.uccsd(qubits, thetas, electron_num, qubit_num) function.
58
+
59
+ The STO-3G water molecuule UCCSD ansatz requires optimization of 140 parameters.
60
+
61
+ electron_count = 10
62
+ qubit_count = 2 * 7
63
+
64
+
65
+ @cudaq.kernel
66
+ def kernel(qubit_num: int, electron_num: int, thetas: list[float]):
67
+ qubits = cudaq.qvector(qubit_num)
68
+
69
+ for i in range(electron_num):
70
+ x(qubits[i])
71
+
72
+ cudaq.kernels.uccsd(qubits, thetas, electron_num, qubit_num)
73
+
74
+
75
+ parameter_count = cudaq.kernels.uccsd_num_parameters(electron_count,
76
+ qubit_count)
77
+
78
+ print(parameter_count)
79
+ 140
80
+ The classical optimizer requires a custom cost function which is defined below. The cudaq.observe() function computes an expectation given the Hamiltonian and the kernel defined above.
81
+
82
+ def cost(theta):
83
+
84
+ exp_val = cudaq.observe(kernel, spin_ham, qubit_count, electron_count,
85
+ theta).expectation()
86
+
87
+ return exp_val
88
+
89
+
90
+ exp_vals = []
91
+
92
+
93
+ def callback(xk):
94
+ exp_vals.append(cost(xk))
95
+
96
+
97
+ # Initial variational parameters.
98
+ np.random.seed(42)
99
+ x0 = np.random.normal(0, 1, parameter_count)
100
+ The final step is to run the optimization using the scipy minimize function and a selected optimizer, in this case COBYLA.
101
+
102
+ cudaq.set_target('nvidia')
103
+ start_time = timeit.default_timer()
104
+ result = minimize(cost,
105
+ x0,
106
+ method='COBYLA',
107
+ callback=callback,
108
+ options={'maxiter': 50})
109
+ end_time = timeit.default_timer()
110
+
111
+ print('UCCSD-VQE energy = ', result.fun)
112
+ print('Total number of qubits = ', qubit_count)
113
+ print('Total number of parameters = ', parameter_count)
114
+ print('Total number of terms in the spin hamiltonian = ',
115
+ spin_ham.get_term_count())
116
+ print('Total elapsed time (s) = ', end_time - start_time)
117
+
118
+ plt.plot(exp_vals)
119
+ plt.xlabel('Epochs')
120
+ plt.ylabel('Energy')
121
+ plt.title('VQE')
122
+ plt.show()
123
+ UCCSD-VQE energy = -70.21455023422772
124
+ Total number of qubits = 14
125
+ Total number of parameters = 140
126
+ Total number of terms in the spin hamiltonian = 1086
127
+ Total elapsed time (s) = 3.9171073289999185
128
+ ../../_images/applications_python_vqe_advanced_21_1.png
129
+ The result of this procedure is an estimate of the ground state energy of water. However, the convergence behavior is not perfect, more iterations would greatly improve the result, but would take a few minutes to run.
130
+
131
+ Parallel Parameter Shift Gradients¶
132
+ One way to accelerate VQE is to use an optimizer that accepts a gradient. This can drastically lower the number of VQE iterations required at the cost of computing the gradient on the quantum side of the algorithm.
133
+
134
+ The parameter shift rule is a common technique to compute the gradient for parameterized circuits. It is obtained by computing two expectation values for each parameter corresponding to a small forward and backward shift in the ith parameter. These results are used to estimate finite difference contribution to the gradient.
135
+
136
+ parametershift.png
137
+
138
+ This procedure can become cost prohibitive as the number of parameters becomes large.
139
+
140
+ Each of the expectation values needed to evaluate a parameter shift gradient can be computed independently. The CUDA-Q nvidia-mqpu backend is designed for parallel computations across multiple simulated QPUs. The function below uses cudaq.observe_asynch to distribute all of the expectation values evaluations across as many GPUs that are available. First, try it with num_qpus set to 1.
141
+
142
+ np.random.seed(42)
143
+ x0 = np.random.normal(0, 1, parameter_count)
144
+
145
+ cudaq.set_target("nvidia-mqpu")
146
+
147
+ num_qpus = cudaq.get_target().num_qpus()
148
+
149
+ epsilon = np.pi / 4
150
+
151
+
152
+ def batched_gradient_function(kernel, parameters, hamiltonian, epsilon):
153
+
154
+ x = np.tile(parameters, (len(parameters), 1))
155
+
156
+ xplus = x + (np.eye(x.shape[0]) * epsilon)
157
+
158
+ xminus = x - (np.eye(x.shape[0]) * epsilon)
159
+
160
+ g_plus = []
161
+ g_minus = []
162
+ gradients = []
163
+
164
+ qpu_counter = 0 # Iterate over the number of GPU resources available
165
+ for i in range(x.shape[0]):
166
+
167
+ g_plus.append(
168
+ cudaq.observe_async(kernel,
169
+ hamiltonian,
170
+ qubit_count,
171
+ electron_count,
172
+ xplus[i],
173
+ qpu_id=qpu_counter%num_qpus))
174
+ qpu_counter += 1
175
+
176
+ g_minus.append(
177
+ cudaq.observe_async(kernel,
178
+ hamiltonian,
179
+ qubit_count,
180
+ electron_count,
181
+ xminus[i],
182
+ qpu_id=qpu_counter%num_qpus))
183
+ qpu_counter += 1
184
+
185
+ gradients = [
186
+ (g_plus[i].get().expectation() - g_minus[i].get().expectation()) /
187
+ (2 * epsilon) for i in range(len(g_minus))
188
+ ]
189
+
190
+ assert len(gradients) == len(
191
+ parameters) == x.shape[0] == xplus.shape[0] == xminus.shape[0]
192
+
193
+ return gradients
194
+ [warning] Target nvidia-mqpu: This target is deprecating. Please use the 'nvidia' target with option 'mqpu,fp32' or 'mqpu' (fp32 is the default precision option) by adding the command line option '--target-option mqpu,fp32' or passing it as cudaq.set_target('nvidia', option='mqpu,fp32') in Python. Please refer to CUDA-Q ]8;;https://nvidia.github.io/cuda-quantum/latest/using/backends/platform\documentation]8;;\ for more information.
195
+ The cost function needs to be slightly updated to make use of the gradient in the optimization procedure and allow for a gradient based optimizer like L-BFGS-B to be used.
196
+
197
+ gradient = batched_gradient_function(kernel, x0, spin_ham, epsilon)
198
+
199
+ exp_vals = []
200
+
201
+
202
+ def objective_function(parameter_vector: list[float], \
203
+ gradient=gradient, hamiltonian=spin_ham, kernel=kernel):
204
+
205
+ get_result = lambda parameter_vector: cudaq.observe\
206
+ (kernel, hamiltonian, qubit_count, electron_count, parameter_vector).expectation()
207
+
208
+ cost = get_result(parameter_vector)
209
+ exp_vals.append(cost)
210
+ gradient_vector = batched_gradient_function(kernel, parameter_vector,
211
+ spin_ham, epsilon)
212
+
213
+ return cost, gradient_vector
214
+ Run the code below. Notice how the result is converged to a lower energy using only 10% of the steps as optimization above without a gradient.
215
+
216
+ np.random.seed(42)
217
+ init_params = np.random.normal(0, 1, parameter_count)
218
+
219
+ start_time = timeit.default_timer()
220
+ result_vqe = minimize(objective_function,
221
+ init_params,
222
+ method='L-BFGS-B',
223
+ jac=True,
224
+ tol=1e-8,
225
+ options={'maxiter': 5})
226
+ end_time = timeit.default_timer()
227
+
228
+ print('VQE-UCCSD energy= ', result_vqe.fun)
229
+ print('Total elapsed time (s) = ', end_time - start_time)
230
+
231
+ plt.plot(exp_vals)
232
+ plt.xlabel('Epochs')
233
+ plt.ylabel('Energy')
234
+ plt.title('VQE')
235
+ plt.show()
236
+ VQE-UCCSD energy= -73.19471262288755
237
+ Total elapsed time (s) = 57.27010986900132
238
+ ../../_images/applications_python_vqe_advanced_30_1.png
239
+ Now, run the code again (the three previous cells) and specify num_qpus to be more than one if you have access to multiple GPUs and notice resulting speedup. Thanks to CUDA-Q, this code could be used without modification in a setting where multiple physical QPUs were availible.
240
+
241
+ Using an Active Space¶
242
+ Performing electronic structure computations with all electrons and orbitals is often prohibitively expensive and unnecessary. Most of the interesting chemistry can be modeled by restricting simulations to the highest energy occupied molecular orbitals and lowest energy unoccupied molecular orbitals. This is known as the active space approximation.
243
+
244
+ Below is an example of STO-3G water modeled with a 4 electron 3 orbital active space simulated with UCCSD-VQE. Using an active space means you can run VQE for the same molecule using fewer qubits and a more shallow circuit.
245
+
246
+ cas.png
247
+
248
+ The molecule is defined the same way, expect for you now include variables nele_cas and norb_cas to define the active space. The ncore
249
+
250
+ geometry = [('O', (0.1173, 0.0, 0.0)), ('H', (-0.4691, 0.7570, 0.0)),
251
+ ('H', (-0.4691, -0.7570, 0.0))]
252
+ basis = 'sto3g'
253
+ multiplicity = 1
254
+ charge = 0
255
+ ncore = 3
256
+ nele_cas, norb_cas = (4, 3)
257
+
258
+ molecule = openfermionpyscf.run_pyscf(
259
+ openfermion.MolecularData(geometry, basis, multiplicity, charge))
260
+ The Hamiltonian is now constrcuted with the same steps, but only models the active space.
261
+
262
+ molecular_hamiltonian = molecule.get_molecular_hamiltonian(
263
+ occupied_indices=range(ncore),
264
+ active_indices=range(ncore, ncore + norb_cas))
265
+
266
+ fermion_hamiltonian = get_fermion_operator(molecular_hamiltonian)
267
+
268
+ qubit_hamiltonian = jordan_wigner(fermion_hamiltonian)
269
+
270
+ spin_ham = cudaq.SpinOperator(qubit_hamiltonian)
271
+ Similarly, the kernel is defined only by the orbitals and electrons in the active space. Notice how this means you only need to optimize 8 parameters now.
272
+
273
+ electron_count = nele_cas
274
+ qubit_count = 2 * norb_cas
275
+
276
+
277
+ @cudaq.kernel
278
+ def kernel(qubit_num: int, electron_num: int, thetas: list[float]):
279
+ qubits = cudaq.qvector(qubit_num)
280
+
281
+ for i in range(electron_num):
282
+ x(qubits[i])
283
+
284
+ cudaq.kernels.uccsd(qubits, thetas, electron_num, qubit_num)
285
+
286
+
287
+ parameter_count = cudaq.kernels.uccsd_num_parameters(electron_count,
288
+ qubit_count)
289
+
290
+ print(parameter_count)
291
+ 8
292
+ def cost(theta):
293
+
294
+ exp_val = cudaq.observe(kernel, spin_ham, qubit_count, electron_count,
295
+ theta).expectation()
296
+ thetas = theta
297
+ return exp_val
298
+
299
+
300
+ exp_vals = []
301
+
302
+
303
+ def callback(xk):
304
+ exp_vals.append(cost(xk))
305
+
306
+
307
+ # Initial variational parameters.
308
+ np.random.seed(42)
309
+ x0 = np.random.normal(0, 1, parameter_count)
310
+ The VQE procedure below is much faster using an active space compared to inclusion of all orbitals and electrons.
311
+
312
+ cudaq.set_target("nvidia")
313
+
314
+ start_time = timeit.default_timer()
315
+ result = minimize(cost,
316
+ x0,
317
+ method='COBYLA',
318
+ callback=callback,
319
+ options={'maxiter': 500})
320
+ end_time = timeit.default_timer()
321
+
322
+ print('UCCSD-VQE energy = ', result.fun)
323
+ print('Total number of qubits = ', qubit_count)
324
+ print('Total number of parameters = ', parameter_count)
325
+ print('Total number of terms in the spin hamiltonian = ',
326
+ spin_ham.get_term_count())
327
+ print('Total elapsed time (s) = ', end_time - start_time)
328
+
329
+ plt.plot(exp_vals)
330
+ plt.xlabel('Epochs')
331
+ plt.ylabel('Energy')
332
+ plt.title('VQE')
333
+ plt.show()
334
+ UCCSD-VQE energy = -74.96341992791962
335
+ Total number of qubits = 6
336
+ Total number of parameters = 8
337
+ Total number of terms in the spin hamiltonian = 62
338
+ Total elapsed time (s) = 1.754178541001238
339
+ ../../_images/applications_python_vqe_advanced_43_1.png
340
+ Gate Fusion for Larger Circuits¶
341
+ CUDA-Q simulations take advantage of a technique called gate fusion. Gate fusion is an optimization technique where consecutive gates are combined into a single gate operation to improve the efficiency of the simulation (See figure below). By targeting the nvidia-mgpu backend and setting the CUDAQ_MGPU_FUSE environment variable, you can select the degree of fusion that takes place.
342
+
343
+ gate-fuse.png
344
+
345
+ This is particularly important for larger circuits and can have a significant impact on the performance of the simulation. Each system is different, so you should test different gate fusion levels to find out what is best for your system. You can find more information here.
docref/molgallery_demo.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The gradio_molgallery3d package is a custom component for Gradio, designed specifically for displaying an interactive gallery of 3D molecular structures. Here are some key features and details:
2
+ Installation: You can install this component using pip install gradio_molgallery3d.
3
+ Input: It accepts inputs in the form of RDKit objects, which are commonly used for cheminformatics and molecular modeling.
4
+ Features:
5
+ Automatic Rotation: When the automatic_rotation parameter is set to True, the molecular structures rotate automatically, providing a dynamic view of the molecule.
6
+ Download Capability: Users can download the 3D structure as a .png file by clicking the right mouse button on the structure.
7
+ Known Issues: Web browsers have limitations on the number of canvas objects that can be displayed on a single page, which might result in some structures not being drawn. This limitation varies by browser.
8
+ Example Usage for Visualizing H2O:
9
+ Here's a code snippet to show how you can use gradio_molgallery3d to visualize the H2O molecule:
10
+
11
+
12
+ import gradio as gr
13
+ from rdkit import Chem
14
+ from gradio_molgallery import MolGallery3D
15
+
16
+ # Create an RDKit molecule object for H2O
17
+ h2o_mol = Chem.MolFromSmiles('O')
18
+
19
+ # Set up the gallery
20
+ gallery3d = MolGallery3D(
21
+ columns=1, # Since we're showing only one molecule
22
+ height=400, # Adjust the height as needed
23
+ label="3D Structure of H2O",
24
+ automatic_rotation=True
25
+ )
26
+
27
+ # Define a function to return the molecule
28
+ def show_h2o():
29
+ return [h2o_mol]
30
+
31
+ # Create the Gradio interface
32
+ with gr.Blocks() as demo:
33
+ gr.Markdown("# 3D Visualization of H2O")
34
+ gallery = gallery3d
35
+ btn = gr.Button("Show H2O")
36
+ btn.click(fn=show_h2o, outputs=gallery)
37
+
38
+ # Launch the demo
39
+ demo.launch()
docref/py3dmol_demo.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Here's a sample code to show an H2 molecule and a water molecule in a py3Dmol flow inside a Gradio display:
2
+
3
+ Explanation:
4
+ Molecule Creation: The molecule_viewer function takes a molecule_type parameter to determine which molecule to visualize. It uses RDKit to create the molecule from SMILES strings, add hydrogens, and embed the molecule in 3D space.
5
+ Visualization: The molecule is then converted to a MolBlock format, which py3Dmol can read. The view object is created with the molecule data, and styles are applied to show the molecule in stick and sphere representations.
6
+ Gradio Interface: The Gradio interface allows users to select between H2 and H2O molecules using a radio button. The selected molecule is then visualized in the HTML output.
7
+ Launch: The Gradio app is launched, providing an interactive web interface where users can view the 3D models of the selected molecules.
8
+ This code provides a simple yet effective way to visualize H2 and H2O molecules in 3D within a Gradio interface. Users can rotate, zoom, and interact with the molecules to explore their structures.
9
+
10
+
11
+ import gradio as gr
12
+ from rdkit import Chem
13
+ from rdkit.Chem import AllChem
14
+ import py3Dmol
15
+
16
+ def molecule_viewer(molecule_type):
17
+ if molecule_type == "H2":
18
+ # Create an H2 molecule
19
+ h2 = Chem.MolFromSmiles("H[H]")
20
+ h2 = AllChem.AddHs(h2)
21
+ AllChem.EmbedMolecule(h2)
22
+ mol_block = Chem.MolToMolBlock(h2)
23
+ elif molecule_type == "H2O":
24
+ # Create a water molecule
25
+ water = Chem.MolFromSmiles("O")
26
+ water = AllChem.AddHs(water)
27
+ AllChem.EmbedMolecule(water)
28
+ mol_block = Chem.MolToMolBlock(water)
29
+ else:
30
+ return "Invalid molecule type"
31
+
32
+ # Create the 3Dmol view
33
+ view = py3Dmol.view(data=mol_block, style={"stick": {}, "sphere": {"scale": 0.3}})
34
+ view.zoomTo()
35
+ return view
36
+
37
+ # Define the Gradio interface
38
+ demo = gr.Interface(
39
+ fn=molecule_viewer,
40
+ inputs=gr.Radio(choices=["H2", "H2O"], label="Select Molecule"),
41
+ outputs=gr.HTML(label="3D Molecule Viewer")
42
+ )
43
+
44
+ # Launch the Gradio app
45
+ demo.launch()
docref/quantum-demo-blueprint.md ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NVIDIA Quantum Library Gradio Demo - Project Blueprint
2
+
3
+ ## Overview
4
+ This project creates an interactive web demo showcasing NVIDIA's CUDA Quantum Library (CUDA-Q) for quantum computations. The demo focuses on the Variational Quantum Eigensolver (VQE) algorithm for molecular energy estimation, providing users with an educational interface for quantum control and simulation.
5
+
6
+ ## Project Objectives
7
+ - Create an intuitive, interactive demo of quantum computations using CUDA-Q
8
+ - Visualize molecular energy estimation using the VQE algorithm
9
+ - Provide real-time feedback on quantum simulation parameters
10
+ - Educate users about quantum chemistry concepts
11
+
12
+ ## Core Features
13
+
14
+ ### Molecule Selection
15
+ - Simple molecule options: H₂, LiH, and HeH⁺
16
+ - Intuitive dropdown or button-based selection interface
17
+ - Expandable framework for additional molecules
18
+
19
+ ### Parameter Controls
20
+ - **Bond Length Control**
21
+ - Range: 0.5 Å to 2.5 Å
22
+ - Real-time updates of molecular geometry
23
+ - Continuous slider interface
24
+
25
+ - **Bond Angle Adjustment**
26
+ - Range: 90° to 180° (for multi-atom molecules)
27
+ - Dynamic updates of molecular structure
28
+ - Visual feedback on angle changes
29
+
30
+ ### Visualization Components
31
+ - **Energy Convergence Display**
32
+ - Real-time line plot showing VQE optimization
33
+ - Interactive zoom and pan capabilities
34
+ - Clear indication of convergence points
35
+
36
+ - **3D Molecular Visualization**
37
+ - Dynamic molecular structure rendering
38
+ - Rotation and zoom capabilities
39
+ - Real-time updates with parameter changes
40
+
41
+ ### Backend Features
42
+ - CUDA-Q integration for quantum calculations
43
+ - VQE algorithm implementation
44
+ - GPU-accelerated computations
45
+ - Real-time parameter optimization
46
+
47
+ ## Technology Stack
48
+
49
+ ### Frontend Framework
50
+ - **Gradio**
51
+ - Version: Latest stable
52
+ - Purpose: Web interface construction
53
+ - Key components: Sliders, buttons, plots
54
+
55
+ - **Bokeh**
56
+ - Version: Latest stable
57
+ - Purpose: Interactive plotting
58
+ - Features: Real-time updates, zoom, pan
59
+
60
+ ### Quantum Computing Stack
61
+ - **NVIDIA CUDA Quantum Library**
62
+ - Core quantum simulation engine
63
+ - GPU acceleration support
64
+ - Hybrid quantum-classical optimization
65
+
66
+ - **Supporting Libraries**
67
+ - PySCF (optional): Molecular Hamiltonian preparation
68
+ - Qiskit (optional): Additional quantum tools
69
+
70
+ ### Visualization Tools
71
+ - **ASE (Atomic Simulation Environment)**
72
+ - Molecular geometry handling
73
+ - Structure visualization
74
+ - Real-time updates
75
+
76
+ - **Matplotlib**
77
+ - Backup plotting capability
78
+ - Static visualizations
79
+ - Export functionality
80
+
81
+ ### Core Dependencies
82
+ - NumPy (≥1.20.0)
83
+ - SciPy (≥1.7.0)
84
+ - CUDA Toolkit (≥11.0)
85
+ - Python (≥3.8)
86
+
87
+ ## Implementation Guide
88
+
89
+ ### 1. Environment Setup
90
+ ```bash
91
+ # Core dependencies
92
+ pip install gradio>=3.50.0 bokeh>=3.0.0 matplotlib>=3.5.0
93
+ pip install ase>=3.22.0 numpy>=1.20.0 scipy>=1.7.0
94
+
95
+ # NVIDIA components
96
+ # Follow NVIDIA documentation for CUDA Toolkit installation
97
+ # Install CUDA-Q as per NVIDIA guidelines
98
+ ```
99
+
100
+ ### 2. Molecular System Implementation
101
+ ```python
102
+ # Example molecular system setup
103
+ def setup_molecule(name: str, bond_length: float):
104
+ molecules = {
105
+ 'H2': create_h2_molecule,
106
+ 'LiH': create_lih_molecule,
107
+ 'HeH+': create_heh_molecule
108
+ }
109
+ return molecules[name](bond_length)
110
+ ```
111
+
112
+ ### 3. VQE Algorithm Setup
113
+ ```python
114
+ # Basic VQE implementation structure
115
+ class VQESimulation:
116
+ def __init__(self, molecule, ansatz):
117
+ self.molecule = molecule
118
+ self.ansatz = ansatz
119
+
120
+ # Implementation using CUDA-Q
121
+ result = optimizer.minimize(self.cost_function, initial_parameters, callback=self.callback)
122
+ return result
123
+ def cost_function(self, parameters):
124
+ # Compute expectation value
125
+ exp_val = cudaq.observe(self.ansatz, self.molecule.hamiltonian, parameters).expectation()
126
+ return exp_val
127
+
128
+ def callback(self, parameters):
129
+ # Optionally store or display intermediate results
130
+ pass
131
+ ```
132
+
133
+
134
+
135
+ ### 4. Ansatz Implementation
136
+ ```python
137
+ # Example UCCSD Ansatz implementation
138
+ def create_uccsd_ansatz(qubit_num: int, electron_num: int):
139
+ @cudaq.kernel
140
+ def kernel(qubit_num: int, electron_num: int, thetas: list[float]):
141
+ qubits = cudaq.qvector(qubit_num)
142
+ for i in range(electron_num):
143
+ x(qubits[i])
144
+ cudaq.kernels.uccsd(qubits, thetas, electron_num, qubit_num)
145
+ return kernel
146
+ Cost function using CUDA-Q observe
147
+ def cost(theta, kernel, hamiltonian, qubit_count, electron_count):
148
+ exp_val = cudaq.observe(kernel, hamiltonian, qubit_count, electron_count, theta).expectation()
149
+ return exp_val
150
+ Optimization procedure
151
+ def run_optimization(initial_params, kernel, hamiltonian, qubit_count, electron_count):
152
+ from scipy.optimize import minimize
153
+ exp_vals = []
154
+ def callback(xk):
155
+ exp_vals.append(cost(xk, kernel, hamiltonian, qubit_count, electron_count))
156
+ result = minimize(
157
+ cost,
158
+ initial_params,
159
+ args=(kernel, hamiltonian, qubit_count, electron_count),
160
+ method='COBYLA',
161
+ callback=callback,
162
+ options={'maxiter': 500}
163
+ )
164
+ return result, exp_vals
165
+ UI
166
+ python:src/ui.py
167
+ Gradio interface setup with VQE integration
168
+ def create_interface(vqe_simulation):
169
+ with gr.Blocks() as demo:
170
+ gr.Markdown("# Quantum Chemistry Simulator")
171
+ # Molecule Selection
172
+ molecule = gr.Dropdown(choices=['H2', 'LiH', 'HeH+'], label="Select Molecule")
173
+ # Parameter Controls
174
+ bond_length = gr.Slider(0.5, 2.5, step=0.1, label="Bond Length (Å)")
175
+ bond_angle = gr.Slider(90, 180, step=1, label="Bond Angle (°)")
176
+ electric_field = gr.Slider(0, 10, step=0.1, label="Electric Field (V/nm)")
177
+ # Run VQE Button
178
+ run_button = gr.Button("Run VQE")
179
+ # Output Display
180
+ energy_output = gr.Textbox(label="Computed Energy")
181
+ convergence_plot = gr.Plot(label="Energy Convergence")
182
+ def run_vqe(selected_molecule, bond_len, bond_ang, e_field):
183
+ molecule = setup_molecule(selected_molecule, bond_len)
184
+ # Update molecular system parameters based on UI inputs
185
+ vqe_simulation = VQESimulation(molecule, ansatz)
186
+ initial_params = np.random.normal(0, 1, vqe_simulation.ansatz.parameter_count)
187
+ result, exp_vals = run_optimization(initial_params, vqe_simulation.ansatz, molecule.hamiltonian, molecule.qubit_count, molecule.electron_count)
188
+ energy = result.fun
189
+ # Plot convergence
190
+ plt.plot(exp_vals)
191
+ plt.xlabel('Iterations')
192
+ plt.ylabel('Energy')
193
+ plt.title('VQE Energy Convergence')
194
+ return energy, plt
195
+ run_button.click(run_vqe, inputs=[molecule, bond_length, bond_angle, electric_field], outputs=[energy_output, convergence_plot])
196
+ return demo
197
+ integration
198
+
199
+ ### 7. Advanced VQE Features
200
+ - **Parallel Gradients:** Implement parameter shift rule with parallel computations using multiple QPUs for gradient calculations to accelerate optimization.
201
+ - **Active Space Reduction:** Allow users to select active space parameters, reducing the number of qubits and parameters for larger molecules, enhancing performance.
202
+ - **Gate Fusion:** Optimize larger circuits by implementing gate fusion techniques to improve simulation efficiency on GPU backends.
203
+
204
+
205
+ ## Deployment Workflow
206
+
207
+ ### Local Development
208
+ 1. Clone repository
209
+ 2. Install dependencies
210
+ 3. Configure CUDA environment
211
+ 4. Run development server
212
+
213
+ ### Production Deployment
214
+ 1. Package application
215
+ 2. Verify CUDA compatibility
216
+ 3. Deploy to target platform
217
+ 4. Configure monitoring
218
+
219
+ ## Testing Strategy
220
+
221
+ ### Unit Tests
222
+ - Molecular system creation
223
+ - Parameter validation
224
+ - Energy calculation accuracy
225
+
226
+ ### Integration Tests
227
+ - UI component interaction
228
+ - Data flow verification
229
+ - GPU utilization
230
+
231
+ ### Performance Tests
232
+ - Load testing
233
+ - GPU memory usage
234
+ - Calculation speed
235
+
236
+ ## Future Enhancements
237
+ - Support for larger molecular systems
238
+ - Advanced visualization features
239
+ - Additional quantum algorithms
240
+ - Export functionality for results
241
+ - Multi-GPU support
242
+
243
+ ## Documentation
244
+ - User guide
245
+ - API documentation
246
+ - Installation instructions
247
+ - Troubleshooting guide
248
+
249
+ ## Maintenance
250
+ - Regular dependency updates
251
+ - Performance optimization
252
+ - Bug fixes and improvements
253
+ - User feedback integration
254
+
gitpush.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Check if a commit message was provided
4
+ if [ $# -eq 0 ]; then
5
+ echo "Error: Please provide a commit message"
6
+ echo "Usage: ./git_push.sh \"Your commit message\""
7
+ exit 1
8
+ fi
9
+
10
+ # Store the commit message
11
+ commit_message="$1"
12
+
13
+ # Echo and execute git add
14
+ echo "➡️ Adding all files..."
15
+ git add .
16
+
17
+ # Echo and show status
18
+ echo -e "\n📋 Current git status:"
19
+ git status
20
+
21
+ # Echo and commit with provided message
22
+ echo -e "\n💾 Committing changes with message: '$commit_message'"
23
+ git commit -m "$commit_message"
24
+
25
+ # Echo and push to main
26
+ echo -e "\n⬆️ Pushing to main branch..."
27
+ git push origin main
28
+
29
+ echo -e "\n✅ Done!"
history.md ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Development History
2
+
3
+ ## 2024-01-06 (Update 9)
4
+ - Fixed scale factor slider behavior:
5
+ * Added initial placeholder text for visualization and results
6
+ * Removed all automatic updates except molecule description
7
+ * Made scale factor slider explicitly interactive
8
+ * Added state management for molecule data
9
+ * Improved user feedback by showing "Click Run" messages
10
+
11
+ ## 2024-01-06 (Update 8)
12
+ - Improved interface stability for multi-molecule support:
13
+ * Removed automatic simulation updates on parameter changes
14
+ * Combined molecule info updates into single atomic operation
15
+ * Fixed component creation order and initialization
16
+ * Added proper error handling for molecule selection
17
+ * Addressed "Casting complex values to real" warning (normal for quantum chemistry)
18
+
19
+ ## 2024-01-06 (Update 7)
20
+ - Major update to handle molecular geometry scaling:
21
+ * Replaced bond length with universal scale factor for all molecules
22
+ * Added expand_geometry function to scale molecules from their center
23
+ * Updated molecules.json to use scale ranges (0.1 to 3.0) instead of bond lengths
24
+ * Fixed molecules.json structure by removing extra nesting level
25
+ * Added dynamic scale factor range updates in UI
26
+ * Updated all molecule descriptions to reflect scaling approach
27
+
28
+ ## 2024-01-06 (Update 6)
29
+ - Major restructuring to support multiple molecules:
30
+ * Created molecules.json to store all molecule metadata
31
+ * Updated app.py to load molecule data and create dynamic interface
32
+ * Modified quantum_utils.py to use molecule metadata instead of hardcoded values
33
+ * Updated visualization.py to use molecule data from JSON
34
+ * Added molecule descriptions and dynamic bond length ranges
35
+ * Improved error handling across all components
36
+
37
+ ## 2024-01-06 (Update 5)
38
+ - Fixed 3D molecule viewer to properly render in Gradio
39
+ - Modified create_molecule_viewer to return HTML string instead of view object
40
+ - Added explicit width and height to py3Dmol view
41
+ - Added more detailed logging for visualization debugging
42
+
43
+ ## 2024-01-06 (Update 4)
44
+ - Simplified molecule visualization to match py3dmol_demo.md exactly
45
+ - Fixed SMILES format for H2 molecule to "[H][H]"
46
+ - Removed overcomplicated bond length adjustment code
47
+ - Fixed py3Dmol view creation parameters
48
+
49
+ ## 2024-01-06 (Update 3)
50
+ - Completely revamped molecule visualization to use RDKit for proper molecular structure generation
51
+ - Added RDKit dependency to requirements.txt
52
+ - Updated create_molecule_viewer to handle proper 3D coordinates and bond length adjustments
53
+ - Improved molecule viewer to return py3Dmol view object directly instead of HTML
54
+
55
+ ## 2024-01-06 (Update 2)
56
+ - Fixed py3Dmol viewer in Gradio by using _repr_html_() instead of _make_html()
57
+ - This resolved the JupyterLab extension error message in the 3D viewer
58
+
59
+ ## 2024-01-06
60
+ - Fixed import error by updating app.py to import create_molecule_viewer instead of draw_molecule_2d
61
+ - This resolved the application crash that occurred after the 3D visualization update
62
+
63
+ ## 2024-01-04 (Update 9)
64
+ - Successfully integrated HuggingFace ZeroGPU functionality with CUDA-Q
65
+ - First known working CUDA-Q demo on HuggingFace Spaces platform
66
+ - Implemented @spaces.GPU decorator for GPU-dependent functions
67
+ - Added appropriate duration parameter (120s) for VQE simulation
68
+ - Historic milestone: First functional CUDA-Q HuggingFace Space demo by Alex Krause and... possibly ever, he found no other examples of this.
69
+ - Current Status: Fully functional GPU-accelerated quantum simulation with dynamic GPU allocation
70
+
71
+ ## 2024-01-04 (Update 8)
72
+ - Completely refactored visualization system to use Plotly instead of Matplotlib
73
+ - Removed complex base64 encoding/file handling for plots
74
+ - Switched to direct Plotly integration with Gradio's Plot component
75
+ - Added interactive features to plots (zoom, pan, hover tooltips)
76
+ - Fixed font debug message spam by adjusting logging levels
77
+ - Added plotly==5.18.0 to requirements.txt
78
+ - Current Status: Clean, interactive plotting with proper Gradio integration
79
+
80
+ ## 2024-01-02 (Update 7)
81
+ - Modified quantum_utils.py to store iterations as integers instead of strings
82
+ - Simplified visualization.py to handle native integer iterations
83
+ - Improved type consistency across the codebase
84
+ - Current Status: Cleaner data handling with proper numeric types
85
+
86
+ ## 2024-01-02 (Update 6)
87
+ - Enhanced visualization.py to handle string iteration numbers robustly
88
+ - Added error handling for iteration and energy value conversions
89
+ - Implemented safe type conversion with try-except blocks
90
+ - Current Status: Visualization properly handles all data types from results dictionary
91
+
92
+ ## 2024-01-02 (Update 5)
93
+ - Fixed type conversion in visualization.py for iteration numbers
94
+ - Modified energy extraction to use actual iteration energy values
95
+ - Resolved "list indices must be integers or slices, not str" error
96
+ - Current Status: Visualization properly handles string iteration numbers from results
97
+
98
+ ## 2024-01-02 (Update 4)
99
+ - Modified visualization.py to handle full results dictionary
100
+ - Updated plot_convergence to extract iteration data from results history
101
+ - Added final energy reference line to convergence plot
102
+ - Maintained rich data structure between modules for future extensibility
103
+ - Current Status: Improved integration between quantum_utils.py and visualization.py while preserving data
104
+
105
+ ## 2024-01-02 (Update 2)
106
+ - Fixed "unhashable type: 'dict'" error in results processing
107
+ - Modified results dictionary structure to ensure all keys are strings
108
+ - Added energy values to iteration history entries
109
+ - Improved type handling in results dictionary to prevent hashing issues
110
+ - Current Status: VQE optimization and results processing working correctly
111
+
112
+ ## 2024-01-02 (Update 1)
113
+ - Fixed iteration history processing in VQE simulation
114
+ - Added more robust data extraction from solver results
115
+ - Implemented fallback mechanism for iteration history
116
+ - Current Status: VQE optimization successfully completes with final energy: -1.8513513513513509
117
+ - Added better error handling and logging for iteration data processing
118
+ - Previous warning about missing 'energy' attribute has been addressed with more flexible data extraction
119
+
120
+ ## Current Status (2024-01-02)
121
+ - VQE optimization now successfully completes with final energy: -1.8513513513513509
122
+ - Fixed several major issues:
123
+ 1. Replaced `@cudaq.kernel` decorator with `cudaq.make_kernel()` to properly handle variational parameters
124
+ 2. Modified parameter handling to use list type instead of float
125
+ 3. Successfully implemented proper logging to both file and console
126
+ - Current Issue:
127
+ Unable to process iteration history data from VQE solver. Error indicates the solver's iteration objects
128
+ don't have expected 'energy' attribute. Need to investigate CUDA-Q solver's iteration data structure.
129
+ Error: "'cudaq_solvers._pycudaqx_solvers_the_suffix_matters' object has no attribute 'energy'"
130
+
131
+ ## Bug Fixes (2024-01-02)
132
+ - Fixed VQE kernel initialization error by replacing `@cudaq.kernel` decorator with `cudaq.make_kernel()`.
133
+ This resolved the "local variable 'id' referenced before assignment" error by properly creating a parameterized kernel
134
+ that can handle variational parameters in the VQE optimization process.
135
+ The `make_kernel()` approach matches CUDA-Q's expected way of handling parameterized quantum circuits.
136
+
137
+ ## Current Investigation (2024-12-30)
138
+ Investigating error: "capi_return is NULL Call-back cb_calcfc_in__cobyla__user__routines failed"
139
+
140
+ Steps tried so far:
141
+ 1. Initially used direct CUDA-Q optimizer (cudaq.optimizers.COBYLA)
142
+ 2. Switched to cudaq_solvers.vqe with scipy.minimize
143
+ 3. Added args=() tuple based on StackOverflow solution for similar COBYLA callback error
144
+ 4. Added explicit measurement operations to quantum kernel
145
+
146
+ ## Bug Fixes
147
+
148
+ ### 2024-12-30
149
+ - Fixed CUDA-Q kernel return type issue: Modified `h2_ansatz` kernel to have void return type (removed `return qubits` statement) to comply with CUDA-Q's requirement that kernels passed to `observe` must have void return type.
150
+ - Fixed Hamiltonian construction to use proper CUDA-Q spin operators (`cudaq.spin.x/y/z`) instead of string-based operator construction.
151
+ - Fixed missing operation error: Added registration of `meas_id` operation using `cudaq.register_operation("meas_id", np.identity(2))` for noise modeling.
152
+ - Fixed optimization error: Added empty args tuple to VQE solver call to properly handle callback arguments in scipy.optimize.minimize.
153
+ - Fixed measurement error: Added explicit measurement operation (`mz`) to the quantum kernel to ensure proper state measurement.
154
+
155
+ ## 2024-03-19
156
+ - Enhanced molecular visualization by replacing 2D Plotly plots with interactive 3D visualization using py3Dmol
157
+ - Added py3Dmol dependency to requirements.txt
158
+ - Updated app interface to use HTML component for 3D molecule viewer
159
+ - Improved H2 molecule representation with proper 3D coordinates and styling
install.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import sys
3
+
4
+ def install_requirements():
5
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
6
+
7
+ if __name__ == "__main__":
8
+ install_requirements()
molecules.json ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "H2": {
3
+ "name": "Hydrogen molecule",
4
+ "formula": "H₂",
5
+ "smiles": "[H][H]",
6
+ "basis": "sto-3g",
7
+ "multiplicity": 1,
8
+ "charge": 0,
9
+ "spatial_orbitals": 2,
10
+ "electron_count": 2,
11
+ "default_scale": 1.0,
12
+ "scale_range": {
13
+ "min": 0.1,
14
+ "max": 3.0,
15
+ "step": 0.05
16
+ },
17
+ "geometry_template": [
18
+ ["H", [0.0, 0.0, 0.0]],
19
+ ["H", [0.74, 0.0, 0.0]]
20
+ ],
21
+ "description": "Diatomic hydrogen molecule, the simplest neutral molecule. Scale factor affects the overall size of the molecule."
22
+ },
23
+ "H2O": {
24
+ "name": "Water",
25
+ "formula": "H₂O",
26
+ "smiles": "O",
27
+ "basis": "sto-3g",
28
+ "multiplicity": 1,
29
+ "charge": 0,
30
+ "spatial_orbitals": 7,
31
+ "electron_count": 10,
32
+ "default_scale": 1.0,
33
+ "scale_range": {
34
+ "min": 0.1,
35
+ "max": 3.0,
36
+ "step": 0.05
37
+ },
38
+ "geometry_template": [
39
+ ["O", [0.1173, 0.0, 0.0]],
40
+ ["H", [-0.4691, 0.7570, 0.0]],
41
+ ["H", [-0.4691, -0.7570, 0.0]]
42
+ ],
43
+ "description": "Triatomic water molecule with bent geometry. Scale factor affects the overall size of the molecule."
44
+ },
45
+ "CH4": {
46
+ "name": "Methane",
47
+ "formula": "CH₄",
48
+ "smiles": "C",
49
+ "basis": "sto-3g",
50
+ "multiplicity": 1,
51
+ "charge": 0,
52
+ "spatial_orbitals": 9,
53
+ "electron_count": 10,
54
+ "default_scale": 1.0,
55
+ "scale_range": {
56
+ "min": 0.1,
57
+ "max": 3.0,
58
+ "step": 0.05
59
+ },
60
+ "geometry_template": [
61
+ ["C", [0.0, 0.0, 0.0]],
62
+ ["H", [1.09, 0.0, 0.0]],
63
+ ["H", [-0.363, 1.03, 0.0]],
64
+ ["H", [-0.363, -0.515, 0.891]],
65
+ ["H", [-0.363, -0.515, -0.891]]
66
+ ],
67
+ "description": "Tetrahedral methane molecule with single bonds. Scale factor affects the overall size of the molecule."
68
+ },
69
+ "BeH2": {
70
+ "name": "Beryllium hydride",
71
+ "formula": "BeH₂",
72
+ "smiles": "[BeH2]",
73
+ "basis": "sto-3g",
74
+ "multiplicity": 1,
75
+ "charge": 0,
76
+ "spatial_orbitals": 7,
77
+ "electron_count": 4,
78
+ "default_scale": 1.0,
79
+ "scale_range": {
80
+ "min": 0.1,
81
+ "max": 3.0,
82
+ "step": 0.05
83
+ },
84
+ "geometry_template": [
85
+ ["Be", [0.0, 0.0, 0.0]],
86
+ ["H", [1.33, 0.0, 0.0]],
87
+ ["H", [-1.33, 0.0, 0.0]]
88
+ ],
89
+ "description": "Linear beryllium hydride molecule. Scale factor affects the overall size of the molecule."
90
+ },
91
+ "CO": {
92
+ "name": "Carbon monoxide",
93
+ "formula": "CO",
94
+ "smiles": "[C-]#[O+]",
95
+ "basis": "sto-3g",
96
+ "multiplicity": 1,
97
+ "charge": 0,
98
+ "spatial_orbitals": 10,
99
+ "electron_count": 14,
100
+ "default_scale": 1.0,
101
+ "scale_range": {
102
+ "min": 0.1,
103
+ "max": 3.0,
104
+ "step": 0.05
105
+ },
106
+ "geometry_template": [
107
+ ["C", [0.0, 0.0, 0.0]],
108
+ ["O", [1.13, 0.0, 0.0]]
109
+ ],
110
+ "description": "Diatomic carbon monoxide molecule with a triple bond. Scale factor affects the overall size of the molecule."
111
+ },
112
+ "N2": {
113
+ "name": "Nitrogen molecule",
114
+ "formula": "N₂",
115
+ "smiles": "N#N",
116
+ "basis": "sto-3g",
117
+ "multiplicity": 1,
118
+ "charge": 0,
119
+ "spatial_orbitals": 10,
120
+ "electron_count": 14,
121
+ "default_scale": 1.0,
122
+ "scale_range": {
123
+ "min": 0.1,
124
+ "max": 3.0,
125
+ "step": 0.05
126
+ },
127
+ "geometry_template": [
128
+ ["N", [0.0, 0.0, 0.0]],
129
+ ["N", [1.1, 0.0, 0.0]]
130
+ ],
131
+ "description": "Diatomic nitrogen molecule with a triple bond. Scale factor affects the overall size of the molecule."
132
+ },
133
+ "NH3": {
134
+ "name": "Ammonia",
135
+ "formula": "NH₃",
136
+ "smiles": "N",
137
+ "basis": "sto-3g",
138
+ "multiplicity": 1,
139
+ "charge": 0,
140
+ "spatial_orbitals": 9,
141
+ "electron_count": 10,
142
+ "default_scale": 1.0,
143
+ "scale_range": {
144
+ "min": 0.1,
145
+ "max": 3.0,
146
+ "step": 0.05
147
+ },
148
+ "geometry_template": [
149
+ ["N", [0.0, 0.0, 0.0]],
150
+ ["H", [1.01, 0.0, 0.0]],
151
+ ["H", [-0.505, 0.875, 0.0]],
152
+ ["H", [-0.505, -0.875, 0.0]]
153
+ ],
154
+ "description": "Trigonal pyramidal ammonia molecule with lone pair on nitrogen. Scale factor affects the overall size of the molecule."
155
+ }
156
+ }
157
+
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ python3-dev
2
+ python3-pip
3
+ build-essential
quantum_utils.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ quantum_utils.py
3
+ Helper library for running a simplified VQE or quantum simulation
4
+ that references CUDA-Q for computations.
5
+
6
+ References:
7
+ - @Cuda-Q_install.md for installation details
8
+ - @VQE_Example.md for example VQE code using nvidia or nvidia-mqpu backend
9
+ """
10
+
11
+ import cudaq
12
+ import cudaq_solvers as solvers # Import solvers from cudaq_solvers package
13
+ import numpy as np
14
+ from scipy.optimize import minimize
15
+ import spaces
16
+ from typing import Dict, List, Union, Any, Tuple
17
+ import sys
18
+ import logging
19
+ import os
20
+ from logging.handlers import RotatingFileHandler
21
+ import openfermion
22
+ import openfermionpyscf
23
+ from openfermion.transforms import jordan_wigner, get_fermion_operator
24
+
25
+ # Create logs directory if it doesn't exist
26
+ os.makedirs('logs', exist_ok=True)
27
+
28
+ # Configure logging to write to both file and stderr
29
+ # Create a formatter
30
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
31
+
32
+ # Set up file handler with rotation
33
+ file_handler = RotatingFileHandler(
34
+ 'logs/vqe_simulation.log',
35
+ maxBytes=10*1024*1024, # 10MB
36
+ backupCount=5
37
+ )
38
+ file_handler.setFormatter(formatter)
39
+
40
+ # Set up console handler
41
+ console_handler = logging.StreamHandler()
42
+ console_handler.setFormatter(formatter)
43
+
44
+ # Configure root logger
45
+ logger = logging.getLogger()
46
+ logger.setLevel(logging.DEBUG)
47
+ logger.addHandler(file_handler)
48
+ logger.addHandler(console_handler)
49
+
50
+ # Log startup message
51
+ logging.info("VQE Simulation logging initialized")
52
+
53
+ print("quantum_utils imported", file=sys.stderr, flush=True)
54
+
55
+ @spaces.GPU
56
+ def setup_target():
57
+ """Set up CUDA-Q target based on available hardware."""
58
+ try:
59
+ print("Setting up target...", file=sys.stderr, flush=True)
60
+ gpu_count = cudaq.num_available_gpus()
61
+ print(f"Number of available GPUs: {gpu_count}", file=sys.stderr, flush=True)
62
+
63
+ if gpu_count > 0:
64
+ print("Attempting to set NVIDIA GPU target...", file=sys.stderr, flush=True)
65
+ cudaq.set_target("nvidia")
66
+ print("Successfully set NVIDIA GPU target", file=sys.stderr, flush=True)
67
+ else:
68
+ print("No GPU found, attempting to set CPU target...", file=sys.stderr, flush=True)
69
+ cudaq.set_target("qpp-cpu")
70
+ print("Successfully set CPU target", file=sys.stderr, flush=True)
71
+ except Exception as e:
72
+ print(f"Error setting up quantum target: {str(e)}", file=sys.stderr, flush=True)
73
+ import traceback
74
+ print(f"Traceback:\n{traceback.format_exc()}", file=sys.stderr, flush=True)
75
+ raise
76
+
77
+ def vqe_callback(xk):
78
+ """Callback function for VQE optimization to track progress."""
79
+ try:
80
+ logging.info(f"VQE iteration - Current parameters: {xk}")
81
+ return True
82
+ except Exception as e:
83
+ logging.error(f"Error in VQE callback: {str(e)}")
84
+ return False
85
+
86
+ @spaces.GPU
87
+ def validate_ansatz(kernel_generator, init_params, qubit_num, electron_num):
88
+ """Validate the ansatz kernel before VQE."""
89
+ try:
90
+ logging.debug("Validating ansatz kernel...")
91
+ # Set up target before validation
92
+ setup_target()
93
+
94
+ # Generate the kernel with the correct number of qubits and electrons
95
+ kernel = kernel_generator(qubit_num, electron_num)
96
+
97
+ # Log the types and values of parameters and qubits
98
+ logging.debug(f"Kernel parameters type: {type(init_params)}, values: {init_params}")
99
+ logging.debug(f"Kernel qubits type: {type(qubit_num)}, value: {qubit_num}")
100
+ logging.debug(f"Kernel electrons type: {type(electron_num)}, value: {electron_num}")
101
+
102
+ # Try running the kernel with initial parameters
103
+ result = cudaq.sample(kernel, init_params.tolist()) # Convert to list for validation
104
+ logging.debug(f"Ansatz validation successful. Sample result: {result}")
105
+ return True
106
+ except Exception as e:
107
+ logging.error(f"Ansatz validation failed: {str(e)}")
108
+ return False
109
+
110
+ # ----------------------------------------------------------
111
+ # New or updated utility function for generating a Hamiltonian
112
+ # ----------------------------------------------------------
113
+ def create_molecular_hamiltonian(geometry: list[tuple[str, tuple[float, float, float]]],
114
+ basis: str,
115
+ multiplicity: int,
116
+ charge: int) -> cudaq.SpinOperator:
117
+ """
118
+ Create a SpinOperator for a given molecule using PySCF + OpenFermion + CUDA-Q.
119
+
120
+ Parameters
121
+ ----------
122
+ geometry : list of (str, (float, float, float)))
123
+ List describing each atom, e.g. [('H', (0.,0.,0.)), ('H',(0.,0.,bond_length))]
124
+ basis : str
125
+ Basis set, e.g. "sto-3g"
126
+ multiplicity : int
127
+ Spin multiplicity
128
+ charge : int
129
+ Net charge of the molecule
130
+
131
+ Returns
132
+ -------
133
+ cudaq.SpinOperator
134
+ The qubit Hamiltonian in CUDA-Q spin-operator form.
135
+ """
136
+ try:
137
+ # Use PySCF + OpenFermion to build a molecular object
138
+ molecule = openfermionpyscf.run_pyscf(
139
+ openfermion.MolecularData(geometry, basis, multiplicity, charge)
140
+ )
141
+
142
+ # Construct the fermionic Hamiltonian
143
+ molecular_hamiltonian = molecule.get_molecular_hamiltonian()
144
+ fermion_hamiltonian = get_fermion_operator(molecular_hamiltonian)
145
+ qubit_hamiltonian = jordan_wigner(fermion_hamiltonian)
146
+
147
+ # Convert to CUDA-Q SpinOperator
148
+ spin_op = cudaq.SpinOperator(qubit_hamiltonian)
149
+ return spin_op
150
+ except Exception as e:
151
+ logging.error(f"Failed to create molecular Hamiltonian: {str(e)}")
152
+ raise
153
+
154
+ # ----------------------------------------------------------
155
+ # New or updated function for generating a UCCSD ansatz kernel
156
+ # ----------------------------------------------------------
157
+ @cudaq.kernel
158
+ def kernel(qubit_num: int, electron_num: int, thetas: list[float]):
159
+ """
160
+ Generate a UCCSD ansatz kernel given the number of qubits and electrons.
161
+
162
+ Parameters
163
+ ----------
164
+ qubit_num : int
165
+ Number of qubits in the system
166
+ electron_num : int
167
+ Number of electrons (which is the same as number of X gates for HF ref.)
168
+
169
+ Returns
170
+ -------
171
+ A CUDA-Q kernel that accepts parameters for the UCCSD ansatz.
172
+ """
173
+ qubits = cudaq.qvector(qubit_num)
174
+ # Prepare the Hartree-Fock reference: X on each occupied orbital
175
+ for i in range(electron_num):
176
+ x(qubits[i]) # Apply X gate to each occupied orbital it is defined in the cuda kernel decorator space
177
+
178
+ # Add the UCCSD terms
179
+ cudaq.kernels.uccsd(qubits, thetas, electron_num, qubit_num)
180
+
181
+ # define the cost function as the observation of the spin_hamiltonian w.r.t. the ansatz
182
+ def cost_function(kernel, spin_hamiltonian, qubit_count, electron_count, thetas):
183
+ # The "cost" is the expectation of the spin_operator w.r.t. the ansatz, for these params
184
+ exp_val = cudaq.observe(kernel, spin_hamiltonian, qubit_count, electron_count, thetas).expectation()
185
+
186
+ logging.info(f"Cost function evaluation: {exp_val:.6f}")
187
+ return exp_val
188
+
189
+ def expand_geometry(geometry_template: List[List[Any]], scale_factor: float) -> List[Tuple[str, Tuple[float, float, float]]]:
190
+ """
191
+ Expand or contract a molecule's geometry by a scale factor.
192
+
193
+ Args:
194
+ geometry_template: List of [atom_symbol, [x, y, z]] coordinates
195
+ scale_factor: Factor to scale the geometry by (1.0 = no change)
196
+
197
+ Returns:
198
+ List of (atom_symbol, (x, y, z)) tuples with scaled coordinates
199
+ """
200
+ scaled_geometry = []
201
+ # Find the center of mass (assuming equal weights for simplicity)
202
+ coords = np.array([coord for _, coord in geometry_template])
203
+ center = np.mean(coords, axis=0)
204
+
205
+ for atom_symbol, coord in geometry_template:
206
+ # Convert coord to numpy array for vector operations
207
+ coord = np.array(coord)
208
+ # Calculate vector from center
209
+ vec = coord - center
210
+ # Scale the vector and add back to center
211
+ scaled_coord = center + vec * scale_factor
212
+ # Convert back to tuple
213
+ scaled_geometry.append((atom_symbol, tuple(scaled_coord)))
214
+
215
+ return scaled_geometry
216
+
217
+ # ----------------------------------------------------------
218
+ # Edits in run_vqe_simulation to use the new approach
219
+ # and a CUDA-Q optimizer
220
+ # ----------------------------------------------------------
221
+ @spaces.GPU(duration=20)
222
+ def run_vqe_simulation(molecule_data: Dict[str, Any],
223
+ scale_factor: float) -> Dict[str, Any]:
224
+ """
225
+ Run a VQE simulation using CUDA-Q.
226
+
227
+ Parameters
228
+ ----------
229
+ molecule_data : Dict[str, Any]
230
+ Dictionary containing all molecule metadata and parameters
231
+ scale_factor : float
232
+ Factor to scale the molecule geometry by (1.0 = original size)
233
+
234
+ Returns
235
+ -------
236
+ Dict[str, Any]
237
+ The dictionary has the following keys:
238
+ - 'final_energy': (float) the final VQE energy
239
+ - 'parameters': (list) the optimized ansatz parameters
240
+ - 'success': (bool) indicates if optimization converged
241
+ - 'iterations': (int) the maximum number of optimizer iterations
242
+ - 'history': (list) energy values from each iteration
243
+ - 'message': (str) status or error message
244
+ """
245
+ logging.info(f"Starting VQE simulation for {molecule_data['name']} with scale factor {scale_factor}")
246
+
247
+ # Ensure the correct GPU/CPU target is set
248
+ setup_target()
249
+
250
+ # Create scaled geometry
251
+ geometry = expand_geometry(molecule_data['geometry_template'], scale_factor)
252
+ logging.info(f"Created scaled geometry with factor = {scale_factor}")
253
+
254
+ spin_hamiltonian = create_molecular_hamiltonian(
255
+ geometry=geometry,
256
+ basis=molecule_data['basis'],
257
+ multiplicity=molecule_data['multiplicity'],
258
+ charge=molecule_data['charge']
259
+ )
260
+
261
+ # Log debugging info about the Hamiltonian
262
+ term_count = spin_hamiltonian.get_term_count()
263
+ logging.debug(f"Hamiltonian has {term_count} terms.")
264
+ logging.debug(f"Hamiltonian details:\n{spin_hamiltonian}")
265
+
266
+ # Create the UCCSD ansatz kernel
267
+ qubit_count = 2 * molecule_data['spatial_orbitals']
268
+ electron_count = molecule_data['electron_count']
269
+ optimizer_max_iterations = 10
270
+
271
+ # Compute how many UCCSD parameters we need to optimize
272
+ parameter_count = cudaq.kernels.uccsd_num_parameters(electron_count, qubit_count)
273
+ logging.info(f"Number of UCCSD parameters to optimize = {parameter_count}")
274
+
275
+ # Initialize the parameters for the UCCSD ansatz
276
+ thetas0 = np.random.normal(0, 1, parameter_count)
277
+ logging.info(f"Initial parameters: {thetas0}")
278
+
279
+ # Create a wrapper function that only takes the parameters to optimize
280
+ def objective_wrapper(thetas):
281
+ return cost_function(kernel, spin_hamiltonian, qubit_count, electron_count, thetas)
282
+
283
+ # Define the callback function that uses the wrapper
284
+ exp_vals = []
285
+ def callback(xk):
286
+ val = objective_wrapper(xk)
287
+ exp_vals.append(val)
288
+ return True
289
+
290
+ try:
291
+ # Test the wrapper function before optimization
292
+ test_val = objective_wrapper(thetas0)
293
+ logging.info(f"Debug cost check (initial params): {test_val:.6f}")
294
+
295
+ if not np.isfinite(test_val):
296
+ logging.warning("Debug cost check returned non-finite value. The optimizer may fail.")
297
+
298
+ # Perform the VQE optimization using the wrapper function
299
+ result = minimize(objective_wrapper,
300
+ thetas0,
301
+ method='COBYLA',
302
+ callback=callback,
303
+ options={'maxiter': optimizer_max_iterations})
304
+
305
+ energy = result.fun
306
+ parameters = result.x
307
+ success = result.success
308
+ message = "VQE optimization completed successfully."
309
+
310
+ except Exception as e:
311
+ logging.error(f"VQE optimization failed: {str(e)}")
312
+ energy = float("inf")
313
+ parameters = []
314
+ success = False
315
+ message = f"VQE optimization failed: {str(e)}"
316
+
317
+ logging.info(message)
318
+ logging.debug(f"Final energy: {energy}")
319
+ logging.debug(f"Optimized parameters: {parameters}")
320
+
321
+ # Build the results dictionary
322
+ results = {
323
+ "final_energy": float(energy),
324
+ "parameters": list(parameters),
325
+ "success": success,
326
+ "iterations": optimizer_max_iterations,
327
+ "history": exp_vals,
328
+ "message": message,
329
+ "parameter_count": parameter_count,
330
+ "hamiltonian_terms": term_count
331
+ }
332
+ return results
requirements.txt ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # requirements.txt
2
+ # Lists the essential Python dependencies for the CUDA-Q Gradio VQE demo.
3
+
4
+ # Core packages with specific versions
5
+ numpy==1.24.3
6
+ scipy==1.10.1
7
+
8
+ # Gradio and visualization components
9
+ gradio==4.44.1
10
+ gradio_molgallery3d>=0.0.4
11
+
12
+ # Plotting and visualization libraries
13
+ plotly==5.18.0
14
+ bokeh==3.2.2
15
+ matplotlib==3.7.1
16
+ rdkit==2023.09.1
17
+
18
+ # ASE for molecular geometry handling
19
+ ase==3.22.1
20
+
21
+ # CUDA-Q libraries for quantum simulation
22
+ cudaq
23
+ cudaq_solvers
24
+
25
+ # Optional extras for quantum chemistry
26
+ pyscf==2.6.2
27
+ openfermion==1.6.1
28
+ openfermionpyscf==0.5
29
+
30
+ # (Optional) Qiskit if desired
31
+ # qiskit>=0.42.0
visualization.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ visualization.py
3
+ Routines for plotting VQE results and molecular visualization using Plotly and MolGallery3D
4
+ """
5
+
6
+ import plotly.graph_objects as go
7
+ import numpy as np
8
+ import logging
9
+ from rdkit import Chem
10
+ from rdkit.Chem import AllChem
11
+ from gradio_molgallery import MolGallery3D
12
+ import json
13
+
14
+ # Configure logging
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.INFO)
17
+
18
+ def format_molecule_params(molecule_data: dict) -> str:
19
+ """
20
+ Format molecule parameters into a readable HTML string.
21
+
22
+ Args:
23
+ molecule_data: Dictionary containing molecule parameters
24
+ Returns:
25
+ HTML formatted string of parameters
26
+ """
27
+ try:
28
+ # Create a formatted HTML string with molecule parameters
29
+ params_html = f"""
30
+ <div style='font-family: monospace; padding: 10px;'>
31
+ <h3>Molecule Parameters:</h3>
32
+ <ul style='list-style-type: none; padding-left: 0;'>
33
+ <li><b>Name:</b> {molecule_data.get('name', 'N/A')} - {molecule_data.get('formula', 'N/A')}</li>
34
+ <li><b>Electrons:</b> {molecule_data.get('electron_count', 'N/A')}</li>
35
+ <li><b>Basis:</b> {molecule_data.get('basis', 'N/A')}, <b>Spatial Orbitals:</b> {molecule_data.get('spatial_orbitals', 'N/A')}</li>
36
+ <li><b>Charge:</b> {molecule_data.get('charge', 'N/A')}</li>
37
+ </ul>
38
+ </div>
39
+ """
40
+ return params_html
41
+ except Exception as e:
42
+ logger.error(f"Error formatting molecule parameters: {e}")
43
+ return "<div>Error: Could not format molecule parameters</div>"
44
+
45
+ def plot_convergence(results):
46
+ """
47
+ Create a convergence plot from VQE iteration history using Plotly.
48
+ Returns Plotly figure
49
+
50
+ Args:
51
+ results: Dictionary containing VQE results including history of energy values,
52
+ or list of energy values directly
53
+ """
54
+ logger.info(f"Plotting convergence with results type: {type(results)}")
55
+
56
+ # Extract iteration numbers and energies
57
+ iterations = []
58
+ energies = []
59
+
60
+ # Handle different input formats
61
+ if isinstance(results, dict):
62
+ history = results.get('history', [])
63
+ else:
64
+ history = results
65
+
66
+ logger.info(f"History type: {type(history)}, length: {len(history)}")
67
+
68
+ # Handle both dictionary entries and direct energy values
69
+ for i, entry in enumerate(history):
70
+ if isinstance(entry, dict):
71
+ # Handle dictionary format
72
+ try:
73
+ iterations.append(entry.get('iteration', i))
74
+ energies.append(entry['energy'])
75
+ except Exception as e:
76
+ logger.warning(f"Skipping invalid entry {i}: {str(e)}")
77
+ continue
78
+ else:
79
+ # Handle direct energy values
80
+ try:
81
+ iterations.append(i)
82
+ energies.append(float(entry))
83
+ except (ValueError, TypeError) as e:
84
+ logger.warning(f"Skipping invalid entry {i}: {str(e)}")
85
+ continue
86
+
87
+ if not iterations or not energies:
88
+ raise ValueError("No valid iteration data found in results")
89
+
90
+ # Create Plotly figure
91
+ fig = go.Figure()
92
+
93
+ # Add energy convergence line
94
+ fig.add_trace(go.Scatter(
95
+ x=iterations,
96
+ y=energies,
97
+ mode='lines+markers',
98
+ name='Energy',
99
+ line=dict(color='blue')
100
+ ))
101
+
102
+ # Add final energy line if available
103
+ final_energy = None
104
+ if isinstance(results, dict) and 'final_energy' in results:
105
+ try:
106
+ final_energy = float(results['final_energy'])
107
+ except (ValueError, TypeError):
108
+ pass
109
+
110
+ if final_energy is None and energies:
111
+ final_energy = energies[-1]
112
+
113
+ if final_energy is not None:
114
+ fig.add_hline(
115
+ y=final_energy,
116
+ line_dash="dash",
117
+ line_color="red",
118
+ annotation_text=f"Final Energy: {final_energy:.6f}",
119
+ annotation_position="bottom right"
120
+ )
121
+
122
+ # Update layout
123
+ fig.update_layout(
124
+ title='VQE Convergence',
125
+ xaxis_title='Iteration',
126
+ yaxis_title='Energy (Hartree)',
127
+ showlegend=True,
128
+ hovermode='x',
129
+ width=800,
130
+ height=600
131
+ )
132
+
133
+ return fig
134
+
135
+ def create_molecule_viewer(molecule_id: str, scale_factor: float) -> MolGallery3D:
136
+ """
137
+ Create a 3D visualization of the molecule using MolGallery3D and RDKit.
138
+ Returns MolGallery3D component for Gradio display
139
+
140
+ Args:
141
+ molecule_id: Molecule identifier (e.g., "H2")
142
+ scale_factor: Bond length scaling factor (1.0 = original size)
143
+ """
144
+ logger.info(f"Creating 3D view for {molecule_id} with scale_factor={scale_factor}")
145
+
146
+ try:
147
+ # Load molecule data
148
+ with open('molecules.json', 'r') as f:
149
+ molecules = json.load(f)
150
+
151
+ if molecule_id not in molecules:
152
+ logger.error(f"Unknown molecule {molecule_id}")
153
+ return None
154
+
155
+ molecule_data = molecules[molecule_id]
156
+
157
+ # Create molecule using SMILES
158
+ mol = Chem.MolFromSmiles(molecule_data['smiles'])
159
+ if mol is None:
160
+ logger.error(f"Could not create molecule from SMILES: {molecule_data['smiles']}")
161
+ return None
162
+
163
+ mol = AllChem.AddHs(mol)
164
+ AllChem.EmbedMolecule(mol, randomSeed=42) # Use fixed seed for consistency
165
+
166
+ # Scale the molecule coordinates
167
+ conf = mol.GetConformer()
168
+ positions = conf.GetPositions()
169
+ centroid = np.mean(positions, axis=0)
170
+
171
+ for i in range(mol.GetNumAtoms()):
172
+ pos = positions[i]
173
+ scaled_pos = centroid + (pos - centroid) * scale_factor
174
+ conf.SetAtomPosition(i, scaled_pos)
175
+
176
+ # Create the MolGallery3D component
177
+ gallery = MolGallery3D(
178
+ columns=1,
179
+ height=400,
180
+ label=f"{molecule_data.get('name', molecule_id)} (Scale: {scale_factor:.2f})",
181
+ automatic_rotation=True
182
+ )
183
+
184
+ logger.info("Created MolGallery3D component")
185
+ return [mol] # Return the molecule as a list as expected by MolGallery3D
186
+
187
+ except Exception as e:
188
+ logger.error(f"Failed to create molecule viewer: {e}")
189
+ return None