microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/python-cli

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

samples/estimation/df-chemistry/chemistry.py

501lines · modecode

1# Copyright (c) Microsoft Corporation. All rights reserved.
2# Licensed under the MIT License.
3import math
4import numpy as np
5import numpy.typing as npt
6import qsharp
7from argparse import ArgumentParser
8from dataclasses import dataclass
9from scipy import linalg as LA
10from urllib.parse import urlparse
11from urllib.request import urlretrieve
12
13
14@dataclass
15class FCIDumpFileContent:
16 num_orbitals: int
17 one_body_terms: list[tuple[list[int], float]]
18 two_body_terms: list[tuple[list[int], float]]
19
20 @classmethod
21 def from_file(self, file_url):
22 """Read FCIDump file (given by either a local path or a URL) and parse
23 it to get the input data for double-factorized chemistry algorithm."""
24 url = urlparse(file_url)
25
26 file_name = ""
27 if url.scheme in ["http", "https"]:
28 # Download the file
29 file_name = url.path.rsplit("/", 1)[-1]
30 print(f"Downloading {file_url} to {file_name}...")
31 urlretrieve(file_url, file_name)
32 elif url.scheme in ["", "file"]:
33 # Use the whole URL as the file path
34 file_name = file_url
35 else:
36 raise f"Unsupported file location {file_url}"
37
38 with open(file_name, "r") as f:
39 lines = [str.strip() for str in f.readlines()]
40
41 assert lines[0].startswith("&FCI")
42
43 parse_header = True
44 header = ""
45 header_values = {}
46 self.one_body_terms = []
47 self.two_body_terms = []
48
49 for line in lines:
50 if parse_header:
51 # File header might not have key-value pairs in separate lines,
52 # so accumulate the whole header first and then parse.
53 header += line + " "
54 if line.endswith("&END"):
55 # Strip "&FCI" and "&END"
56 rest_header = header[4:-4]
57
58 # Find each key-value pair based on the "=" sign
59 while (ind := rest_header.find("=")) > -1:
60 key = rest_header[0:ind].strip()
61 rest_header = rest_header[ind + 1 :]
62 # Figure out which part of the rest is value: until
63 # either whitespace or (if key is not ORBSYM) comma
64 ind = rest_header.find("," if key != "ORBSYM" else " ")
65 value = rest_header[0:ind].strip()
66 rest_header = rest_header[ind + 1 :]
67 if key == "ORBSYM":
68 value = value[:-1]
69 header_values[key] = value
70 parse_header = False
71 else:
72 tokens = line.split()
73 coefficient = float(tokens[0])
74 indices = [int(str) - 1 for str in tokens[1:] if str != "0"]
75
76 if len(indices) == 2:
77 indices.sort()
78 self.one_body_terms.append((indices, coefficient))
79 elif len(indices) == 4:
80 # conversion from Mulliken to Dirac
81 i = indices[0]
82 j = indices[2]
83 k = indices[3]
84 l = indices[1]
85
86 symmetries = [
87 [i, j, k, l],
88 [j, i, l, k],
89 [k, l, i, j],
90 [l, k, j, i],
91 [i, k, j, l],
92 [k, i, l, j],
93 [j, l, i, k],
94 [l, j, k, i],
95 ]
96 symmetries.sort()
97 self.two_body_terms.append((symmetries[0], coefficient))
98
99 self.num_orbitals = int(header_values["NORB"])
100
101 return self
102
103
104@dataclass
105class TwoBodyResult:
106 eigenvalues: list[npt.NDArray[np.float64]]
107 eigenvectors: list[npt.NDArray[np.float64]]
108 one_norms: list[float]
109 two_norms: list[float]
110
111
112@dataclass
113class DoubleFactorization:
114 num_orbitals: int
115 rank: int
116 one_body_norm: float
117 two_body_norm: float
118 one_body_eigenvalues: list[float]
119 one_body_eigenvectors: npt.NDArray[npt.NDArray[np.float64]]
120 two_body_eigenvalues: npt.NDArray[npt.NDArray[np.float64]]
121 two_body_eigenvectors: list[npt.NDArray[npt.NDArray[np.float64]]]
122
123 @classmethod
124 def process_fcidump(self, structure: FCIDumpFileContent, error: float):
125
126 # The `structure` provides us with one- and two electron integrals
127 # h_{ij} and h_{ijkl} as described in Eq. (2). These coefficients are
128 # real and satisfy some symmetries which are outlined in Eq. (6).
129
130 # In this step we compute R (`rank`) as in Eq. (7), h_{ij}
131 # (`one_electron_vector`), and L_{ij}^{(r)} (`two_electron_vectors`).
132 (rank, eigenvalue_signs, one_electron_vector, two_electron_vectors) = (
133 self.perform_svd(structure)
134 )
135
136 # This computes Eq. (15). It stores L^{-1}_{ij} into
137 # `one_body_eigenvectors`, which is repurposed from
138 # `one_electron_vector` and uses the same indices. It also returns the
139 # Schatten norm, which is the first summand in Eq. (16).
140 (one_body_eigenvalues, one_body_eigenvectors, one_body_norm, norm2) = (
141 self.process_one_body_term(
142 structure.num_orbitals,
143 rank,
144 one_electron_vector,
145 eigenvalue_signs,
146 two_electron_vectors,
147 )
148 )
149
150 assert len(one_body_eigenvectors) == structure.num_orbitals**2
151
152 # This computes the second sum of Eq. (9), it computes the eigenvalues
153 # \lambda_m^{(r)} and eigenvectors $\vec R_{m,i}^{(r)}$. It does not
154 # normalize the vectors, but returns the one- and two-norms of the
155 # eigenvalues.
156 two_body_result = self.process_two_body_terms(
157 structure.num_orbitals, rank, two_electron_vectors
158 )
159
160 # Discard terms that will make the description exceed the error budget
161 self.truncate_terms(structure.num_orbitals, error, two_body_result)
162
163 # This computes the second summand in Eq. (16).
164 two_body_norm = 0.0
165 two_body_norm += sum(0.25 * norm * norm for norm in two_body_result.one_norms)
166
167 # Reshape the one body eigenvectors so that they are represented
168 # row-wise in a 2D array
169 one_body_eigenvectors = np.reshape(
170 one_body_eigenvectors, (structure.num_orbitals, structure.num_orbitals)
171 )
172
173 for i in range(len(two_body_result.eigenvectors)):
174 cols = structure.num_orbitals
175 rows = int(len(two_body_result.eigenvectors[i]) / cols)
176 assert rows * cols == len(two_body_result.eigenvectors[i])
177
178 # Reshape the two body eigenvectors so that they represented
179 # row-wise in a 2D array
180 two_body_result.eigenvectors[i] = np.reshape(
181 two_body_result.eigenvectors[i], (rows, cols)
182 )
183
184 self.num_orbitals = structure.num_orbitals
185 # Use the post-truncation rank
186 self.rank = len(two_body_result.eigenvectors)
187 self.one_body_norm = one_body_norm
188 self.two_body_norm = two_body_norm
189 self.one_body_eigenvalues = one_body_eigenvalues
190 self.one_body_eigenvectors = one_body_eigenvectors
191 self.two_body_eigenvalues = two_body_result.eigenvalues
192 self.two_body_eigenvectors = two_body_result.eigenvectors
193
194 return self
195
196 def combined_index(i: int, j: int) -> int:
197 return int(max(i, j) * (max(i, j) + 1) / 2 + min(i, j))
198
199 def vectors_to_sym_mat(
200 vector: npt.NDArray[float], dimension: int
201 ) -> npt.NDArray[npt.NDArray[float]]:
202 matrix = np.zeros((dimension, dimension), dtype=float)
203
204 # Create lower triangular matrix
205 matrix[np.tril_indices(dimension, 0)] = vector
206
207 # Convert lower triangular matrix to symmetrix matrix
208 matrix = matrix + matrix.T
209
210 # Halve elements of diagonal to avoid doubling
211 matrix = matrix - 0.5 * np.diag(np.diagonal(matrix, 0))
212
213 return matrix
214
215 @classmethod
216 def populate_two_body_terms(
217 self, two_body_terms: list[tuple[list[int], float]]
218 ) -> list[tuple[list[int], float]]:
219 complete_two_body_terms = []
220
221 for [i, j, k, l], val in two_body_terms:
222 ii = self.combined_index(i, l)
223 jj = self.combined_index(j, k)
224
225 complete_two_body_terms.append(([ii, jj], val))
226 if ii != jj:
227 complete_two_body_terms.append(([jj, ii], val))
228
229 return complete_two_body_terms
230
231 @classmethod
232 def eigen_svd(
233 self, orbitals: int, two_body_terms: list[tuple[list[int], float]]
234 ) -> (int, list[int], list[float]):
235 # combined = nC2 for n = orbitals
236 combined = int(orbitals * (orbitals + 1) / 2)
237
238 coeff_matrix = np.zeros((combined, combined), dtype=float)
239 for [i, j], v in two_body_terms:
240 coeff_matrix[int(i)][int(j)] = v
241
242 # Compute the eigen decomposition of the symmetric coefficient matrix
243 # with two body terms
244 evals, evecs = LA.eigh(coeff_matrix)
245 rows, cols = np.shape(evecs)
246
247 evals_signs = np.zeros(cols, dtype=int)
248
249 # let eigenvectors be represented as row vectors
250 evecs = np.transpose(evecs)
251
252 # Scale eigenvector by square root of corresponding eigenvalue
253 for i in range(len(evals)):
254 evecs[i] = math.sqrt(abs(evals[i])) * evecs[i]
255 evals_signs[i] = np.sign(evals[i])
256
257 # Collect eigenvectors as 1D array
258 scaled_evecs_1D = np.reshape(evecs, cols * rows)
259
260 return (cols, evals_signs.tolist(), scaled_evecs_1D)
261
262 @classmethod
263 def perform_svd(
264 self, structure: FCIDumpFileContent
265 ) -> (int, npt.NDArray[int], npt.NDArray[float], npt.NDArray[float]):
266
267 full_two_body_terms = self.populate_two_body_terms(structure.two_body_terms)
268
269 # Compute the eigen decomposition of two-electron terms
270 (rank, eigenvalue_signs, two_electron_vectors) = self.eigen_svd(
271 structure.num_orbitals, full_two_body_terms
272 )
273
274 length = (
275 self.combined_index(structure.num_orbitals - 1, structure.num_orbitals - 1)
276 + 1
277 )
278 one_electron_vector = np.zeros((length), dtype=float)
279
280 # Collect one-electron terms into a single 2D array
281 for [i, j], v in structure.one_body_terms:
282 one_electron_vector[self.combined_index(i, j)] = v
283
284 return (rank, eigenvalue_signs, one_electron_vector, two_electron_vectors)
285
286 @classmethod
287 def eigen_decomp(
288 self, dimension: int, vector: npt.NDArray[float]
289 ) -> (npt.NDArray[float], npt.NDArray[float], float, float):
290 matrix = np.zeros((dimension, dimension), dtype=float)
291
292 # Create lower triangular matrix
293 matrix[np.tril_indices(dimension, 0)] = vector
294
295 # Compute the eigen decomposition of the lower triangular matrix
296 evals, evecs = LA.eigh(matrix, lower=True)
297 norm1 = np.linalg.norm(evals, 1)
298 norm2 = np.linalg.norm(evals)
299
300 (rows, cols) = np.shape(evecs)
301 evecs_1D = np.reshape(np.transpose(evecs), cols * rows)
302
303 return (evals, evecs_1D, norm1, norm2)
304
305 @classmethod
306 def process_one_body_term(
307 self,
308 orbitals: int,
309 rank: int,
310 one_electron_vector: npt.NDArray[float],
311 eigenvalue_signs: npt.NDArray[bool],
312 two_electron_vectors: npt.NDArray[float],
313 ) -> (list[float], npt.NDArray[float], float, float):
314 # combined = nC2 for n = orbitals
315 combined = int(orbitals * (orbitals + 1) / 2)
316
317 vector = np.zeros(combined, dtype=float)
318
319 for l in range(rank):
320 matrix = np.zeros((orbitals, orbitals), dtype=float)
321 H_l = self.vectors_to_sym_mat(
322 two_electron_vectors[range(combined * l, combined * (l + 1))], orbitals
323 )
324
325 H_issj = eigenvalue_signs[l] * np.matmul(H_l, H_l)
326
327 H_ssij = eigenvalue_signs[l] * np.trace(H_l) * H_l
328
329 matrix = -0.5 * H_issj + H_ssij
330
331 # Convert symmetric matrix to lower triangular matrix
332 vector += matrix[np.tril_indices(orbitals, 0)]
333
334 one_electron_vector += vector
335
336 (one_body_eigenvalues, one_body_eigenvectors, one_body_norm, norm2) = (
337 self.eigen_decomp(orbitals, one_electron_vector)
338 )
339
340 return (one_body_eigenvalues, one_body_eigenvectors, one_body_norm, norm2)
341
342 @classmethod
343 def process_two_body_terms(
344 self, orbitals: int, rank: int, two_electron_vectors: npt.NDArray[float]
345 ) -> TwoBodyResult:
346
347 two_body_eigenvectors = []
348 two_body_eigenvalues = []
349 one_norms = []
350 two_norms = []
351 combined = int(orbitals * (orbitals + 1) / 2)
352
353 for i in range(rank):
354 matrix = two_electron_vectors[range(combined * i, combined * (i + 1))]
355 (evals, evecs, norm1, norm2) = self.eigen_decomp(orbitals, matrix)
356
357 two_body_eigenvalues.append(evals)
358 two_body_eigenvectors.append(evecs)
359 one_norms.append(norm1)
360 two_norms.append(norm2)
361
362 two_body_result = TwoBodyResult(
363 two_body_eigenvalues, two_body_eigenvectors, one_norms, two_norms
364 )
365
366 return two_body_result
367
368 @classmethod
369 def truncate_terms(
370 self, orbitals: int, error_eigenvalues: float, two_body_result: TwoBodyResult
371 ):
372 values_with_error = []
373 for r, values in enumerate(two_body_result.eigenvalues):
374 for i, v in enumerate(values):
375 error = abs(v) * two_body_result.two_norms[r]
376 values_with_error.append((error, r, i))
377
378 # Sort in ascending order by error values
379 values_with_error = sorted(values_with_error, key=lambda tup: tup[0])
380
381 # Truncate the list so that the sum of squares of errors left is less
382 # than the square of the input error
383 total_error = 0
384 truncate = len(values_with_error)
385 for i, (error, _, _) in enumerate(values_with_error):
386 error_compare = error**2
387 if total_error + error_compare < error_eigenvalues**2:
388 total_error += error_compare
389 else:
390 truncate = i
391 break
392
393 # Keep the first `truncate` values
394 values_with_error = values_with_error[:truncate]
395
396 indices_by_rank = []
397 for _, r, i in values_with_error:
398 while r >= len(indices_by_rank):
399 indices_by_rank.append([])
400 indices_by_rank[r].append(i)
401
402 for r, indices in reversed(list(enumerate(indices_by_rank))):
403 if len(indices) == orbitals:
404 # All indices are to be removed: fully remove the r^th entry
405 # for the norms, eigenvalues and eigenvectors.
406 del two_body_result.eigenvalues[r]
407 del two_body_result.eigenvectors[r]
408 del two_body_result.one_norms[r]
409 del two_body_result.two_norms[r]
410 else:
411 indices.sort()
412 # Remove only eigenvalues/vectors corresponding to `indices`
413 two_body_result.eigenvalues[r] = np.delete(
414 arr=two_body_result.eigenvalues[r], obj=indices
415 )
416 arr = np.reshape(two_body_result.eigenvectors[r], (orbitals, orbitals))
417 arr = np.delete(arr=arr, obj=indices, axis=0)
418 (rows, cols) = np.shape(arr)
419 two_body_result.eigenvectors[r] = np.reshape(arr, rows * cols)
420
421 # Check that same number of terms are removed for norms,
422 # eigenvalues and eigenvectors
423 assert (
424 len(two_body_result.one_norms) == len(two_body_result.two_norms)
425 and len(two_body_result.one_norms) == len(two_body_result.eigenvalues)
426 and len(two_body_result.one_norms) == len(two_body_result.eigenvectors)
427 )
428
429
430# Convert 2D array into string representation
431def ndarray2d_to_string(arr):
432 str_arr = []
433 for elem in arr:
434 str_arr.append(np.array2string(elem, separator=","))
435 return f"[{','.join(str_arr)}]"
436
437
438# The script takes one required positional argument, URI of the FCIDUMP file
439parser = ArgumentParser(description="Double-factorized chemistry sample")
440# Use n2-10e-8o as the default sample.
441# Pass a different filename to get estimates for different compounds
442parser.add_argument(
443 "-f",
444 "--fcidumpfile",
445 default="https://aka.ms/fcidump/n2-10e-8o",
446 help="Path to the FCIDUMP file describing the Hamiltonian",
447)
448args = parser.parse_args()
449
450# ----- Read the FCIDUMP file and get resource estimates from Q# algorithm -----
451structure = FCIDumpFileContent.from_file(args.fcidumpfile)
452df = DoubleFactorization.process_fcidump(structure, 0.001)
453
454# Load Q# project
455qsharp.init(project_root=".")
456
457# Construct the Q# operation call for which we need to perform resource estimate
458str_one_body_eigenvalues = np.array2string(df.one_body_eigenvalues, separator=",")
459
460str_one_body_eigenvectors = ndarray2d_to_string(df.one_body_eigenvectors)
461
462str_two_body_eigenvalues = ndarray2d_to_string(df.two_body_eigenvalues)
463
464str_two_body_eigenvectors = (
465 "["
466 + ",".join(
467 [ndarray2d_to_string(eigenvectors) for eigenvectors in df.two_body_eigenvectors]
468 )
469 + "]"
470)
471
472qsharp_string = (
473 "Microsoft.Quantum.Applications.Chemistry.DoubleFactorizedChemistry("
474 "Microsoft.Quantum.Applications.Chemistry.DoubleFactorizedChemistryProblem("
475 f"{df.num_orbitals}, {df.one_body_norm}, {df.two_body_norm}, "
476 f"{str_one_body_eigenvalues}, {str_one_body_eigenvectors}, "
477 f"[1.0, size = {df.rank}], {str_two_body_eigenvalues}, "
478 f"{str_two_body_eigenvectors}),"
479 "Microsoft.Quantum.Applications.Chemistry.DoubleFactorizedChemistryParameters(0.001,))"
480)
481
482# Get resource estimates
483res = qsharp.estimate(
484 qsharp_string,
485 params={
486 "errorBudget": 0.01,
487 "qubitParams": {"name": "qubit_maj_ns_e6"},
488 "qecScheme": {"name": "floquet_code"},
489 },
490)
491
492# Store estimates in json file
493with open("resource_estimate.json", "w") as f:
494 f.write(res.json)
495
496# Print high-level resource estimation results
497print(f"Algorithm runtime: {res['physicalCountsFormatted']['runtime']}")
498print(
499 f"Number of physical qubits required: {res['physicalCountsFormatted']['physicalQubits']}"
500)
501print("For more detailed resource counts, see file resource_estimate.json")
502