@@ -50,7 +50,7 @@ class VQD(VariationalAlgorithm, Eigensolver):
50
50
51
51
`VQD <https://arxiv.org/abs/1805.08138>`__ is a quantum algorithm that uses a
52
52
variational technique to find
53
- the k eigenvalues of the Hamiltonian :math:`H` of a given system.
53
+ the k lowest eigenvalues of the Hamiltonian :math:`H` of a given system.
54
54
55
55
The algorithm computes excited state energies of generalised hamiltonians
56
56
by optimizing over a modified cost function where each successive eigenvalue
@@ -108,6 +108,9 @@ class VQD(VariationalAlgorithm, Eigensolver):
108
108
follows during each evaluation by the optimizer: the evaluation count,
109
109
the optimizer parameters for the ansatz, the estimated value, the estimation
110
110
metadata, and the current step.
111
+ convergence_threshold: A threshold under which the algorithm is considered to have
112
+ converged. It corresponds to the maximal average fidelity an eigenstate is allowed
113
+ to have with the previous eigenstates. If set to None, no check is performed.
111
114
"""
112
115
113
116
def __init__ (
@@ -121,6 +124,7 @@ def __init__(
121
124
betas : np .ndarray | None = None ,
122
125
initial_point : np .ndarray | list [np .ndarray ] | None = None ,
123
126
callback : Callable [[int , np .ndarray , float , dict [str , Any ], int ], None ] | None = None ,
127
+ convergence_threshold : float | None = None ,
124
128
) -> None :
125
129
"""
126
130
@@ -147,6 +151,9 @@ def __init__(
147
151
follows during each evaluation by the optimizer: the evaluation count,
148
152
the optimizer parameters for the ansatz, the estimated value,
149
153
the estimation metadata, and the current step.
154
+ convergence_threshold: A threshold under which the algorithm is considered to have
155
+ converged. It corresponds to the maximal average fidelity an eigenstate is allowed
156
+ to have with the previous eigenstates. If set to None, no check is performed.
150
157
"""
151
158
super ().__init__ ()
152
159
@@ -159,6 +166,7 @@ def __init__(
159
166
# this has to go via getters and setters due to the VariationalAlgorithm interface
160
167
self .initial_point = initial_point
161
168
self .callback = callback
169
+ self .convergence_threshold = convergence_threshold
162
170
163
171
self ._eval_count = 0
164
172
@@ -267,16 +275,24 @@ def compute_eigenvalues(
267
275
self .initial_point , self .ansatz # type: ignore[arg-type]
268
276
)
269
277
278
+ current_optimal_point : dict [str , Any ] = {"optimal_value" : float ("inf" )}
279
+
270
280
for step in range (1 , self .k + 1 ):
281
+ current_optimal_point ["optimal_value" ] = float ("inf" )
282
+
271
283
if num_initial_points > 1 :
272
284
initial_point = validate_initial_point (initial_points [step - 1 ], self .ansatz )
273
285
274
286
if step > 1 :
275
- prev_states .append (self .ansatz .assign_parameters (result . optimal_points [ - 1 ]))
287
+ prev_states .append (self .ansatz .assign_parameters (current_optimal_point [ "x" ]))
276
288
277
289
self ._eval_count = 0
278
290
energy_evaluation = self ._get_evaluate_energy (
279
- step , operator , betas , prev_states = prev_states
291
+ step ,
292
+ operator ,
293
+ betas ,
294
+ prev_states = prev_states ,
295
+ current_optimal_point = current_optimal_point ,
280
296
)
281
297
282
298
start_time = time ()
@@ -309,11 +325,13 @@ def compute_eigenvalues(
309
325
310
326
eval_time = time () - start_time
311
327
312
- self ._update_vqd_result (result , opt_result , eval_time , self .ansatz .copy ())
328
+ self ._update_vqd_result (
329
+ result , opt_result , eval_time , self .ansatz .copy (), current_optimal_point
330
+ )
313
331
314
332
if aux_operators is not None :
315
333
aux_value = estimate_observables (
316
- self .estimator , self .ansatz , aux_operators , result . optimal_points [ - 1 ]
334
+ self .estimator , self .ansatz , aux_operators , current_optimal_point [ "x" ]
317
335
)
318
336
aux_values .append (aux_value )
319
337
@@ -326,6 +344,29 @@ def compute_eigenvalues(
326
344
self ._eval_count ,
327
345
)
328
346
else :
347
+ average_fidelity = current_optimal_point ["total_fidelity" ][0 ] / (step - 1 )
348
+
349
+ if (
350
+ self .convergence_threshold is not None
351
+ and average_fidelity > self .convergence_threshold
352
+ ):
353
+ last_digit = step % 10
354
+
355
+ if last_digit == 1 and step % 100 != 11 :
356
+ suffix = "st"
357
+ elif last_digit == 2 :
358
+ suffix = "nd"
359
+ elif last_digit == 3 :
360
+ suffix = "rd"
361
+ else :
362
+ suffix = "th"
363
+
364
+ raise AlgorithmError (
365
+ f"Convergence threshold is set to { self .convergence_threshold } but an "
366
+ f"average fidelity of { average_fidelity :.5f} with the previous eigenstates"
367
+ f"has been observed during the evaluation of the { step } { suffix } lowest"
368
+ f"eigenvalue."
369
+ )
329
370
logger .info (
330
371
(
331
372
"%s excited state optimization complete in %s s.\n "
@@ -345,21 +386,24 @@ def compute_eigenvalues(
345
386
346
387
return result
347
388
348
- def _get_evaluate_energy (
389
+ def _get_evaluate_energy ( # pylint: disable=too-many-positional-arguments
349
390
self ,
350
391
step : int ,
351
392
operator : BaseOperator ,
352
393
betas : np .ndarray ,
394
+ current_optimal_point : dict ["str" , Any ],
353
395
prev_states : list [QuantumCircuit ] | None = None ,
354
396
) -> Callable [[np .ndarray ], float | np .ndarray ]:
355
397
"""Returns a function handle to evaluate the ansatz's energy for any given parameters.
356
398
This is the objective function to be passed to the optimizer that is used for evaluation.
357
399
358
400
Args:
359
- step: level of energy being calculated. 0 for ground, 1 for first excited state...
401
+ step: level of energy being calculated. 1 for ground, 2 for first excited state...
360
402
operator: The operator whose energy to evaluate.
361
403
betas: Beta parameters in the VQD paper.
362
404
prev_states: List of optimal circuits from previous rounds of optimization.
405
+ current_optimal_point: A dict to keep track of the current optimal point, which is used
406
+ to check the algorithm's convergence.
363
407
364
408
Returns:
365
409
A callable that computes and returns the energy of the hamiltonian
@@ -425,6 +469,17 @@ def evaluate_energy(parameters: np.ndarray) -> float | np.ndarray:
425
469
else :
426
470
self ._eval_count += len (values )
427
471
472
+ for param , value in zip (parameters , values ):
473
+ if value < current_optimal_point ["optimal_value" ]:
474
+ current_optimal_point ["optimal_value" ] = value
475
+ current_optimal_point ["x" ] = param
476
+
477
+ if step > 1 :
478
+ current_optimal_point ["total_fidelity" ] = total_cost
479
+ current_optimal_point ["eigenvalue" ] = (value - total_cost )[0 ]
480
+ else :
481
+ current_optimal_point ["eigenvalue" ] = value
482
+
428
483
return values if len (values ) > 1 else values [0 ]
429
484
430
485
return evaluate_energy
@@ -444,20 +499,22 @@ def _build_vqd_result() -> VQDResult:
444
499
445
500
@staticmethod
446
501
def _update_vqd_result (
447
- result : VQDResult , opt_result : OptimizerResult , eval_time , ansatz
502
+ result : VQDResult , opt_result : OptimizerResult , eval_time , ansatz , optimal_point
448
503
) -> VQDResult :
449
504
result .optimal_points = (
450
- np .concatenate ([result .optimal_points , [opt_result . x ]])
505
+ np .concatenate ([result .optimal_points , [optimal_point [ "x" ] ]])
451
506
if len (result .optimal_points ) > 0
452
- else np .array ([opt_result . x ])
507
+ else np .array ([optimal_point [ "x" ] ])
453
508
)
454
509
result .optimal_parameters .append (
455
- dict (zip (ansatz .parameters , cast (np .ndarray , opt_result .x )))
510
+ dict (zip (ansatz .parameters , cast (np .ndarray , optimal_point ["x" ])))
511
+ )
512
+ result .optimal_values = np .concatenate (
513
+ [result .optimal_values , [optimal_point ["optimal_value" ]]]
456
514
)
457
- result .optimal_values = np .concatenate ([result .optimal_values , [opt_result .fun ]])
458
515
result .cost_function_evals = np .concatenate ([result .cost_function_evals , [opt_result .nfev ]])
459
516
result .optimizer_times = np .concatenate ([result .optimizer_times , [eval_time ]])
460
- result .eigenvalues .append (opt_result . fun + 0j ) # type: ignore[attr-defined]
517
+ result .eigenvalues .append (optimal_point [ "eigenvalue" ] + 0j ) # type: ignore[attr-defined]
461
518
result .optimizer_results .append (opt_result )
462
519
result .optimal_circuits .append (ansatz )
463
520
return result
0 commit comments