22import argparse
33import importlib
44import logging
5- import sys
65import multiprocessing
6+ import sys
7+ import warnings
78from dataclasses import dataclass , field
89from pathlib import Path
910from random import Random
10- from typing import Callable , Sequence , TypedDict
11+ from typing import Any , Callable , Sequence , TypedDict
1112
1213import numpy as np
14+ import openfermion as of
1315import pandas as pd
16+ import scipy
1417import tequila as tq
1518from numpy import eye , floating
1619from numpy .typing import NDArray
@@ -40,6 +43,8 @@ class OptimizationResult(TypedDict):
4043 orbital_coefficients : NDArray | None
4144 orbital_transformation : NDArray | None
4245 variables : dict | None
46+ circuit : Any | None
47+ molecule : Any | None
4348 custom_data : Sequence [CustomData ] | None
4449
4550
@@ -55,6 +60,9 @@ class Job:
5560 # custom optimization algorithm
5661 custom_job_data : Sequence [CustomData ] = field (default_factory = list )
5762
63+ # Flag to determine whether fidelity should be calculated
64+ calculate_fidelity : bool = False
65+
5866 # custom arguments that one might want to pass to the optimization execution
5967 kwargs : dict = field (default_factory = dict )
6068
@@ -134,6 +142,7 @@ def run_fci_optimization(cls, molecule: QuantumChemistryBase, *args, **kwargs):
134142 orbital_transformation = None ,
135143 variables = None ,
136144 custom_data = None ,
145+ circuit = None ,
137146 )
138147
139148 @classmethod
@@ -145,7 +154,7 @@ def run_spa_optimization(
145154 edges = edges , vertices = coordinates
146155 ).T
147156
148- U = molecule .make_ansatz (name = "HCB-SPA" , edges = edges )
157+ U = molecule .make_ansatz (name = "HCB-SPA" , edges = edges ) # Hardcore Boson Circuit
149158
150159 opt = tq .chemistry .optimize_orbitals (
151160 molecule .use_native_orbitals (),
@@ -160,14 +169,40 @@ def run_spa_optimization(
160169
161170 E = tq .ExpectationValue (H = H , U = U )
162171 result = tq .minimize (E , silent = True )
172+
173+ U_x = mol .hcb_to_me (U )
174+
163175 return OptimizationResult (
164176 energy = result .energy ,
165177 orbital_coefficients = opt .molecule .integral_manager .orbital_coefficients ,
166178 orbital_transformation = opt .mo_coeff ,
167179 variables = result .variables ,
180+ circuit = U_x ,
181+ molecule = mol ,
168182 custom_data = None ,
169183 )
170184
185+ @classmethod
186+ def get_ground_states (cls , molecule , num_states = 10 ):
187+
188+ H = molecule .make_hamiltonian ().to_openfermion ()
189+ H_sparse = of .linalg .get_sparse_operator (H )
190+ v , vv = scipy .sparse .linalg .eigsh (
191+ H_sparse , k = num_states , ncv = 100 , sigma = molecule .compute_energy ("fci" )
192+ )
193+
194+ wfns = [tq .QubitWaveFunction .from_array (vv [:, i ]) for i in range (num_states )]
195+ energies = v .tolist ()
196+
197+ return wfns , energies
198+
199+ @classmethod
200+ def calculate_fidelity (cls , true_state , optimized_state ):
201+ inner_product = true_state .inner (optimized_state )
202+ fidelity = abs (inner_product ) ** 2
203+
204+ return fidelity
205+
171206 @classmethod
172207 def generate_jobs (
173208 cls ,
@@ -177,6 +212,7 @@ def generate_jobs(
177212 method = "spa" ,
178213 custom_method = None ,
179214 compare_to = [],
215+ calculate_fidelity = False , # parameter for fidelity between methods
180216 ):
181217 def get_algorithm_from_method (method ) -> Callable :
182218 if callable (method ):
@@ -202,13 +238,16 @@ def get_algorithm_from_method(method) -> Callable:
202238
203239 custom_job_data = []
204240
241+ fidelity_flag = calculate_fidelity and method != "fci"
242+
205243 if custom_method :
206244 job = Job (
207245 id = i ,
208246 geometry = geometry ,
209247 coordinates = coordinates ,
210248 optimization_algorithm = custom_method ,
211249 custom_job_data = custom_job_data ,
250+ calculate_fidelity = fidelity_flag ,
212251 )
213252 jobs .append (job )
214253 else :
@@ -218,6 +257,7 @@ def get_algorithm_from_method(method) -> Callable:
218257 coordinates = coordinates ,
219258 optimization_algorithm = get_algorithm_from_method (method ),
220259 custom_job_data = custom_job_data ,
260+ calculate_fidelity = fidelity_flag ,
221261 )
222262 jobs .append (job )
223263
@@ -226,12 +266,14 @@ def get_algorithm_from_method(method) -> Callable:
226266 # do not duplicate methods
227267 if compare == method and not custom_method :
228268 continue
269+ fidelity_flag = calculate_fidelity and compare != "fci"
229270 job = Job (
230271 id = i ,
231272 geometry = geometry ,
232273 coordinates = coordinates ,
233274 optimization_algorithm = get_algorithm_from_method (compare ),
234275 custom_job_data = custom_job_data ,
276+ calculate_fidelity = fidelity_flag ,
235277 )
236278 jobs .append (job )
237279
@@ -240,13 +282,40 @@ def get_algorithm_from_method(method) -> Callable:
240282 @classmethod
241283 def execute_job (cls , job : Job , basis_set = "sto-3g" ):
242284 mol = tq .Molecule (geometry = job .geometry , basis_set = basis_set )
243- return job .optimization_algorithm (
285+
286+ result = job .optimization_algorithm (
244287 mol , coordinates = job .coordinates , ** job .kwargs
245288 )
246289
290+ if job .calculate_fidelity :
291+ if "circuit" in result :
292+ mol = result ["molecule" ] # use same molecule as in the optimization
293+ optimized_variables = result ["variables" ]
294+ U = result ["circuit" ]
295+
296+ state = tq .simulate (U , optimized_variables )
297+
298+ wfns , energies = cls .get_ground_states (molecule = mol )
299+ fidelity = 0.0
300+ k = 0
301+
302+ while k < len (wfns ) - 1 :
303+ fidelity += cls .calculate_fidelity (state , wfns [k ])
304+ if fidelity < 1e-6 :
305+ k += 1
306+ continue
307+ if abs (energies [k ] - energies [k + 1 ]) < 1e-6 :
308+ break
309+
310+ k += 1
311+
312+ return {"result" : result , "fidelity" : fidelity }
313+ else :
314+ return {"result" : result , "fidelity" : None }
315+
247316 @classmethod
248317 def create_result_df (
249- cls , jobs : Sequence [Job ], job_results : Sequence [OptimizationResult ]
318+ cls , jobs : Sequence [Job ], job_results : Sequence [dict [ str , Any ] ]
250319 ) -> pd .DataFrame :
251320 max_variable_count = max (
252321 [0 ]
@@ -263,15 +332,19 @@ def create_result_df(
263332 f"{ job .optimization_algorithm .__module__ } .{ job .optimization_algorithm .__name__ } "
264333 for job in jobs
265334 ],
266- "optimized_energy" : [result ["energy" ] for result in job_results ],
335+ "optimized_energy" : [
336+ entry ["result" ]["energy" ] for entry in job_results
337+ ],
338+ "fidelity" : [entry ["fidelity" ] for entry in job_results ],
267339 "optimized_variable_count" : max_variable_count ,
268340 "atom_count" : [len (job .coordinates ) for job in jobs ],
269341 "edge_count" : [len (job .coordinates ) // 2 for job in jobs ],
270342 }
271343 )
272344
273345 # apply custom data to data frame
274- for i , result in enumerate (job_results ):
346+ for i , entry in enumerate (job_results ):
347+ result = entry ["result" ]
275348 if "custom_data" not in result :
276349 continue
277350 custom_data = result ["custom_data" ]
@@ -423,6 +496,14 @@ def main():
423496 help = "parameter for the job generator: influences the distance between atoms" ,
424497 )
425498
499+ parser .add_argument (
500+ "--fidelity" ,
501+ "-F" ,
502+ action = "store_true" ,
503+ default = False ,
504+ help = "calculate the fidelity between the true ground state and the optimized state" ,
505+ )
506+
426507 parser .add_argument (
427508 "-v" ,
428509 "--verbose" ,
@@ -448,13 +529,21 @@ def main():
448529
449530 job_generator = DataGenerator .generate_jobs
450531
532+ if number_of_atoms >= 10 and args .fidelity == True :
533+ warnings .warn (
534+ "The calculations may be very slow due to the high number of atoms, and enabled fidelity calculation ." ,
535+ UserWarning ,
536+ 2 ,
537+ )
538+
451539 jobs = job_generator (
452540 number_of_atoms = number_of_atoms ,
453541 number_of_jobs = number_of_jobs ,
454542 size = args .size ,
455543 method = args .method ,
456544 custom_method = opt_method ,
457545 compare_to = args .compare_to ,
546+ calculate_fidelity = args .fidelity ,
458547 )
459548
460549 job_results = []
0 commit comments