|
| 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