|
| 1 | +""" |
| 2 | +******************************************************************************** |
| 3 | +* Copyright (c) 2025 the Qrisp authors |
| 4 | +* |
| 5 | +* This program and the accompanying materials are made available under the |
| 6 | +* terms of the Eclipse Public License 2.0 which is available at |
| 7 | +* http://www.eclipse.org/legal/epl-2.0. |
| 8 | +* |
| 9 | +* This Source Code may also be made available under the following Secondary |
| 10 | +* Licenses when the conditions for such availability set forth in the Eclipse |
| 11 | +* Public License, v. 2.0 are satisfied: GNU General Public License, version 2 |
| 12 | +* with the GNU Classpath Exception which is |
| 13 | +* available at https://www.gnu.org/software/classpath/license.html. |
| 14 | +* |
| 15 | +* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| 16 | +******************************************************************************** |
| 17 | +""" |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +from qrisp import control, cx , swap, QuantumFloat, QuantumArray, h, z, invert,ry, gate_wrap |
| 22 | + |
| 23 | + |
| 24 | +from scipy.sparse.linalg import eigsh |
| 25 | +from qrisp.alg_primitives import dicke_state |
| 26 | +from scipy.sparse import diags |
| 27 | + |
| 28 | +import numpy as np |
| 29 | + |
| 30 | +def gauss_jordan_operations_general(matrix): |
| 31 | + """ |
| 32 | + Extracts the actions of a Gauss-Jordan-elimination, which affect the relevants (last) column of an augmented matrix (the augmenting rows are not included in the input matrix) and returns the operations, |
| 33 | + encoded as a list of strings (gates) and integers (qubits). |
| 34 | +
|
| 35 | + Parameters |
| 36 | + ---------- |
| 37 | + matrix : np.ndarray |
| 38 | + Definition of the matrix for extraction of the operations. |
| 39 | +
|
| 40 | + Returns |
| 41 | + ------- |
| 42 | + operations : List |
| 43 | + The operations on the relevant rows, encoded as a list of strings (gates) and integers (qubits). |
| 44 | + mat : np.ndarray |
| 45 | + The matrix after the operations. |
| 46 | + |
| 47 | + """ |
| 48 | + # quick exit on empty or invalid matrix |
| 49 | + if not matrix or not matrix[0]: |
| 50 | + return [], [] |
| 51 | + m = len(matrix) |
| 52 | + num_cols = len(matrix[0]) |
| 53 | + n = num_cols - 1 # last column is RHS |
| 54 | + # work on a copy |
| 55 | + mat = [row.copy() for row in matrix] |
| 56 | + operations = [] |
| 57 | + pivot_row = 0 |
| 58 | + pivot_col = 0 |
| 59 | + while pivot_row < m and pivot_col < n: |
| 60 | + # find pivot in this column at or below pivot_row |
| 61 | + pivot_idx = None |
| 62 | + for r in range(pivot_row, m): |
| 63 | + if mat[r][pivot_col] == 1: |
| 64 | + pivot_idx = r |
| 65 | + break |
| 66 | + if pivot_idx is None: |
| 67 | + pivot_col += 1 |
| 68 | + continue |
| 69 | + |
| 70 | + # swap into pivot position if needed |
| 71 | + if pivot_idx != pivot_row: |
| 72 | + operations.append(('swap', pivot_row, pivot_idx)) |
| 73 | + mat[pivot_row], mat[pivot_idx] = mat[pivot_idx], mat[pivot_row] |
| 74 | + |
| 75 | + # eliminate other rows |
| 76 | + pivot_data = mat[pivot_row] |
| 77 | + for r in range(m): |
| 78 | + if r != pivot_row and mat[r][pivot_col] == 1: |
| 79 | + operations.append(('xor', pivot_row, r)) |
| 80 | + # XOR rows from pivot_col onward |
| 81 | + row_r = mat[r] |
| 82 | + row_r[pivot_col:] = [a ^ b for a, b in zip(row_r[pivot_col:], pivot_data[pivot_col:])] |
| 83 | + |
| 84 | + pivot_row += 1 |
| 85 | + pivot_col += 1 |
| 86 | + |
| 87 | + return operations, mat |
| 88 | + |
| 89 | +def syndrome_decoding(matrix, qv_syndrome): |
| 90 | + """ |
| 91 | + Performs the decoding step of the DQI algorithm on a binary ``QuantumArray``, by extracting the operations in a classical way. |
| 92 | + Only the actions, which affect the relevants (last) column of an augmented matrix are considered. The row, by which the matrix is augmented is given by ``qv_syn``. |
| 93 | +
|
| 94 | + Parameters |
| 95 | + ---------- |
| 96 | + matrix : np.ndarray |
| 97 | + Definition of the matrix on which the the decoding is based on. |
| 98 | + qv_syndrome : QuantumArray |
| 99 | + ``QuantumArray`` to be decoded. |
| 100 | + |
| 101 | + Returns |
| 102 | + ------- |
| 103 | + mat : np.ndarray |
| 104 | + The matrix after the operations, according to function ``gauss_jordan_operations_general```. |
| 105 | + qv_syn : QuantumArray |
| 106 | + ``QuantumArray`` after decoding. |
| 107 | + |
| 108 | + """ |
| 109 | + |
| 110 | + qv_syn = qv_syndrome |
| 111 | + if isinstance(matrix, np.ndarray): |
| 112 | + mat_list = matrix.tolist() |
| 113 | + ops, mat = gauss_jordan_operations_general(mat_list) |
| 114 | + num_qubits = len(mat_list) |
| 115 | + for op_type, src, tgt in ops: |
| 116 | + if op_type == "swap": |
| 117 | + swap(qv_syn[src][0], qv_syn[tgt][0]) |
| 118 | + elif op_type == "xor": |
| 119 | + cx(qv_syn[src][0], qv_syn[tgt][0]) |
| 120 | + |
| 121 | + return mat , qv_syn |
| 122 | + |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | + |
| 128 | +# m is number of constraints, p is the that defines F_p (=2 in case of MAX-XORSAT), r=1 in case of MAX-XORSAT |
| 129 | +def get_optimal_w(m: int, l: int, p: int, r: int) -> np.ndarray: |
| 130 | + r""" |
| 131 | + Solve for the eigenvector of the symmetric tridiagonal matrix, which emerges from the expected number of solved constraints. |
| 132 | + Used for a fan-out Uniary Amplitude Encoding state-preparation. |
| 133 | + (See Lemma 9.2 in the `DQI paper <https://arxiv.org/pdf/2408.08292>`_) |
| 134 | +
|
| 135 | + Parameters |
| 136 | + ---------- |
| 137 | + m : int |
| 138 | + The number of constraints, i.e. rows of the input matrix. |
| 139 | + l : int |
| 140 | + Degree for polynomial to encode objective function in. Can be chosen in an optimized way. |
| 141 | + p : int |
| 142 | + Defines the modulus of the field, i.e. $\mathcal{F}_p$. |
| 143 | + r : int |
| 144 | + Number of inputs yielding $+1$. |
| 145 | +
|
| 146 | + Returns |
| 147 | + ------- |
| 148 | + weights : np.ndarray |
| 149 | + Eigenvector of the symmetric tridiagonal matrix. |
| 150 | +
|
| 151 | + """ |
| 152 | + d = (p - 2*r) / np.sqrt(r * (p - r)) |
| 153 | + diag = np.arange(l + 1) * d |
| 154 | + off = np.sqrt(np.arange(1, l + 1) * (m - np.arange(1, l + 1) + 1)) |
| 155 | + |
| 156 | + A = diags([off, diag, off], offsets=(-1, 0, 1), format="csr") |
| 157 | + |
| 158 | + _, vecs = eigsh(A, k=1, which="LA") |
| 159 | + w = vecs.flatten() |
| 160 | + |
| 161 | + orig = len(w) |
| 162 | + pad_len = 2**int(np.ceil(np.log2(l+1))) |
| 163 | + weights = list(w) |
| 164 | + weights += [0 for i in range((pad_len -orig)) ] |
| 165 | + return weights |
| 166 | + |
| 167 | + |
| 168 | +#@gate_wrap |
| 169 | +def uae_encoding(qa_error, num_constraints, weights): |
| 170 | + r""" |
| 171 | + Fan-out Uniary Amplitude Encoding state-preparation, based on the weights received from solving for the eigenvector of the symmetric tridiagonal matrix. |
| 172 | + |
| 173 | + Parameters |
| 174 | + ---------- |
| 175 | + qa_error : QuantumArray |
| 176 | + The ``QuantumArray`` (in the form of single qubit ``QuantumVariables``) to encode. |
| 177 | + num_constraints : int |
| 178 | + The number of constraints, i.e. rows of the input matrix. |
| 179 | + weights : np.ndarray |
| 180 | + Eigenvector of the symmetric tridiagonal matrix. |
| 181 | +
|
| 182 | + """ |
| 183 | + |
| 184 | + weights = weights/sum(weights) |
| 185 | + w2 = weights |
| 186 | + cum = np.concatenate(([0.0], np.cumsum(w2[:-1]))) |
| 187 | + denom = 1.0 - cum |
| 188 | + # Avoid division by zero, clip ratios |
| 189 | + ratio = np.where(denom > 0, w2 / denom, 0.0) |
| 190 | + ratio = np.clip(ratio, 0.0, 1.0) |
| 191 | + betas = 2.0 * np.arccos(np.sqrt(ratio)) |
| 192 | + |
| 193 | + # Apply first RY if nonzero |
| 194 | + if betas[0] != 0.0 and not np.isnan(betas[0]): |
| 195 | + ry(betas[0], qa_error[0][0]) |
| 196 | + |
| 197 | + # Controlled rotations |
| 198 | + for i in range(1, num_constraints): |
| 199 | + if i < betas.size: |
| 200 | + b = betas[i] |
| 201 | + if b != 0.0 and not np.isnan(b): |
| 202 | + with control( qa_error[i-1][0]): |
| 203 | + ry(b, qa_error[i][0]) |
| 204 | + |
| 205 | + |
| 206 | +def specific_phase_encoding(qa_error, v): |
| 207 | + r""" |
| 208 | + Specific phase encoding for DQI problem instance. |
| 209 | +
|
| 210 | + Parameters |
| 211 | + ---------- |
| 212 | + qa_error : QuantumArray |
| 213 | + The ``QuantumArray`` (in the form of single qubit ``QuantumVariables``) to encode. |
| 214 | + v_j : List |
| 215 | + Phase information corresponding to the problem constraints |
| 216 | +
|
| 217 | + """ |
| 218 | + for index in range(len(v)): |
| 219 | + if v[index] == 1: |
| 220 | + z(qa_error[index]) |
| 221 | + |
| 222 | + |
| 223 | + |
| 224 | +def constraint_encoding(qa_error, qv_syndrome, B): |
| 225 | + r""" |
| 226 | + Constraint encoding for DQI problem instance. |
| 227 | +
|
| 228 | + Parameters |
| 229 | + ---------- |
| 230 | + qa_error : QuantumArray |
| 231 | + The ``QuantumArray`` (in the form of single qubit ``QuantumVariables``) which represents the error register. |
| 232 | + qa_error : QuantumArray |
| 233 | + The ``QuantumArray`` (in the form of single qubit ``QuantumVariables``) which represents the syndrome register. |
| 234 | + B : np.ndarray |
| 235 | + Problem matrix to encode. |
| 236 | +
|
| 237 | + """ |
| 238 | + # reverse indices due to circuit definition |
| 239 | + qa_error, qv_syndrome = qa_error[::-1], qv_syndrome[::-1] |
| 240 | + |
| 241 | + # create B^T |
| 242 | + B_t = np.transpose(B) |
| 243 | + |
| 244 | + # encode constraints |
| 245 | + i_ind, j_ind = B_t.shape |
| 246 | + for i in range(i_ind): |
| 247 | + for j in range(j_ind): |
| 248 | + if B_t[i][j] == 1: |
| 249 | + cx(qa_error[j], qv_syndrome[i]) |
| 250 | + |
| 251 | + return qa_error, qv_syndrome |
| 252 | + |
| 253 | + |
| 254 | + |
| 255 | + |
| 256 | + |
| 257 | + |
| 258 | +def dqi_optimization(B, v, l=2, p=2, r=1): |
| 259 | + r""" |
| 260 | + Implementation of `Optimization by Decoded Quantum Interferometry <https://arxiv.org/pdf/2408.08292>`_, with inspiration from `Quantum Circuit Design for Decoded Quantum |
| 261 | + Interferometry <https://arxiv.org/pdf/2504.18334>`_. |
| 262 | + This algorithm is used to solve optimization problems, by using the quantum |
| 263 | + Fourier transform (or more so properties) to reduce optimization problems to decoding problems. |
| 264 | + For an indepth explanation, please refer to the referenced papers. |
| 265 | +
|
| 266 | + Parameters |
| 267 | + ---------- |
| 268 | + B : np.ndarray |
| 269 | + Definition of the problem instance. |
| 270 | + v : List |
| 271 | + Constraints enforced on the solution. |
| 272 | + l : int |
| 273 | + Degree for polynomial to encode objective function in. Can be chosen in an optimized way. |
| 274 | + p : int |
| 275 | + Defines the modulus of the field, i.e. $\mathcal{F}_p$. |
| 276 | + r : int |
| 277 | + Number of inputs yielding $+1$. |
| 278 | +
|
| 279 | + Returns |
| 280 | + ------- |
| 281 | + qa_error : QuantumArray |
| 282 | + The ``QuantumArray`` (in the form of single qubit ``QuantumVariables``) which represents the final error register. |
| 283 | + qa_error : QuantumArray |
| 284 | + The ``QuantumArray`` (in the form of single qubit ``QuantumVariables``) which represents the final syndrome register. |
| 285 | + |
| 286 | + Examples |
| 287 | + -------- |
| 288 | +
|
| 289 | + We create an example case for a maxCut instance. |
| 290 | + |
| 291 | + :: |
| 292 | +
|
| 293 | + B = np.array([ |
| 294 | + [1, 1, 0, 0, 0, 0], |
| 295 | + [1, 0, 0, 0, 1, 0], |
| 296 | + [0, 1, 1, 0, 0, 0], |
| 297 | + [0, 0, 1, 1, 0, 0], |
| 298 | + [0, 0, 0, 1, 0, 1], |
| 299 | + [0, 0, 0, 0, 1, 1] |
| 300 | + ]) |
| 301 | +
|
| 302 | + v = np.array( |
| 303 | + [1, 1, 1, 1, 1, 1], |
| 304 | + ) |
| 305 | +
|
| 306 | +
|
| 307 | +
|
| 308 | + qvin, resqf = dqi_optimization(B, v) |
| 309 | + res_dict = multi_measurement([qvin, resqf]) |
| 310 | +
|
| 311 | + """ |
| 312 | + # L =2 for MaxCut |
| 313 | + |
| 314 | + m = B.shape[0] |
| 315 | + n = B.shape[1] |
| 316 | + q_type = QuantumFloat(1, 0) |
| 317 | + qa_error = QuantumArray(qtype= q_type , shape=(m,)) |
| 318 | + |
| 319 | + # create syndrome quantumArray |
| 320 | + qa_syndrome = QuantumArray(qtype= q_type , shape=(m,)) |
| 321 | + |
| 322 | + # l can be chosen to be equal to m, or as rank(B) as an optimized version (or even more optimized) |
| 323 | + # --> it defines the uae_eigenvector consideration and size qv_inner, |
| 324 | + princ_eigenvec = get_optimal_w(m,l,p,r) |
| 325 | + |
| 326 | + # UAE encoding encoding |
| 327 | + uae_encoding(qa_error, m, princ_eigenvec) |
| 328 | + |
| 329 | + # need to reverse due to dicke_state function definition |
| 330 | + dicke_state(qa_error[::-1], len(qa_error)) |
| 331 | + |
| 332 | + # phase encoding |
| 333 | + specific_phase_encoding(qa_error, v) |
| 334 | + |
| 335 | + # constraint encoding |
| 336 | + qa_error, qa_syndrome = constraint_encoding(qa_error, qa_syndrome, B) |
| 337 | + |
| 338 | + # syndrome decoding step, this will in the future be replaced by a quantum version |
| 339 | + syndrome_decoding(B.T, qa_syndrome) |
| 340 | + |
| 341 | + # then perform further application onto the error register |
| 342 | + for i in range(m): |
| 343 | + cx(qa_syndrome[i], qa_error[i]) |
| 344 | + |
| 345 | + with invert(): |
| 346 | + #quantum_GJE(Bt_full) |
| 347 | + syndrome_decoding(B.T, qa_syndrome) |
| 348 | + |
| 349 | + # H-transform for readout |
| 350 | + h(qa_syndrome) |
| 351 | + |
| 352 | + |
| 353 | + return qa_error, qa_syndrome |
| 354 | + |
| 355 | + |
0 commit comments