Skip to content

Commit c306c20

Browse files
committed
Create Lcucode.txt
1 parent 50dfa6b commit c306c20

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed

doc/Programs/LCUcodes/Lcucode.txt

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Linear Combination of Unitaries (LCU) Algorithm Simulation
2+
# This script demonstrates the LCU method [oai_citation:0‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=,of%20the%20target%20operator) for applying a non-unitary operator (like a Hamiltonian H
3+
# expressed as a sum of unitaries) to a quantum state by embedding it into a larger unitary with an ancilla [oai_citation:1‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=The%20LCU%20method%20is%20a,into%20a%20larger%20unitary%20circuit).
4+
# We simulate ground state energy estimation and time evolution for a random Hamiltonian using NumPy (state-vector simulation).
5+
6+
import numpy as np
7+
import numpy.linalg as la
8+
import scipy.linalg
9+
10+
# Define single-qubit Pauli matrices (and identity) as our set of unitaries
11+
I2 = np.array([[1, 0],
12+
[0, 1]], dtype=complex)
13+
X = np.array([[0, 1],
14+
[1, 0]], dtype=complex)
15+
Y = np.array([[0, -1j],
16+
[1j, 0]], dtype=complex)
17+
Z = np.array([[1, 0],
18+
[0,-1]], dtype=complex)
19+
pauli_ops = {'I': I2, 'X': X, 'Y': Y, 'Z': Z}
20+
21+
def kron_n(mats):
22+
"""Kronecker product of a list of matrices (multiqubit operator)."""
23+
result = np.array([[1]], dtype=complex)
24+
for m in mats:
25+
result = np.kron(result, m)
26+
return result
27+
28+
def generate_random_hamiltonian(num_qubits, num_terms):
29+
"""
30+
Generate a random Hermitian operator H on `num_qubits` expressed as a
31+
weighted sum of `num_terms` Pauli-string unitaries: H = sum_j alpha_j U_j.
32+
Returns the Hamiltonian matrix, original coefficients, Pauli terms,
33+
and the modified coefficients/unitaries for LCU (with positive weights).
34+
"""
35+
pauli_strings = []
36+
coeffs = []
37+
for _ in range(num_terms):
38+
# Randomly choose a Pauli string (tensor product of I, X, Y, Z on each qubit)
39+
s = ''.join(np.random.choice(list(pauli_ops.keys())) for _ in range(num_qubits))
40+
pauli_strings.append(s)
41+
coeffs.append(np.random.uniform(-1, 1)) # random weight in [-1, 1]
42+
coeffs = np.array(coeffs)
43+
H = np.zeros((2**num_qubits, 2**num_qubits), dtype=complex)
44+
mod_weights = [] # |alpha_j| for LCU ancilla probabilities
45+
mod_unitaries = [] # potentially sign-adjusted unitaries for negative coefficients
46+
for w, s in zip(coeffs, pauli_strings):
47+
U = kron_n([pauli_ops[p] for p in s]) # unitary matrix for Pauli string
48+
if w < 0:
49+
# If coefficient is negative, flip the sign of U so that the weight becomes positive [oai_citation:2‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=,of%20the%20target%20operator).
50+
U = -U
51+
mod_weights.append(-w)
52+
else:
53+
mod_weights.append(w)
54+
mod_unitaries.append(U)
55+
H += w * U # accumulate H
56+
mod_weights = np.array(mod_weights, dtype=float)
57+
return H, coeffs, pauli_strings, mod_weights, mod_unitaries
58+
59+
# --- Simulation Parameters (feel free to modify) ---
60+
num_qubits = 2 # number of system qubits
61+
num_ancilla = 2 # number of ancilla qubits for LCU (controls how many terms can be combined)
62+
time_param = 1.0 # time parameter t for Hamiltonian time evolution simulation
63+
64+
# Generate a random Hamiltonian H = sum_i alpha_i U_i (each U_i is a Pauli-string unitary)
65+
num_terms = 2 ** num_ancilla # we will use 2^ancilla states for terms
66+
H, orig_coeffs, pauli_terms, mod_weights, mod_unitaries = generate_random_hamiltonian(num_qubits, num_terms)
67+
dim = H.shape[0]
68+
print(f"Generated a random {dim}x{dim} Hamiltonian on {num_qubits} qubit(s):")
69+
for coeff, term in zip(orig_coeffs, pauli_terms):
70+
# List each term as coefficient * tensor_product(Paulis)
71+
term_ops = ' ⊗ '.join(term) # pretty print with tensor product symbol
72+
print(f" {coeff:+.3f} * ({term_ops})")
73+
# Compute eigenvalues and eigenvectors for reference (ground state energy, etc.)
74+
eigenvals, eigenvecs = la.eigh(H)
75+
ground_energy = eigenvals[0]
76+
print(f"Ground state energy (smallest eigenvalue): {ground_energy:.6f}")
77+
78+
# Prepare an initial state for simulation.
79+
# For ground state energy estimation, we use the ground state (eigenvector of H with eigenvalue = ground_energy).
80+
psi0 = eigenvecs[:, 0] # ground state (normalized)
81+
# Verify that the expectation value of H on psi0 equals the ground energy
82+
exp_val = np.vdot(psi0, H.dot(psi0)).real
83+
print(f"Verification: <psi_ground|H|psi_ground> = {exp_val:.6f}")
84+
85+
# --- Ground State Energy Estimation via LCU ---
86+
# We will apply the LCU block-encoding of H on |psi0>.
87+
# The LCU circuit structure: PREPARE (oracle) → SELECT → PREPARE† [oai_citation:3‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=,of%20the%20target%20operator).
88+
# Prepare: ancilla in superposition ∑_i sqrt(alpha_i/λ) |i>, where α_i = |coeff_i| and λ = ∑_i α_i [oai_citation:4‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=%5C%5B%5Cmathrm).
89+
lambda_sum = mod_weights.sum() # λ = sum of positive coefficients
90+
anc_state = np.sqrt(mod_weights / lambda_sum) # ancilla state vector after PREPARE
91+
combined_state = np.kron(anc_state, psi0) # joint state after preparation (ancilla entangled with nothing yet)
92+
# SELECT: apply U_i to the system conditioned on ancilla state |i> [oai_citation:5‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=,i%5Crangle).
93+
combined_state = combined_state.reshape((len(mod_weights), -1)) # reshape to (ancilla_dim, system_dim)
94+
for i, U in enumerate(mod_unitaries):
95+
combined_state[i, :] = U.dot(combined_state[i, :])
96+
# Now the joint state is ∑_i sqrt(α_i/λ) |i> ⊗ (U_i |psi0>).
97+
# Unprepare (PREPARE†): invert the ancilla preparation. We find the amplitude of ancilla returning to |0> (the initial ancilla basis state).
98+
# The success probability of post-selecting ancilla |0> is proportional to || A|psi0> ||^2 / λ^2 [oai_citation:6‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=The%20LCU%20protocol%20is%20deemed,and%20the%20transformed%20target%20variable), where A = ∑_i α_i U_i = H.
99+
main_proj = anc_state.conjugate().T @ combined_state # project ancilla onto the prepared superposition (equivalent to measuring ancilla in |0> after unprepare)
100+
success_prob = la.norm(main_proj) ** 2
101+
print(f"LCU success probability (postselect ancilla = |0>): {success_prob:.4f}")
102+
if success_prob > 1e-12:
103+
psi_post = main_proj / np.linalg.norm(main_proj) # normalized system state after successful postselection
104+
overlap = np.vdot(psi_post, psi0)
105+
print(f"Post-selection main state overlap with initial state: {abs(overlap):.4f}")
106+
# If |psi0> is the true ground state, H|psi0> = E0|psi0>. In that case, the ancilla |0> success probability = (E0/λ)^2 and the post-selected state returns to |psi0> (global phase e^{-iE0t} acquired).
107+
# In a real algorithm, one could measure the ancilla success frequency to infer |E0/λ|, or use amplitude amplification to boost success probability [oai_citation:7‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=The%20success%20probability%20depends%20on,included%20as%20an%20experimental%20feature).
108+
# (Amplitude amplification can make the LCU nearly deterministic [oai_citation:8‡arxiv.org](https://arxiv.org/abs/1202.5822#:~:text=technique,a%20large%20class%20of%20methods) [oai_citation:9‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=The%20success%20probability%20depends%20on,included%20as%20an%20experimental%20feature) at the cost of more gates.)
109+
110+
# --- Time Evolution via LCU (Hamiltonian Simulation) ---
111+
# We approximate U(t) = exp(-i H * time_param) using the LCU method for Hamiltonian simulation.
112+
# Using a first-order Taylor/Trotter expansion [oai_citation:10‡journals.aps.org](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.6.010359#:~:text=Our%20algorithm%2C%20called%20Trotter%20LCU%2C,model%2C%20making%20it%20a%20powerful): e^{-iH Δt} ≈ I - i H Δt for small Δt.
113+
# This means we treat A = I - i H Δt as a linear combination of the identity and the H terms.
114+
# To implement A via LCU, we add an extra ancilla basis state for the identity term.
115+
segments = max(1, int(np.ceil(lambda_sum * time_param))) # number of segments to break time evolution into (more segments = higher accuracy)
116+
print(f"\nSimulating time evolution for t = {time_param} with {segments} segment(s).")
117+
psi = psi0.copy()
118+
cumulative_success = 1.0
119+
show_steps = (segments <= 10) # only print step-by-step if few segments
120+
if not show_steps:
121+
print("Using many segments, skipping per-segment details...")
122+
for seg in range(segments):
123+
dt = time_param / segments
124+
# Coefficients for A = I - i H dt: weight_0 = 1 (identity), and weight_i = |α_i| * dt for each H term (α_i = mod_weights[i])
125+
weights_time = [1.0] + list(mod_weights * dt)
126+
lambda_time = sum(weights_time)
127+
anc_state_time = np.sqrt(np.array(weights_time) / lambda_time) # ancilla state including identity term
128+
# Build joint state for this segment
129+
state_mat = np.kron(anc_state_time, psi).reshape((len(weights_time), -1))
130+
# SELECT: apply identity for ancilla index 0 (do nothing), and apply each U_i with appropriate phase for ancilla i>0.
131+
for i in range(1, len(weights_time)):
132+
# Original coefficient (could be negative) determines a phase: -i * sign(original_coeff) [oai_citation:11‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=,of%20the%20target%20operator).
133+
orig_coeff = orig_coeffs[i-1]
134+
phase = -1j * np.sign(orig_coeff) # phase factor to account for -i * sign
135+
U = phase * mod_unitaries[i-1] # modified unitary with phase
136+
state_mat[i, :] = U.dot(state_mat[i, :])
137+
# Unprepare ancilla (project onto |0> equivalent state anc_state_time):
138+
main_proj_time = anc_state_time.conjugate().T @ state_mat
139+
p_success = la.norm(main_proj_time) ** 2
140+
cumulative_success *= p_success
141+
if p_success < 1e-12:
142+
# Failure (very low success probability); break to avoid division by zero
143+
psi = np.zeros_like(psi)
144+
break
145+
psi = main_proj_time / np.sqrt(p_success) # normalize state after successful postselection
146+
if show_steps:
147+
print(f" Segment {seg+1}: success probability = {p_success:.4f}")
148+
if segments > 1:
149+
print(f" Combined success probability (one try per segment) = {cumulative_success:.4e}")
150+
# Now `psi` is the final state after simulated evolution (if all segments succeeded).
151+
# For verification, compute the exact state by directly exponentiating H.
152+
psi_exact = scipy.linalg.expm(-1j * H * time_param).dot(psi0)
153+
fidelity = np.abs(np.vdot(psi, psi_exact))**2
154+
print(f"Final state fidelity vs exact e^(-iHt)|psi0>: {fidelity:.4f}")
155+
if la.norm(psi_exact) > 1e-12:
156+
phase_diff = np.angle(np.vdot(psi, psi_exact))
157+
print(f"Global phase difference (simulated vs exact) [rad]: {phase_diff:.4f}")
158+
# Note: The first-order LCU approximation incurs error O((H Δt)^2). Using more segments (smaller Δt) or higher-order formulas can improve accuracy [oai_citation:12‡journals.aps.org](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.6.010359#:~:text=challenging,high%20accuracy%20with%20minimal%20resources).
159+
# The success probability may decrease with larger segments, but one can use repeat-until-success or oblivious amplitude amplification to eventually obtain the final state [oai_citation:13‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=The%20LCU%20protocol%20is%20deemed,and%20the%20transformed%20target%20variable) [oai_citation:14‡qrisp.eu](https://qrisp.eu/reference/Primitives/LCU.html#:~:text=The%20success%20probability%20depends%20on,included%20as%20an%20experimental%20feature).

0 commit comments

Comments
 (0)