excitationsolve 2.2.1__tar.gz → 2.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {excitationsolve-2.2.1/excitationsolve.egg-info → excitationsolve-2.2.3}/PKG-INFO +38 -4
  2. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/README.md +37 -3
  3. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/excitation_solve_scipy.py +30 -1
  4. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/info.py +1 -1
  5. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/parameter_prediction.py +8 -12
  6. {excitationsolve-2.2.1 → excitationsolve-2.2.3/excitationsolve.egg-info}/PKG-INFO +38 -4
  7. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve.egg-info/SOURCES.txt +2 -1
  8. excitationsolve-2.2.3/tests/test_callback.py +266 -0
  9. excitationsolve-2.2.1/tests/tcc_tests.py → excitationsolve-2.2.3/tests/test_tcc.py +6 -6
  10. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/.gitignore +0 -0
  11. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/CITATION.bib +0 -0
  12. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/LICENSE.txt +0 -0
  13. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/assets/convergence_plots.png +0 -0
  14. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/examples/main_pennylane.py +0 -0
  15. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/examples/main_pennylane_pyscf.py +0 -0
  16. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/examples/main_qiskit.py +0 -0
  17. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/__init__.py +0 -0
  18. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/excitation_solve.py +0 -0
  19. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/excitation_solve_2d.py +0 -0
  20. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/excitation_solve_adapt.py +0 -0
  21. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/excitation_solve_pennylane.py +0 -0
  22. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/excitation_solve_qiskit.py +0 -0
  23. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve/trig_poly_utils.py +0 -0
  24. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve.egg-info/dependency_links.txt +0 -0
  25. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve.egg-info/requires.txt +0 -0
  26. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/excitationsolve.egg-info/top_level.txt +0 -0
  27. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/pyproject.toml +0 -0
  28. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/setup.cfg +0 -0
  29. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/tests/pyscf_molecules.py +0 -0
  30. {excitationsolve-2.2.1 → excitationsolve-2.2.3}/tests/tcc_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excitationsolve
3
- Version: 2.2.1
3
+ Version: 2.2.3
4
4
  Summary: ExcitationSolve: a fast globally-informed gradient-free optimizer for physically-motivated ansätze constructed of excitation operators
5
5
  Author-email: Jonas Jäger <jojaeger@cs.ubc.ca>, "Thierry N. Kaldenbach" <Thierry.Kaldenbach@dlr.de>, Max Haas <Max.Haas@dlr.de>, Erik Schultheis <Erik.Schultheis@dlr.de>
6
6
  License: Copyright 2025 Jonas Jäger, Thierry N. Kaldenbach, Max Haas, Erik Schultheis,
@@ -241,7 +241,7 @@ Requires-Dist: tencirchem-ng==2024.10; extra == "test"
241
241
  Requires-Dist: pyscf==2.8.0; extra == "test"
242
242
  Dynamic: license-file
243
243
 
244
- # ExcitationSolve [![paper](https://img.shields.io/badge/Commun.%20Phys.-Paper-0274b5.svg)](https://www.nature.com/articles/s42005-025-02375-9) [![DOI](https://zenodo.org/badge/958004975.svg)](https://doi.org/10.5281/zenodo.17457122)
244
+ # ExcitationSolve [![paper](https://img.shields.io/badge/Commun.%20Phys.-Paper-0274b5.svg)](https://www.nature.com/articles/s42005-025-02375-9) [![DOI](https://zenodo.org/badge/958004975.svg)](https://doi.org/10.5281/zenodo.17457122) [![PyPI](https://img.shields.io/pypi/v/excitationsolve.svg?color=0274b5&label=PyPI)](https://pypi.org/project/excitationsolve/)
245
245
 
246
246
  An optimization algorithm for ansätze consisting of excitation operators in variational quantum eigensolvers (VQEs).
247
247
 
@@ -257,6 +257,11 @@ VQE convergence for different molecules and optimizers, including ExcitationSolv
257
257
  </h1>
258
258
 
259
259
  ## Installation
260
+ ExcitationSolve is available on [PyPI](https://pypi.org/project/excitationsolve/):
261
+ ```
262
+ pip install excitationsolve
263
+ ```
264
+ Alternatively, install directly from GitHub:
260
265
  ```
261
266
  pip install git+https://github.com/dlr-wf/ExcitationSolve.git
262
267
  ```
@@ -287,11 +292,27 @@ Use the `excitationsolve.ExcitationSolveScipy` class in combination with the `sc
287
292
  ```python
288
293
  excsolve_obj = ExcitationSolveScipy(maxiter=100, tol=1e-10, save_parameters=True)
289
294
  optimizer = excsolve_obj.minimize
290
- res = scipy.optimize.minimize(cost, params, method=optimizer)
295
+ def callback(xk): # or callback(intermediate_result)
296
+ print(xk)
297
+ options = dict(parameter_occ=parameter_occ) # optional, use if parameters occur multiple times in the ansatz
298
+ res = scipy.optimize.minimize(cost, params, method=optimizer, callback=callback, options=options)
291
299
  energies = excsolve_obj.energies
292
300
  counts = excsolve_obj.nfevs
293
301
  ```
294
302
 
303
+ ### Optimal parameters of double excitations w.r.t. the Hartree-Fock state
304
+ Use `exitationsolve.optimal_theta` and `excitationsolve.optimal_theta_pyscf` to calculate analytic optimal parameter values for individual double excitations when applied to the Hartree-Fock reference state. Both functions return the optimal parameter and the energy difference to the Hartree-Fock energy. The optimal parameter is defined as $\theta^*=\argmin_\theta\bra{\mathrm{HF}}U^\dagger(\theta) H U(\theta)\ket{\mathrm{HF}}$ where $H$ is the electronic structure Hamiltonian, $U(\theta)$ is **one** double excitation, and $\ket{\mathrm{HF}}$ is the Hartree-Fock state. In [Haas et al., 2026](https://doi.org/10.48550/arXiv.2602.10776) it has been shown that initializing doubles in their respective optimal parameter and appending them to the ansatz in order of energy impact is an efficient operator selection and warm-start strategy.
305
+
306
+ ### Testing
307
+ To run the tests, install the needed dependencies with
308
+ ```bash
309
+ uv pip install .[test]
310
+ ```
311
+ and execute
312
+ ```bash
313
+ pytest
314
+ ```
315
+
295
316
 
296
317
  ## Authors
297
318
  - Jonas Jäger
@@ -305,7 +326,7 @@ Feel free to contact [David Melching](mailto:David.Melching@dlr.de) if you have
305
326
  ## Citation
306
327
  If you use portions of this code please cite our [paper](https://www.nature.com/articles/s42005-025-02375-9):
307
328
  ```bibtex
308
- @article{Jaeger2024Fast,
329
+ @article{Jaeger2025Fast,
309
330
  title={Fast gradient-free optimization of excitations in variational quantum eigensolvers},
310
331
  author={Jäger, Jonas and Kaldenbach, Thierry N. and Haas, Max and Schultheis, Erik},
311
332
  year={2025},
@@ -319,5 +340,18 @@ If you use portions of this code please cite our [paper](https://www.nature.com/
319
340
  }
320
341
  ```
321
342
 
343
+ If you use the `exitationsolve.optimal_theta` or `excitationsolve.optimal_theta_pyscf` functionalities please also cite [Haas et al., 2026](https://doi.org/10.48550/arXiv.2602.10776):
344
+ ```bibtex
345
+ @article{Haas2026Efficient,
346
+ title={Efficient Operator Selection and Warm-Start Strategy for Excitations in Variational Quantum Eigensolvers},
347
+ url={https://arxiv.org/abs/2602.10776},
348
+ DOI={10.48550/ARXIV.2602.10776},
349
+ publisher={arXiv},
350
+ author={Haas, Max and Kaldenbach, Thierry N. and Hammerschmidt, Thomas and Barragan-Yani, Daniel},
351
+ year={2026}
352
+ }
353
+ ```
354
+
355
+
322
356
  ## Acknowledgment
323
357
  This project was made possible by the DLR Quantum Computing Initiative and the Federal Ministry for Economic Affairs and Climate Action; https://qci.dlr.de/quanticom.
@@ -1,4 +1,4 @@
1
- # ExcitationSolve [![paper](https://img.shields.io/badge/Commun.%20Phys.-Paper-0274b5.svg)](https://www.nature.com/articles/s42005-025-02375-9) [![DOI](https://zenodo.org/badge/958004975.svg)](https://doi.org/10.5281/zenodo.17457122)
1
+ # ExcitationSolve [![paper](https://img.shields.io/badge/Commun.%20Phys.-Paper-0274b5.svg)](https://www.nature.com/articles/s42005-025-02375-9) [![DOI](https://zenodo.org/badge/958004975.svg)](https://doi.org/10.5281/zenodo.17457122) [![PyPI](https://img.shields.io/pypi/v/excitationsolve.svg?color=0274b5&label=PyPI)](https://pypi.org/project/excitationsolve/)
2
2
 
3
3
  An optimization algorithm for ansätze consisting of excitation operators in variational quantum eigensolvers (VQEs).
4
4
 
@@ -14,6 +14,11 @@ VQE convergence for different molecules and optimizers, including ExcitationSolv
14
14
  </h1>
15
15
 
16
16
  ## Installation
17
+ ExcitationSolve is available on [PyPI](https://pypi.org/project/excitationsolve/):
18
+ ```
19
+ pip install excitationsolve
20
+ ```
21
+ Alternatively, install directly from GitHub:
17
22
  ```
18
23
  pip install git+https://github.com/dlr-wf/ExcitationSolve.git
19
24
  ```
@@ -44,11 +49,27 @@ Use the `excitationsolve.ExcitationSolveScipy` class in combination with the `sc
44
49
  ```python
45
50
  excsolve_obj = ExcitationSolveScipy(maxiter=100, tol=1e-10, save_parameters=True)
46
51
  optimizer = excsolve_obj.minimize
47
- res = scipy.optimize.minimize(cost, params, method=optimizer)
52
+ def callback(xk): # or callback(intermediate_result)
53
+ print(xk)
54
+ options = dict(parameter_occ=parameter_occ) # optional, use if parameters occur multiple times in the ansatz
55
+ res = scipy.optimize.minimize(cost, params, method=optimizer, callback=callback, options=options)
48
56
  energies = excsolve_obj.energies
49
57
  counts = excsolve_obj.nfevs
50
58
  ```
51
59
 
60
+ ### Optimal parameters of double excitations w.r.t. the Hartree-Fock state
61
+ Use `exitationsolve.optimal_theta` and `excitationsolve.optimal_theta_pyscf` to calculate analytic optimal parameter values for individual double excitations when applied to the Hartree-Fock reference state. Both functions return the optimal parameter and the energy difference to the Hartree-Fock energy. The optimal parameter is defined as $\theta^*=\argmin_\theta\bra{\mathrm{HF}}U^\dagger(\theta) H U(\theta)\ket{\mathrm{HF}}$ where $H$ is the electronic structure Hamiltonian, $U(\theta)$ is **one** double excitation, and $\ket{\mathrm{HF}}$ is the Hartree-Fock state. In [Haas et al., 2026](https://doi.org/10.48550/arXiv.2602.10776) it has been shown that initializing doubles in their respective optimal parameter and appending them to the ansatz in order of energy impact is an efficient operator selection and warm-start strategy.
62
+
63
+ ### Testing
64
+ To run the tests, install the needed dependencies with
65
+ ```bash
66
+ uv pip install .[test]
67
+ ```
68
+ and execute
69
+ ```bash
70
+ pytest
71
+ ```
72
+
52
73
 
53
74
  ## Authors
54
75
  - Jonas Jäger
@@ -62,7 +83,7 @@ Feel free to contact [David Melching](mailto:David.Melching@dlr.de) if you have
62
83
  ## Citation
63
84
  If you use portions of this code please cite our [paper](https://www.nature.com/articles/s42005-025-02375-9):
64
85
  ```bibtex
65
- @article{Jaeger2024Fast,
86
+ @article{Jaeger2025Fast,
66
87
  title={Fast gradient-free optimization of excitations in variational quantum eigensolvers},
67
88
  author={Jäger, Jonas and Kaldenbach, Thierry N. and Haas, Max and Schultheis, Erik},
68
89
  year={2025},
@@ -76,5 +97,18 @@ If you use portions of this code please cite our [paper](https://www.nature.com/
76
97
  }
77
98
  ```
78
99
 
100
+ If you use the `exitationsolve.optimal_theta` or `excitationsolve.optimal_theta_pyscf` functionalities please also cite [Haas et al., 2026](https://doi.org/10.48550/arXiv.2602.10776):
101
+ ```bibtex
102
+ @article{Haas2026Efficient,
103
+ title={Efficient Operator Selection and Warm-Start Strategy for Excitations in Variational Quantum Eigensolvers},
104
+ url={https://arxiv.org/abs/2602.10776},
105
+ DOI={10.48550/ARXIV.2602.10776},
106
+ publisher={arXiv},
107
+ author={Haas, Max and Kaldenbach, Thierry N. and Hammerschmidt, Thomas and Barragan-Yani, Daniel},
108
+ year={2026}
109
+ }
110
+ ```
111
+
112
+
79
113
  ## Acknowledgment
80
114
  This project was made possible by the DLR Quantum Computing Initiative and the Federal Ministry for Economic Affairs and Climate Action; https://qci.dlr.de/quanticom.
@@ -3,6 +3,8 @@ import logging
3
3
  from collections.abc import Callable
4
4
  import numpy as np
5
5
  from scipy.optimize import OptimizeResult
6
+ from scipy.optimize._optimize import _wrap_callback
7
+ from scipy._lib._util import _call_callback_maybe_halt
6
8
  from excitationsolve import excitation_solve_step, excitation_solve_step_shared_param
7
9
 
8
10
 
@@ -14,7 +16,9 @@ class ExcitationSolveScipy:
14
16
  ```python
15
17
  excsolve_obj = ExcitationSolveScipy(maxiter=100, tol=1e-10, save_parameters=True)
16
18
  optimizer = excsolve_obj.minimize
17
- res = scipy.optimize.minimize(cost, params, method=optimizer)
19
+ def callback(xk): # or callback(intermediate_result)
20
+ print(xk)
21
+ res = scipy.optimize.minimize(cost, params, method=optimizer, callback=callback, options=dict(parameter_occ=parameter_occ))
18
22
  energies = excsolve_obj.energies
19
23
  counts = excsolve_obj.nfevs
20
24
  ```
@@ -71,10 +75,30 @@ class ExcitationSolveScipy:
71
75
  how many excitations share each parameter.
72
76
  parameter_occ[i] is the number of times the i-th parameter occurs in different excitations.
73
77
  Defaults to [1, 1, ...] (all set to 1).
78
+ callback (callable, optional): A callable called after each iteration. Supports a callable with the signature::
79
+
80
+ callback(intermediate_result: OptimizeResult)
81
+
82
+ where ``intermediate_result`` is a keyword parameter containing an
83
+ `OptimizeResult` with attributes ``x`` and ``fun``, the present values
84
+ of the parameter vector and objective function. Not all attributes of
85
+ `OptimizeResult` may be present. The name of the parameter must be
86
+ ``intermediate_result`` for the callback to be passed an `OptimizeResult`.
87
+ These methods will also terminate if the callback raises ``StopIteration``.
88
+ Also supports a signature like::
89
+
90
+ callback(xk)
91
+
92
+ where ``xk`` is the current parameter vector.
93
+ Introspection is used to determine which of the signatures above to
94
+ invoke.
74
95
 
75
96
  Returns:
76
97
  OptimizeResult: Scipy OptimizeResult object
77
98
  """
99
+ callback = kwargs.get("callback")
100
+ callback = _wrap_callback(callback)
101
+
78
102
  params_excsolve = np.array(x0.copy())
79
103
  num_params = len(params_excsolve)
80
104
 
@@ -155,6 +179,11 @@ class ExcitationSolveScipy:
155
179
  if len(self.energies_after_it) > 1 and np.abs(self.energies_after_it[-1] - self.energies_after_it[-2]) <= self.tol:
156
180
  break
157
181
 
182
+ if callback is not None:
183
+ intermediate_result = OptimizeResult(x=np.copy(params_excsolve), fun=current_energy_excsolve)
184
+ if _call_callback_maybe_halt(callback, intermediate_result):
185
+ break
186
+
158
187
  result = OptimizeResult()
159
188
  result.x = params_excsolve
160
189
  result.fun = current_energy_excsolve
@@ -1,3 +1,3 @@
1
- __version__ = "2.2.1" # MAJOR.MINOR.PATCH, like in https://semver.org/
1
+ __version__ = "2.2.3" # MAJOR.MINOR.PATCH, like in https://semver.org/
2
2
 
3
3
  HOMEPAGE = "https://github.com/dlr-wf/ExcitationSolve" # GitHub page
@@ -8,8 +8,7 @@ period-pi curve
8
8
 
9
9
  with a = (E_HF - E_exc) / 2 and b = <HF|H|Phi>. The minimiser of this curve has
10
10
  the closed form theta = -1/2 * arctan2(-b, -a). `optimal_theta` evaluates a
11
- and b from the molecular integrals and returns this exact angle together with
12
- its first-order (EN2 / MP2) linearisations.
11
+ and b from the molecular integrals and returns this exact angle.
13
12
 
14
13
  The module is self-contained: it only needs a converged PySCF RHF object and the
15
14
  four excitation indices (in TCC/Qiskit-sorted spin-orbital order).
@@ -47,7 +46,7 @@ def _transform_integrals_to_mo(mf):
47
46
  return h1_mo, eri_mo, occ_indices
48
47
 
49
48
 
50
- def _determinant_energy(h1_mo, eri_mo, occ_indices, MP2=False):
49
+ def _determinant_energy(h1_mo, eri_mo, occ_indices):
51
50
  """
52
51
  Compute the energy of a single Slater determinant in MO basis.
53
52
 
@@ -83,10 +82,7 @@ def _determinant_energy(h1_mo, eri_mo, occ_indices, MP2=False):
83
82
  for q in occ:
84
83
  e_two += eri_mo[p, q, q, p] - eri_mo[p, q, p, q]
85
84
 
86
- if MP2:
87
- return e_one
88
- else:
89
- return e_one + 0.5 * e_two
85
+ return e_one + 0.5 * e_two
90
86
 
91
87
 
92
88
  def _apply_double_excitation(occ_indices, i, j, a, b):
@@ -116,7 +112,7 @@ def _apply_double_excitation(occ_indices, i, j, a, b):
116
112
  return sorted(occ_set)
117
113
 
118
114
 
119
- def _compute_a_b(h1_mo, h2_mo, occ_indices, i, j, k, l, MP2=False):
115
+ def _compute_a_b(h1_mo, h2_mo, occ_indices, i, j, k, l):
120
116
  """
121
117
  Compute the parameters a and b for the two-level effective Hamiltonian
122
118
  defined by the HF determinant and a doubly excited determinant.
@@ -153,11 +149,11 @@ def _compute_a_b(h1_mo, h2_mo, occ_indices, i, j, k, l, MP2=False):
153
149
  b : float
154
150
  """
155
151
  # HF determinant energy
156
- e_hf = _determinant_energy(h1_mo, h2_mo, occ_indices, MP2)
152
+ e_hf = _determinant_energy(h1_mo, h2_mo, occ_indices)
157
153
 
158
154
  # Excited determinant: remove (i, j), add (k, l)
159
155
  occ_exc = _apply_double_excitation(occ_indices, k, l, i, j)
160
- e_exc = _determinant_energy(h1_mo, h2_mo, occ_exc, MP2)
156
+ e_exc = _determinant_energy(h1_mo, h2_mo, occ_exc)
161
157
 
162
158
  a_val = 0.5 * (e_hf - e_exc)
163
159
 
@@ -254,7 +250,7 @@ def _block_to_interleaved(idx, no, nv):
254
250
  return 2 * spatial + spin
255
251
 
256
252
 
257
- def optimal_theta_pyscf(mf: pyscf.scf.hf.RHF, excitation_indices: list[int]):
253
+ def optimal_theta_pyscf(mf: pyscf.scf.hf.RHF, excitation_indices: list[int]) -> tuple[float, float]:
258
254
  """
259
255
  High-level convenience function to compute the optimal VQE angle theta_opt
260
256
  for a given double excitation in an RHF reference.
@@ -283,7 +279,7 @@ def optimal_theta_pyscf(mf: pyscf.scf.hf.RHF, excitation_indices: list[int]):
283
279
  return optimal_theta(h1_mo, eri_mo, occ_spatial, excitation_indices)
284
280
 
285
281
 
286
- def optimal_theta(h1: np.ndarray, eri: np.ndarray, occ_spatial: list[int], excitation_indices: list[int]):
282
+ def optimal_theta(h1: np.ndarray, eri: np.ndarray, occ_spatial: list[int], excitation_indices: list[int]) -> tuple[float, float]:
287
283
  """
288
284
  High-level convenience function to compute the optimal VQE angle theta_opt
289
285
  for a given double excitation in an RHF reference.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excitationsolve
3
- Version: 2.2.1
3
+ Version: 2.2.3
4
4
  Summary: ExcitationSolve: a fast globally-informed gradient-free optimizer for physically-motivated ansätze constructed of excitation operators
5
5
  Author-email: Jonas Jäger <jojaeger@cs.ubc.ca>, "Thierry N. Kaldenbach" <Thierry.Kaldenbach@dlr.de>, Max Haas <Max.Haas@dlr.de>, Erik Schultheis <Erik.Schultheis@dlr.de>
6
6
  License: Copyright 2025 Jonas Jäger, Thierry N. Kaldenbach, Max Haas, Erik Schultheis,
@@ -241,7 +241,7 @@ Requires-Dist: tencirchem-ng==2024.10; extra == "test"
241
241
  Requires-Dist: pyscf==2.8.0; extra == "test"
242
242
  Dynamic: license-file
243
243
 
244
- # ExcitationSolve [![paper](https://img.shields.io/badge/Commun.%20Phys.-Paper-0274b5.svg)](https://www.nature.com/articles/s42005-025-02375-9) [![DOI](https://zenodo.org/badge/958004975.svg)](https://doi.org/10.5281/zenodo.17457122)
244
+ # ExcitationSolve [![paper](https://img.shields.io/badge/Commun.%20Phys.-Paper-0274b5.svg)](https://www.nature.com/articles/s42005-025-02375-9) [![DOI](https://zenodo.org/badge/958004975.svg)](https://doi.org/10.5281/zenodo.17457122) [![PyPI](https://img.shields.io/pypi/v/excitationsolve.svg?color=0274b5&label=PyPI)](https://pypi.org/project/excitationsolve/)
245
245
 
246
246
  An optimization algorithm for ansätze consisting of excitation operators in variational quantum eigensolvers (VQEs).
247
247
 
@@ -257,6 +257,11 @@ VQE convergence for different molecules and optimizers, including ExcitationSolv
257
257
  </h1>
258
258
 
259
259
  ## Installation
260
+ ExcitationSolve is available on [PyPI](https://pypi.org/project/excitationsolve/):
261
+ ```
262
+ pip install excitationsolve
263
+ ```
264
+ Alternatively, install directly from GitHub:
260
265
  ```
261
266
  pip install git+https://github.com/dlr-wf/ExcitationSolve.git
262
267
  ```
@@ -287,11 +292,27 @@ Use the `excitationsolve.ExcitationSolveScipy` class in combination with the `sc
287
292
  ```python
288
293
  excsolve_obj = ExcitationSolveScipy(maxiter=100, tol=1e-10, save_parameters=True)
289
294
  optimizer = excsolve_obj.minimize
290
- res = scipy.optimize.minimize(cost, params, method=optimizer)
295
+ def callback(xk): # or callback(intermediate_result)
296
+ print(xk)
297
+ options = dict(parameter_occ=parameter_occ) # optional, use if parameters occur multiple times in the ansatz
298
+ res = scipy.optimize.minimize(cost, params, method=optimizer, callback=callback, options=options)
291
299
  energies = excsolve_obj.energies
292
300
  counts = excsolve_obj.nfevs
293
301
  ```
294
302
 
303
+ ### Optimal parameters of double excitations w.r.t. the Hartree-Fock state
304
+ Use `exitationsolve.optimal_theta` and `excitationsolve.optimal_theta_pyscf` to calculate analytic optimal parameter values for individual double excitations when applied to the Hartree-Fock reference state. Both functions return the optimal parameter and the energy difference to the Hartree-Fock energy. The optimal parameter is defined as $\theta^*=\argmin_\theta\bra{\mathrm{HF}}U^\dagger(\theta) H U(\theta)\ket{\mathrm{HF}}$ where $H$ is the electronic structure Hamiltonian, $U(\theta)$ is **one** double excitation, and $\ket{\mathrm{HF}}$ is the Hartree-Fock state. In [Haas et al., 2026](https://doi.org/10.48550/arXiv.2602.10776) it has been shown that initializing doubles in their respective optimal parameter and appending them to the ansatz in order of energy impact is an efficient operator selection and warm-start strategy.
305
+
306
+ ### Testing
307
+ To run the tests, install the needed dependencies with
308
+ ```bash
309
+ uv pip install .[test]
310
+ ```
311
+ and execute
312
+ ```bash
313
+ pytest
314
+ ```
315
+
295
316
 
296
317
  ## Authors
297
318
  - Jonas Jäger
@@ -305,7 +326,7 @@ Feel free to contact [David Melching](mailto:David.Melching@dlr.de) if you have
305
326
  ## Citation
306
327
  If you use portions of this code please cite our [paper](https://www.nature.com/articles/s42005-025-02375-9):
307
328
  ```bibtex
308
- @article{Jaeger2024Fast,
329
+ @article{Jaeger2025Fast,
309
330
  title={Fast gradient-free optimization of excitations in variational quantum eigensolvers},
310
331
  author={Jäger, Jonas and Kaldenbach, Thierry N. and Haas, Max and Schultheis, Erik},
311
332
  year={2025},
@@ -319,5 +340,18 @@ If you use portions of this code please cite our [paper](https://www.nature.com/
319
340
  }
320
341
  ```
321
342
 
343
+ If you use the `exitationsolve.optimal_theta` or `excitationsolve.optimal_theta_pyscf` functionalities please also cite [Haas et al., 2026](https://doi.org/10.48550/arXiv.2602.10776):
344
+ ```bibtex
345
+ @article{Haas2026Efficient,
346
+ title={Efficient Operator Selection and Warm-Start Strategy for Excitations in Variational Quantum Eigensolvers},
347
+ url={https://arxiv.org/abs/2602.10776},
348
+ DOI={10.48550/ARXIV.2602.10776},
349
+ publisher={arXiv},
350
+ author={Haas, Max and Kaldenbach, Thierry N. and Hammerschmidt, Thomas and Barragan-Yani, Daniel},
351
+ year={2026}
352
+ }
353
+ ```
354
+
355
+
322
356
  ## Acknowledgment
323
357
  This project was made possible by the DLR Quantum Computing Initiative and the Federal Ministry for Economic Affairs and Climate Action; https://qci.dlr.de/quanticom.
@@ -24,4 +24,5 @@ excitationsolve.egg-info/requires.txt
24
24
  excitationsolve.egg-info/top_level.txt
25
25
  tests/pyscf_molecules.py
26
26
  tests/tcc_helpers.py
27
- tests/tcc_tests.py
27
+ tests/test_callback.py
28
+ tests/test_tcc.py
@@ -0,0 +1,266 @@
1
+ import time
2
+ import numpy as np
3
+ import scipy
4
+ import matplotlib.pyplot as plt
5
+ from pyscf import scf, gto, fci, ao2mo
6
+ from tencirchem import UCCSD
7
+ import tcc_helpers
8
+ import pyscf_molecules
9
+ from excitationsolve import ExcitationSolveScipy
10
+ import logging
11
+
12
+
13
+ def test_tcc_h3plus_callback_xk():
14
+ logging.basicConfig(level=logging.INFO)
15
+
16
+ # molecule = pyscf_molecules.H_chain.build_hydrogen_chain(2)
17
+ molecule = pyscf_molecules.H3plus
18
+ symbols = molecule.symbols
19
+ basis = molecule.basis
20
+ geometry = molecule.geometry
21
+ charge = molecule.charge
22
+
23
+ atom = "; ".join([f"{a} {', '.join([str(x) for x in p.tolist()])}" for a, p in zip(symbols, geometry)])
24
+ symbols_unique, unique_counts = np.unique(symbols, return_counts=True)
25
+ molname = "".join([f"{a}{n}" for a, n in zip(symbols_unique, unique_counts)])
26
+ if charge > 0:
27
+ molname += "+" * np.abs(charge)
28
+ elif charge < 0:
29
+ molname += "-" * np.abs(charge)
30
+
31
+ unit = "Angstrom" # Angstrom or Bohr
32
+ mol_pyscf = gto.M(atom=atom, basis=basis, charge=charge, unit=unit)
33
+ _electrons = mol_pyscf.nelectron
34
+ rhf = scf.RHF(mol_pyscf)
35
+ _hf_energy = rhf.kernel()
36
+
37
+ _mo_occ = rhf.mo_occ
38
+
39
+ print(f"Building TCC Hamiltonian for {molname} (early build to use TCC canonicalized MO-coefficients) ...")
40
+ tcc_uccsd = UCCSD(rhf, init_method="zeros", run_hf=False, run_mp2=False, run_ccsd=False, run_fci=True) # TCC params = -params
41
+ print(f"{tcc_uccsd.engine=}")
42
+ rhf.mo_coeff = tcc_uccsd.hf.mo_coeff
43
+
44
+ fci_calc = fci.FCI(mol_pyscf, rhf.mo_coeff)
45
+ fci_energy, ci_vector = fci_calc.kernel()
46
+ print(f"FCI Energy: {fci_energy} Ha")
47
+
48
+ reference_energy = fci_energy
49
+
50
+ # one_ao = mol_pyscf.intor_symmetric("int1e_kin") + mol_pyscf.intor_symmetric("int1e_nuc")
51
+ # two_ao = mol_pyscf.intor("int2e_sph")
52
+ # one_mo = np.einsum("pi,pq,qj->ij", rhf.mo_coeff, one_ao, rhf.mo_coeff) * 1.0
53
+ # two_mo = ao2mo.incore.full(two_ao, rhf.mo_coeff) * 1.0
54
+ # two_mo = two_mo.swapaxes(1, 2).swapaxes(2, 3) # to physicist order
55
+ # core_constant = rhf.energy_nuc()
56
+
57
+ nelec = mol_pyscf.nelec
58
+ norb = mol_pyscf.nao
59
+
60
+ # energy_offset = core_constant
61
+
62
+ #####################################################################################
63
+ ## Define Ansatz ##
64
+ #####################################################################################
65
+ singles_pool = tcc_helpers.get_ex1_ops(norb, nelec)
66
+ doubles_pool = tcc_helpers.get_ex2_ops(norb, nelec)
67
+ singles_pool_sorted = sorted(singles_pool)
68
+ doubles_pool_sorted = sorted([(*sorted(x[:2]), *sorted(x[2:])) for x in doubles_pool])
69
+ # complete_pool = doubles_pool_sorted + singles_pool_sorted
70
+ complete_pool = singles_pool_sorted + doubles_pool_sorted
71
+
72
+ tcc_uccsd.ex_ops = complete_pool
73
+ # tcc_uccsd.param_ids = None
74
+ # tcc_uccsd.param_ids = [0, 0, 1]
75
+ print(f"{tcc_uccsd.param_ids=}")
76
+
77
+ n_params = len(complete_pool)
78
+ _params = np.zeros(n_params)
79
+ _ex_ops = [(x[: len(x) // 2], x[len(x) // 2 :]) for x in complete_pool]
80
+
81
+ #######################
82
+ # Manual optimization
83
+ times_tcc = []
84
+ eval_count_tcc = 0
85
+ counts_tcc = []
86
+ values_tcc = []
87
+ tcc_vqe_params_lst = []
88
+
89
+ def cost(x):
90
+ nonlocal times_tcc
91
+ nonlocal eval_count_tcc
92
+ nonlocal counts_tcc
93
+ nonlocal values_tcc
94
+
95
+ tcc_energy = tcc_uccsd.energy(x)
96
+ times_tcc.append(time.perf_counter())
97
+
98
+ eval_count_tcc += 1
99
+ print(
100
+ f"Optimizer evaluation #{eval_count_tcc}, Diff. to ref.: {np.abs(tcc_energy - reference_energy)}",
101
+ end="\r",
102
+ flush=True,
103
+ )
104
+ counts_tcc.append(eval_count_tcc)
105
+ values_tcc.append(tcc_energy)
106
+ tcc_vqe_params_lst.append(x)
107
+
108
+ return tcc_energy
109
+
110
+ callback_works = False
111
+
112
+ def callback(xk):
113
+ nonlocal callback_works
114
+ callback_works = True
115
+ print(f" ========= From callback: {xk=} =========")
116
+
117
+ maxiter = 100
118
+ excsolve_obj = ExcitationSolveScipy(maxiter=maxiter, tol=1e-10, save_parameters=True)
119
+ optimizer_func = excsolve_obj.minimize
120
+ _, parameter_occ = np.unique(tcc_uccsd.param_ids, return_counts=True)
121
+ options = dict(reference_energy=reference_energy, parameter_occ=parameter_occ)
122
+
123
+ n_params_tmp = tcc_uccsd.n_params
124
+ initial_params = np.zeros(n_params_tmp)
125
+ res_tcc = scipy.optimize.minimize(cost, initial_params, method=optimizer_func, callback=callback, options=options)
126
+ _params_tcc = res_tcc.x
127
+
128
+ print(f"\nFinal energy difference: {(res_tcc.fun - reference_energy):.2e}")
129
+
130
+ # plt.plot(excsolve_obj.nfevs, np.abs(excsolve_obj.energies - reference_energy))
131
+ # plt.plot(excsolve_obj.nfevs_after_it, np.abs(excsolve_obj.energies_after_it - reference_energy), linestyle="", marker="x")
132
+ # for x in excsolve_obj.nfevs_after_it:
133
+ # plt.axvline(x, linestyle="--", color="black", alpha=0.5)
134
+ # plt.yscale("log")
135
+ # plt.grid()
136
+ # plt.show()
137
+
138
+ assert callback_works, "Callback does not work!"
139
+
140
+
141
+ def test_tcc_h3plus_callback_intermediateresult():
142
+ logging.basicConfig(level=logging.INFO)
143
+
144
+ # molecule = pyscf_molecules.H_chain.build_hydrogen_chain(2)
145
+ molecule = pyscf_molecules.H3plus
146
+ symbols = molecule.symbols
147
+ basis = molecule.basis
148
+ geometry = molecule.geometry
149
+ charge = molecule.charge
150
+
151
+ atom = "; ".join([f"{a} {', '.join([str(x) for x in p.tolist()])}" for a, p in zip(symbols, geometry)])
152
+ symbols_unique, unique_counts = np.unique(symbols, return_counts=True)
153
+ molname = "".join([f"{a}{n}" for a, n in zip(symbols_unique, unique_counts)])
154
+ if charge > 0:
155
+ molname += "+" * np.abs(charge)
156
+ elif charge < 0:
157
+ molname += "-" * np.abs(charge)
158
+
159
+ unit = "Angstrom" # Angstrom or Bohr
160
+ mol_pyscf = gto.M(atom=atom, basis=basis, charge=charge, unit=unit)
161
+ _electrons = mol_pyscf.nelectron
162
+ rhf = scf.RHF(mol_pyscf)
163
+ _hf_energy = rhf.kernel()
164
+
165
+ _mo_occ = rhf.mo_occ
166
+
167
+ print(f"Building TCC Hamiltonian for {molname} (early build to use TCC canonicalized MO-coefficients) ...")
168
+ tcc_uccsd = UCCSD(rhf, init_method="zeros", run_hf=False, run_mp2=False, run_ccsd=False, run_fci=True) # TCC params = -params
169
+ print(f"{tcc_uccsd.engine=}")
170
+ rhf.mo_coeff = tcc_uccsd.hf.mo_coeff
171
+
172
+ fci_calc = fci.FCI(mol_pyscf, rhf.mo_coeff)
173
+ fci_energy, ci_vector = fci_calc.kernel()
174
+ print(f"FCI Energy: {fci_energy} Ha")
175
+
176
+ reference_energy = fci_energy
177
+
178
+ # one_ao = mol_pyscf.intor_symmetric("int1e_kin") + mol_pyscf.intor_symmetric("int1e_nuc")
179
+ # two_ao = mol_pyscf.intor("int2e_sph")
180
+ # one_mo = np.einsum("pi,pq,qj->ij", rhf.mo_coeff, one_ao, rhf.mo_coeff) * 1.0
181
+ # two_mo = ao2mo.incore.full(two_ao, rhf.mo_coeff) * 1.0
182
+ # two_mo = two_mo.swapaxes(1, 2).swapaxes(2, 3) # to physicist order
183
+ # core_constant = rhf.energy_nuc()
184
+
185
+ nelec = mol_pyscf.nelec
186
+ norb = mol_pyscf.nao
187
+
188
+ # energy_offset = core_constant
189
+
190
+ #####################################################################################
191
+ ## Define Ansatz ##
192
+ #####################################################################################
193
+ singles_pool = tcc_helpers.get_ex1_ops(norb, nelec)
194
+ doubles_pool = tcc_helpers.get_ex2_ops(norb, nelec)
195
+ singles_pool_sorted = sorted(singles_pool)
196
+ doubles_pool_sorted = sorted([(*sorted(x[:2]), *sorted(x[2:])) for x in doubles_pool])
197
+ # complete_pool = doubles_pool_sorted + singles_pool_sorted
198
+ complete_pool = singles_pool_sorted + doubles_pool_sorted
199
+
200
+ tcc_uccsd.ex_ops = complete_pool
201
+ # tcc_uccsd.param_ids = None
202
+ # tcc_uccsd.param_ids = [0, 0, 1]
203
+ print(f"{tcc_uccsd.param_ids=}")
204
+
205
+ n_params = len(complete_pool)
206
+ _params = np.zeros(n_params)
207
+ _ex_ops = [(x[: len(x) // 2], x[len(x) // 2 :]) for x in complete_pool]
208
+
209
+ #######################
210
+ # Manual optimization
211
+ times_tcc = []
212
+ eval_count_tcc = 0
213
+ counts_tcc = []
214
+ values_tcc = []
215
+ tcc_vqe_params_lst = []
216
+
217
+ def cost(x):
218
+ nonlocal times_tcc
219
+ nonlocal eval_count_tcc
220
+ nonlocal counts_tcc
221
+ nonlocal values_tcc
222
+
223
+ tcc_energy = tcc_uccsd.energy(x)
224
+ times_tcc.append(time.perf_counter())
225
+
226
+ eval_count_tcc += 1
227
+ print(
228
+ f"Optimizer evaluation #{eval_count_tcc}, Diff. to ref.: {np.abs(tcc_energy - reference_energy)}",
229
+ end="\r",
230
+ flush=True,
231
+ )
232
+ counts_tcc.append(eval_count_tcc)
233
+ values_tcc.append(tcc_energy)
234
+ tcc_vqe_params_lst.append(x)
235
+
236
+ return tcc_energy
237
+
238
+ callback_works = False
239
+
240
+ def callback(intermediate_result):
241
+ nonlocal callback_works
242
+ callback_works = True
243
+ print(f" ========= From callback: {intermediate_result.x=} | {intermediate_result.fun=} =========")
244
+
245
+ maxiter = 100
246
+ excsolve_obj = ExcitationSolveScipy(maxiter=maxiter, tol=1e-10, save_parameters=True)
247
+ optimizer_func = excsolve_obj.minimize
248
+ _, parameter_occ = np.unique(tcc_uccsd.param_ids, return_counts=True)
249
+ options = dict(reference_energy=reference_energy, parameter_occ=parameter_occ)
250
+
251
+ n_params_tmp = tcc_uccsd.n_params
252
+ initial_params = np.zeros(n_params_tmp)
253
+ res_tcc = scipy.optimize.minimize(cost, initial_params, method=optimizer_func, callback=callback, options=options)
254
+ _params_tcc = res_tcc.x
255
+
256
+ print(f"\nFinal energy difference: {(res_tcc.fun - reference_energy):.2e}")
257
+
258
+ # plt.plot(excsolve_obj.nfevs, np.abs(excsolve_obj.energies - reference_energy))
259
+ # plt.plot(excsolve_obj.nfevs_after_it, np.abs(excsolve_obj.energies_after_it - reference_energy), linestyle="", marker="x")
260
+ # for x in excsolve_obj.nfevs_after_it:
261
+ # plt.axvline(x, linestyle="--", color="black", alpha=0.5)
262
+ # plt.yscale("log")
263
+ # plt.grid()
264
+ # plt.show()
265
+
266
+ assert callback_works, "Callback does not work!"
@@ -30,11 +30,11 @@ def test_tcc_h3plus_mult_occurence():
30
30
 
31
31
  unit = "Angstrom" # Angstrom or Bohr
32
32
  mol_pyscf = gto.M(atom=atom, basis=basis, charge=charge, unit=unit)
33
- electrons = mol_pyscf.nelectron
33
+ _electrons = mol_pyscf.nelectron
34
34
  rhf = scf.RHF(mol_pyscf)
35
- hf_energy = rhf.kernel()
35
+ _hf_energy = rhf.kernel()
36
36
 
37
- mo_occ = rhf.mo_occ
37
+ _mo_occ = rhf.mo_occ
38
38
 
39
39
  print(f"Building TCC Hamiltonian for {molname} (early build to use TCC canonicalized MO-coefficients) ...")
40
40
  tcc_uccsd = UCCSD(rhf, init_method="zeros", run_hf=False, run_mp2=False, run_ccsd=False, run_fci=True) # TCC params = -params
@@ -75,8 +75,8 @@ def test_tcc_h3plus_mult_occurence():
75
75
  print(f"{tcc_uccsd.param_ids=}")
76
76
 
77
77
  n_params = len(complete_pool)
78
- params = np.zeros(n_params)
79
- ex_ops = [(x[: len(x) // 2], x[len(x) // 2 :]) for x in complete_pool]
78
+ _params = np.zeros(n_params)
79
+ _ex_ops = [(x[: len(x) // 2], x[len(x) // 2 :]) for x in complete_pool]
80
80
 
81
81
  #######################
82
82
  # Manual optimization
@@ -116,7 +116,7 @@ def test_tcc_h3plus_mult_occurence():
116
116
  n_params_tmp = tcc_uccsd.n_params
117
117
  initial_params = np.zeros(n_params_tmp)
118
118
  res_tcc = scipy.optimize.minimize(cost, initial_params, method=optimizer_func, options=options)
119
- params_tcc = res_tcc.x
119
+ _params_tcc = res_tcc.x
120
120
 
121
121
  print(f"\nFinal energy difference: {(res_tcc.fun - reference_energy):.2e}")
122
122