Skip to content

Commit 0303451

Browse files
authored
Merge branch 'main' into new_IonQ_api_v_04
2 parents c891b47 + b07ff17 commit 0303451

File tree

63 files changed

+1616
-254
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1616
-254
lines changed

docs/README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ defined by the value of the `extensions` configuration in
5959

6060
Additional links that may be helpful that are not listed above:
6161

62-
- [References and automatic link generation in
63-
Doxygen](https://www.star.bnl.gov/public/comp/sofi/doxygen/autolink.html)
6462
- [Using Napoleon style for Python doc
6563
comments](https://docs.softwareheritage.org/devel/contributing/sphinx.html)
6664
- [Cross-referencing Python

docs/sphinx/applications/python/skqd.ipynb

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
import cudaq
2+
from cudaq import spin
3+
4+
from collections import Counter
5+
import numpy as np
6+
7+
from scipy.sparse import csr_matrix
8+
from scipy.sparse.linalg import eigsh
9+
10+
11+
def transform_state_with_pauli_operator(computational_state,
12+
pauli_operator_string):
13+
"""
14+
Transform a computational basis state by applying a Pauli operator string.
15+
16+
This function computes how a Pauli string acts on a computational basis state.
17+
For example, applying "`XY`" to |01⟩ gives i|10⟩ (bit flip + phase).
18+
19+
Implementation follows MSB convention: `pauli_operator_string[i]` acts on computational_state[i]
20+
where i=0 corresponds to the most significant bit (leftmost position).
21+
22+
Args:
23+
computational_state: Binary array representing |0⟩ and |1⟩ states in MSB order
24+
`pauli_operator_string`: String of Pauli operators (I, X, Y, Z) in MSB order
25+
26+
Returns:
27+
tuple: (transformed_state, quantum_phase) where quantum_phase is ±1 or ±1j
28+
"""
29+
transformed_state = computational_state.copy()
30+
quantum_phase = 1.0 + 0j # Start with no phase; Y operators will add ±i phases
31+
32+
# Apply each Pauli operator to its corresponding qubit
33+
for qubit_position, pauli_op in enumerate(pauli_operator_string):
34+
if pauli_op == 'I':
35+
# Identity: no change to state or phase
36+
continue
37+
elif pauli_op == 'X':
38+
# Pauli-X: bit flip |0⟩ ↔ |1⟩
39+
transformed_state[
40+
qubit_position] = 1 - transformed_state[qubit_position]
41+
elif pauli_op == 'Y':
42+
# Pauli-Y = i·X·Z: bit flip with phase ±i depending on initial state
43+
if transformed_state[qubit_position] == 0:
44+
quantum_phase *= 1j # Y|0⟩ = i|1⟩
45+
else:
46+
quantum_phase *= -1j # Y|1⟩ = -i|0⟩
47+
transformed_state[
48+
qubit_position] = 1 - transformed_state[qubit_position]
49+
elif pauli_op == 'Z':
50+
# Pauli-Z: phase flip for |1⟩ state
51+
if transformed_state[qubit_position] == 1:
52+
quantum_phase *= -1 # Z|1⟩ = -|1⟩, Z|0⟩ = |0⟩
53+
54+
return transformed_state, quantum_phase
55+
56+
57+
def construct_hamiltonian_in_subspace(pauli_operator_list,
58+
hamiltonian_coefficients,
59+
subspace_basis_states):
60+
"""
61+
Project Hamiltonian operator onto the computational subspace spanned by basis states.
62+
63+
This is the heart of SKQD: instead of computing expensive quantum matrix elements,
64+
we compute how each Pauli term in the Hamiltonian acts on our sampled basis states.
65+
Only matrix elements between states in our subspace contribute to the final result.
66+
67+
Native CUDA-Q implementation using MSB (Most Significant Bit first) convention:
68+
- Both Pauli operator strings and basis states follow MSB bit ordering
69+
- Pauli operator index 0 acts on qubit 0 (leftmost bit position)
70+
71+
Args:
72+
`pauli_operator_list: List of Pauli operator strings (e.g., ['IIXY', 'ZIII'])`
73+
hamiltonian_coefficients: List of real coefficients for each Pauli operator
74+
subspace_basis_states: Array of computational basis states defining the subspace
75+
76+
Returns:
77+
`scipy.sparse matrix representing projected Hamiltonian within the subspace`
78+
"""
79+
subspace_dimension = subspace_basis_states.shape[0]
80+
81+
# Create fast lookup: basis state → subspace index
82+
# This allows O(1) checking if a transformed state is in our subspace
83+
state_to_index_map = {}
84+
for idx, basis_state in enumerate(subspace_basis_states):
85+
state_key = tuple(basis_state)
86+
state_to_index_map[state_key] = idx
87+
88+
# Build sparse matrix in COO format (rows, cols, values)
89+
matrix_rows, matrix_cols, matrix_elements = [], [], []
90+
91+
# For each basis state |i⟩ in our subspace...
92+
for initial_state_idx, initial_state in enumerate(subspace_basis_states):
93+
# For each Pauli term P_k in the Hamiltonian...
94+
for pauli_operator, coefficient in zip(pauli_operator_list,
95+
hamiltonian_coefficients):
96+
# Compute P_k|i⟩ = phase × |j⟩
97+
final_state, phase_factor = transform_state_with_pauli_operator(
98+
initial_state, pauli_operator)
99+
final_state_key = tuple(final_state)
100+
101+
# Only keep matrix element if |j⟩ is also in our subspace
102+
if final_state_key in state_to_index_map:
103+
final_state_idx = state_to_index_map[final_state_key]
104+
matrix_rows.append(final_state_idx)
105+
matrix_cols.append(initial_state_idx)
106+
107+
# Matrix element: ⟨j|H|i⟩ = coefficient × phase_factor
108+
hamiltonian_element = coefficient * phase_factor
109+
110+
# Clean up tiny imaginary parts (should be real for Hermitian H)
111+
if abs(hamiltonian_element.imag) < 1e-14:
112+
hamiltonian_element = hamiltonian_element.real
113+
matrix_elements.append(hamiltonian_element)
114+
115+
# Convert to efficient sparse matrix format
116+
return csr_matrix((matrix_elements, (matrix_rows, matrix_cols)),
117+
shape=(subspace_dimension, subspace_dimension))
118+
119+
120+
def diagonalize_subspace_hamiltonian(subspace_basis_states,
121+
pauli_operator_list,
122+
hamiltonian_coefficients,
123+
verbose=False,
124+
**solver_options):
125+
"""
126+
Perform eigenvalue decomposition of Hamiltonian within the computational subspace.
127+
128+
This function combines Hamiltonian projection and `diagonalization` into one step.
129+
It's the final piece of SKQD: finding the lowest eigenvalues in our Krylov subspace.
130+
131+
Args:
132+
subspace_basis_states: Array of computational basis states defining the subspace
133+
`pauli_operator_list: List of Pauli operator strings (e.g., ['IIXY', 'ZIII'])`
134+
hamiltonian_coefficients: List of real coefficients for each Pauli operator
135+
verbose: Enable diagnostic output
136+
`**solver_options: Additional arguments for scipy.sparse.linalg.eigsh`
137+
138+
Returns:
139+
`numpy` array of eigenvalues from the subspace `diagonalization`
140+
"""
141+
if subspace_basis_states.shape[0] == 0:
142+
return np.array([])
143+
144+
# Step 1: Project the full Hamiltonian onto our sampled subspace
145+
projected_hamiltonian = construct_hamiltonian_in_subspace(
146+
pauli_operator_list, hamiltonian_coefficients, subspace_basis_states)
147+
148+
if verbose:
149+
print(f"Subspace dimension: {projected_hamiltonian.shape[0]}")
150+
print(
151+
f"Hamiltonian sparsity: {projected_hamiltonian.nnz / (projected_hamiltonian.shape[0]**2):.4f}"
152+
)
153+
154+
# Step 2: Find eigenvalues of the projected Hamiltonian
155+
try:
156+
# Use sparse eigensolver for efficiency (typically we only need a few eigenvalues)
157+
eigenvalues = eigsh(projected_hamiltonian,
158+
return_eigenvectors=False,
159+
**solver_options)
160+
return eigenvalues
161+
except Exception as solver_error:
162+
if verbose:
163+
print(f"Sparse eigensolver failed: {solver_error}")
164+
165+
# Fallback: use dense `diagonalization` for small matrices
166+
if projected_hamiltonian.shape[0] <= 100:
167+
dense_hamiltonian = projected_hamiltonian.toarray()
168+
eigenvalues = np.linalg.eigvals(dense_hamiltonian)
169+
170+
# Extract only the requested eigenvalues to match sparse solver behavior
171+
num_eigenvalues = solver_options.get('k', min(6, len(eigenvalues)))
172+
eigenvalue_selection = solver_options.get('which', 'SA')
173+
if eigenvalue_selection == 'SA': # smallest algebraic eigenvalues
174+
selected_indices = np.argsort(eigenvalues)[:num_eigenvalues]
175+
else:
176+
selected_indices = np.argsort(eigenvalues)[-num_eigenvalues:]
177+
return eigenvalues[selected_indices]
178+
else:
179+
raise
180+
181+
182+
def accumulate_krylov_measurements(measurement_results_sequence,
183+
krylov_dimension):
184+
"""
185+
Progressively accumulate measurement outcomes from Krylov state evolution.
186+
187+
This is a key insight of SKQD: instead of treating each Krylov state separately,
188+
we combine measurements from |ψ⟩, U|ψ⟩, U²|ψ⟩, ... to build increasingly
189+
rich computational `subspaces` that better approximate the true Krylov space.
190+
191+
Args:
192+
measurement_results_sequence: List of measurement dictionaries from each U^k|ψ⟩
193+
krylov_dimension: Number of Krylov states to consider
194+
195+
Returns:
196+
List of accumulated measurement dictionaries, where entry k contains
197+
measurements from all states |ψ⟩, U|ψ⟩, ..., U^k|ψ⟩
198+
"""
199+
accumulated_measurements = []
200+
201+
# For each Krylov dimension k = 1, 2, 3, ...
202+
for evolution_step in range(krylov_dimension):
203+
measurement_accumulator = Counter()
204+
205+
# Combine measurements from all states up to U^k|ψ⟩
206+
for measurement_data in measurement_results_sequence[:evolution_step +
207+
1]:
208+
measurement_accumulator.update(measurement_data)
209+
210+
# Convert back to dictionary and store
211+
combined_measurements = dict(measurement_accumulator)
212+
accumulated_measurements.append(combined_measurements)
213+
214+
return accumulated_measurements
215+
216+
217+
def construct_xyz_spin_hamiltonian(
218+
system_size: int,
219+
interaction_strengths: tuple[float, float, float] = (1.0, 1.0, 1.0),
220+
external_field_strengths: tuple[float, float, float] = (0.0, 0.0, 0.0),
221+
topology_type: str = "ring") -> cudaq.SpinOperator:
222+
"""
223+
Construct `XYZ` spin model Hamiltonian using native CUDA-Q SpinOperator framework.
224+
225+
Implements the quantum many-body Hamiltonian:
226+
H = sum_{(i,j) ∈ edges} [J_x σ_i^x σ_j^x + J_y σ_i^y σ_j^y + J_z σ_i^z σ_j^z] +
227+
sum_{i ∈ sites} [h_x σ_i^x + h_y σ_i^y + h_z σ_i^z]
228+
229+
Args:
230+
system_size: Number of quantum spins in the system
231+
interaction_strengths: (J_x, J_y, J_z) nearest-neighbor coupling parameters
232+
external_field_strengths: (h_x, h_y, h_z) local magnetic field components
233+
topology_type: Lattice connectivity ("ring" implements periodic boundary conditions)
234+
235+
Returns:
236+
CUDA-Q SpinOperator encoding the XYZ spin Hamiltonian
237+
"""
238+
J_x, J_y, J_z = interaction_strengths
239+
h_x, h_y, h_z = external_field_strengths
240+
241+
# Initialize Hamiltonian with null operator
242+
spin_hamiltonian = 0.0 * spin.z(0)
243+
244+
# Construct nearest-neighbor interaction terms (ring topology only)
245+
for site_i in range(system_size):
246+
site_j = (site_i +
247+
1) % system_size # Nearest neighbor with periodic wrapping
248+
249+
# Add Pauli tensor product interactions (skip zero coefficients)
250+
if J_x != 0.0:
251+
spin_hamiltonian += J_x * spin.x(site_i) * spin.x(site_j)
252+
if J_y != 0.0:
253+
spin_hamiltonian += J_y * spin.y(site_i) * spin.y(site_j)
254+
if J_z != 0.0:
255+
spin_hamiltonian += J_z * spin.z(site_i) * spin.z(site_j)
256+
257+
# Add local magnetic field terms (skip zero fields)
258+
if h_x != 0.0 or h_y != 0.0 or h_z != 0.0:
259+
for site_i in range(system_size):
260+
if h_x != 0.0:
261+
spin_hamiltonian += h_x * spin.x(site_i)
262+
if h_y != 0.0:
263+
spin_hamiltonian += h_y * spin.y(site_i)
264+
if h_z != 0.0:
265+
spin_hamiltonian += h_z * spin.z(site_i)
266+
267+
return spin_hamiltonian
268+
269+
270+
def create_heisenberg_hamiltonian(n_spins: int, Jx: float, Jy: float, Jz: float,
271+
h_x: list[float], h_y: list[float],
272+
h_z: list[float]):
273+
274+
ham = 0
275+
276+
# Add two-qubit interaction terms for Heisenberg Hamiltonian
277+
for i in range(0, n_spins - 1):
278+
ham += Jx * spin.x(i) * spin.x(i + 1)
279+
ham += Jy * spin.y(i) * spin.y(i + 1)
280+
ham += Jz * spin.z(i) * spin.z(i + 1)
281+
282+
return ham
283+
284+
285+
def extract_hamiltonian_data(spin_operator: cudaq.SpinOperator):
286+
"""Extract coefficients, Pauli words, and strings from CUDA-Q SpinOperator.
287+
288+
Optimized single-pass extraction of all required Hamiltonian data.
289+
290+
Args:
291+
spin_operator: CUDA-Q SpinOperator to decompose
292+
293+
Returns:
294+
`tuple: (coefficients_list, pauli_words_list, pauli_strings_list)`
295+
"""
296+
system_size = spin_operator.qubit_count
297+
coefficients_list = []
298+
pauli_words_list = []
299+
pauli_strings_list = []
300+
301+
for pauli_term in spin_operator:
302+
# Extract coefficient
303+
term_coefficient = pauli_term.evaluate_coefficient()
304+
assert abs(
305+
term_coefficient.imag
306+
) < 1e-10, f"Non-real coefficient encountered: {term_coefficient}"
307+
coefficients_list.append(float(term_coefficient.real))
308+
309+
# Extract Pauli string
310+
pauli_string = pauli_term.get_pauli_word(system_size)
311+
pauli_strings_list.append(pauli_string)
312+
313+
# Create Pauli word object
314+
pauli_words_list.append(cudaq.pauli_word(pauli_string))
315+
316+
return coefficients_list, pauli_words_list, pauli_strings_list
317+
318+
319+
def create_tfim_hamiltonian(n_spins: int, h_field: float):
320+
"""Create the Hamiltonian operator"""
321+
ham = 0
322+
323+
# Add single-qubit terms
324+
for i in range(0, n_spins):
325+
ham += -1 * h_field * spin.x(i)
326+
327+
# Add two-qubit interaction terms for Ising Hamiltonian
328+
for i in range(0, n_spins - 1):
329+
ham += -1 * spin.z(i) * spin.z(i + 1)
330+
331+
return ham
332+
333+
334+
def extract_basis_states_from_measurements(measurement_counts):
335+
"""
336+
Extract computational basis states from CUDA-Q measurement results.
337+
338+
This function converts the measurement outcome dictionary into a matrix where
339+
each row represents a unique computational basis state observed during sampling.
340+
341+
Args:
342+
measurement_counts: Dictionary mapping bitstring outcomes to their frequencies
343+
344+
Returns:
345+
`numpy` array of computational basis states (MSB ordering)
346+
`Shape: (num_unique_states, num_qubits)`
347+
"""
348+
if not measurement_counts:
349+
return np.array([])
350+
351+
# Extract all unique bitstrings that were observed during measurements
352+
observed_bitstrings = list(measurement_counts.keys())
353+
num_qubits = len(observed_bitstrings[0])
354+
355+
# Convert bitstrings to binary matrix representation
356+
# Each row is a computational basis state |00...⟩, |01...⟩, etc.
357+
basis_state_matrix = np.array(
358+
[[int(bit) for bit in bitstring] for bitstring in observed_bitstrings],
359+
dtype=np.int8)
360+
361+
return basis_state_matrix

docs/sphinx/targets/cpp/oqc.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ struct bell_state {
2323
cudaq::qvector q(2);
2424
h(q[0]);
2525
x<cudaq::ctrl>(q[0], q[1]);
26-
auto result = mz(q);
2726
}
2827
};
2928

docs/sphinx/targets/cpp/quantinuum.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ struct ghz {
1919
for (int i = 0; i < 4; i++) {
2020
x<cudaq::ctrl>(q[i], q[i + 1]);
2121
}
22-
mz(q);
2322
}
2423
};
2524

docs/sphinx/targets/python/braket.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ def kernel():
1818
qvector = cudaq.qvector(2)
1919
h(qvector[0])
2020
x.ctrl(qvector[0], qvector[1])
21-
mz(qvector)
2221

2322

2423
# Execute and print out the results.

0 commit comments

Comments
 (0)