Skip to content

Commit f53c218

Browse files
Refactor how mean orbital occupancies are specified throughout API (#151)
* Flip orbital occupancies inside the solver. * Make type right * style * Remove flip function from docs files * oo * update reno * Return 1 1D array for the occupancies * peer review * Fix notebooks * typo * mypy * lint
1 parent eedfcf9 commit f53c218

9 files changed

+75
-98
lines changed

docs/apidocs/qiskit_addon_sqd.fermion.rst

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ Fermion (:mod:`qiskit_addon_sqd.fermion`)
1212
.. autoclass:: SCIState
1313
.. autofunction:: bitstring_matrix_to_ci_strs
1414
.. autofunction:: enlarge_batch_from_transitions
15-
.. autofunction:: flip_orbital_occupancies
1615
.. autofunction:: solve_fermion
1716
.. autofunction:: optimize_orbitals
1817
.. autofunction:: rotate_integrals

docs/conf.py

-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@
122122
("qiskit_addon_sqd.counts", "normalize_counts_dict"),
123123
("qiskit_addon_sqd.fermion", "bitstring_matrix_to_ci_strs"),
124124
("qiskit_addon_sqd.fermion", "enlarge_batch_from_transitions"),
125-
("qiskit_addon_sqd.fermion", "flip_orbital_occupancies"),
126125
("qiskit_addon_sqd.fermion", "solve_fermion"),
127126
("qiskit_addon_sqd.fermion", "optimize_orbitals"),
128127
("qiskit_addon_sqd.fermion", "rotate_integrals"),

docs/how_tos/choose_subspace_dimension.ipynb

+8-12
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@
139139
"from qiskit_addon_sqd.configuration_recovery import recover_configurations\n",
140140
"from qiskit_addon_sqd.fermion import (\n",
141141
" bitstring_matrix_to_ci_strs,\n",
142-
" flip_orbital_occupancies,\n",
143142
" solve_fermion,\n",
144143
")\n",
145144
"from qiskit_addon_sqd.subsampling import postselect_and_subsample\n",
@@ -162,13 +161,13 @@
162161
" e_hist = np.zeros((iterations, n_batches)) # energy history\n",
163162
" s_hist = np.zeros((iterations, n_batches)) # spin history\n",
164163
" d_hist = np.zeros((iterations, n_batches)) # subspace dimension history\n",
165-
" occupancy_hist = np.zeros((iterations, 2 * num_orbitals))\n",
166-
" occupancies_bitwise = None # orbital i corresponds to column i in bitstring matrix\n",
164+
" occupancy_hist = []\n",
165+
" avg_occupancy = None\n",
167166
" for i in range(iterations):\n",
168167
" print(f\"Starting configuration recovery iteration {i}\")\n",
169168
" # On the first iteration, we have no orbital occupancy information from the\n",
170169
" # solver, so we just post-select from the full bitstring set based on hamming weight.\n",
171-
" if occupancies_bitwise is None:\n",
170+
" if avg_occupancy is None:\n",
172171
" bs_mat_tmp = bitstring_matrix_full\n",
173172
" probs_arr_tmp = probs_arr_full\n",
174173
"\n",
@@ -178,7 +177,7 @@
178177
" bs_mat_tmp, probs_arr_tmp = recover_configurations(\n",
179178
" bitstring_matrix_full,\n",
180179
" probs_arr_full,\n",
181-
" occupancies_bitwise,\n",
180+
" avg_occupancy,\n",
182181
" num_elec_a,\n",
183182
" num_elec_b,\n",
184183
" rand_seed=rng,\n",
@@ -199,7 +198,7 @@
199198
" int_e = np.zeros(n_batches)\n",
200199
" int_s = np.zeros(n_batches)\n",
201200
" int_d = np.zeros(n_batches)\n",
202-
" int_occs = np.zeros((n_batches, 2 * num_orbitals))\n",
201+
" int_occs = []\n",
203202
" cs = []\n",
204203
" for j in range(n_batches):\n",
205204
" ci_strs = bitstring_matrix_to_ci_strs(batches[j], open_shell=open_shell)\n",
@@ -215,20 +214,17 @@
215214
" energy_sci += nuclear_repulsion_energy\n",
216215
" int_e[j] = energy_sci\n",
217216
" int_s[j] = spin\n",
218-
" int_occs[j, :num_orbitals] = avg_occs[0]\n",
219-
" int_occs[j, num_orbitals:] = avg_occs[1]\n",
217+
" int_occs.append(avg_occs)\n",
220218
" cs.append(coeffs_sci)\n",
221219
"\n",
222220
" # Combine batch results\n",
223-
" avg_occupancy = np.mean(int_occs, axis=0)\n",
224-
" # The occupancies from the solver should be flipped to match the bits in the bitstring matrix.\n",
225-
" occupancies_bitwise = flip_orbital_occupancies(avg_occupancy)\n",
221+
" avg_occupancy = tuple(np.mean(int_occs, axis=0))\n",
226222
"\n",
227223
" # Track optimization history\n",
228224
" e_hist[i, :] = int_e\n",
229225
" s_hist[i, :] = int_s\n",
230226
" d_hist[i, :] = int_d\n",
231-
" occupancy_hist[i, :] = avg_occupancy\n",
227+
" occupancy_hist.append(avg_occupancy)\n",
232228
"\n",
233229
" return e_hist.flatten(), d_hist.flatten()"
234230
]

docs/how_tos/integrate_dice_solver.ipynb

+6-12
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@
2929
"from qiskit_addon_dice_solver import solve_fermion\n",
3030
"from qiskit_addon_sqd.configuration_recovery import recover_configurations\n",
3131
"from qiskit_addon_sqd.counts import counts_to_arrays, generate_counts_uniform\n",
32-
"from qiskit_addon_sqd.fermion import (\n",
33-
" flip_orbital_occupancies,\n",
34-
")\n",
3532
"from qiskit_addon_sqd.subsampling import postselect_and_subsample\n",
3633
"\n",
3734
"# Specify molecule properties\n",
@@ -87,12 +84,12 @@
8784
"samples_per_batch = 300\n",
8885
"\n",
8986
"# Self-consistent configuration recovery loop\n",
90-
"occupancies_bitwise = None # orbital i corresponds to column i in bitstring matrix\n",
9187
"e_hist = np.zeros((iterations, n_batches))\n",
88+
"avg_occupancy = None\n",
9289
"for i in range(iterations):\n",
9390
" # On the first iteration, we have no orbital occupancy information from the\n",
9491
" # solver, so we just post-select from the full bitstring set based on hamming weight.\n",
95-
" if occupancies_bitwise is None:\n",
92+
" if avg_occupancy is None:\n",
9693
" bs_mat_tmp = bitstring_matrix_full\n",
9794
" probs_arr_tmp = probs_arr_full\n",
9895
"\n",
@@ -102,7 +99,7 @@
10299
" bs_mat_tmp, probs_arr_tmp = recover_configurations(\n",
103100
" bitstring_matrix_full,\n",
104101
" probs_arr_full,\n",
105-
" occupancies_bitwise,\n",
102+
" avg_occupancy,\n",
106103
" num_elec_a,\n",
107104
" num_elec_b,\n",
108105
" rand_seed=rng,\n",
@@ -120,7 +117,7 @@
120117
" )\n",
121118
" # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n",
122119
" int_e = np.zeros(n_batches)\n",
123-
" int_occs = np.zeros((n_batches, 2 * num_orbitals))\n",
120+
" int_occs = []\n",
124121
" for j, batch in enumerate(batches):\n",
125122
" energy_sci, wf_mags, avg_occs = solve_fermion(\n",
126123
" batch,\n",
@@ -129,13 +126,10 @@
129126
" mpirun_options=[\"-n\", \"8\"],\n",
130127
" )\n",
131128
" int_e[j] = energy_sci + nuclear_repulsion_energy\n",
132-
" int_occs[j, :num_orbitals] = avg_occs[0]\n",
133-
" int_occs[j, num_orbitals:] = avg_occs[1]\n",
129+
" int_occs.append(avg_occs)\n",
134130
"\n",
135131
" # Combine batch results\n",
136-
" avg_occupancy = np.mean(int_occs, axis=0)\n",
137-
" # The occupancies from the solver should be flipped to match the bits in the bitstring matrix.\n",
138-
" occupancies_bitwise = flip_orbital_occupancies(avg_occupancy)\n",
132+
" avg_occupancy = tuple(np.mean(int_occs, axis=0))\n",
139133
"\n",
140134
" # Track optimization history\n",
141135
" e_hist[i, :] = int_e"

docs/how_tos/use_oo_to_optimize_hamiltonian_basis.ipynb

+11-14
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
],
142142
"source": [
143143
"from qiskit_addon_sqd.configuration_recovery import recover_configurations\n",
144-
"from qiskit_addon_sqd.fermion import flip_orbital_occupancies, solve_fermion\n",
144+
"from qiskit_addon_sqd.fermion import solve_fermion\n",
145145
"from qiskit_addon_sqd.subsampling import postselect_and_subsample\n",
146146
"\n",
147147
"# SQSD options\n",
@@ -155,13 +155,13 @@
155155
"# Self-consistent configuration recovery loop\n",
156156
"e_hist = np.zeros((iterations, n_batches)) # energy history\n",
157157
"s_hist = np.zeros((iterations, n_batches)) # spin history\n",
158-
"occupancy_hist = np.zeros((iterations, 2 * num_orbitals))\n",
159-
"occupancies_bitwise = None # orbital i corresponds to column i in bitstring matrix\n",
158+
"occupancy_hist = []\n",
159+
"avg_occupancy = None\n",
160160
"for i in range(iterations):\n",
161161
" print(f\"Starting configuration recovery iteration {i}\")\n",
162162
" # On the first iteration, we have no orbital occupancy information from the\n",
163163
" # solver, so we just post-select from the full bitstring set based on hamming weight.\n",
164-
" if occupancies_bitwise is None:\n",
164+
" if avg_occupancy is None:\n",
165165
" bs_mat_tmp = bitstring_matrix_full\n",
166166
" probs_arr_tmp = probs_arr_full\n",
167167
"\n",
@@ -171,7 +171,7 @@
171171
" bs_mat_tmp, probs_arr_tmp = recover_configurations(\n",
172172
" bitstring_matrix_full,\n",
173173
" probs_arr_full,\n",
174-
" occupancies_bitwise,\n",
174+
" avg_occupancy,\n",
175175
" num_elec_a,\n",
176176
" num_elec_b,\n",
177177
" rand_seed=rng,\n",
@@ -191,7 +191,7 @@
191191
" # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n",
192192
" int_e = np.zeros(n_batches)\n",
193193
" int_s = np.zeros(n_batches)\n",
194-
" int_occs = np.zeros((n_batches, 2 * num_orbitals))\n",
194+
" int_occs = []\n",
195195
" cs = []\n",
196196
" for j in range(n_batches):\n",
197197
" energy_sci, coeffs_sci, avg_occs, spin = solve_fermion(\n",
@@ -205,19 +205,16 @@
205205
" energy_sci += nuclear_repulsion_energy\n",
206206
" int_e[j] = energy_sci\n",
207207
" int_s[j] = spin\n",
208-
" int_occs[j, :num_orbitals] = avg_occs[0]\n",
209-
" int_occs[j, num_orbitals:] = avg_occs[1]\n",
208+
" int_occs.append(avg_occs)\n",
210209
" cs.append(coeffs_sci)\n",
211210
"\n",
212211
" # Combine batch results\n",
213-
" avg_occupancy = np.mean(int_occs, axis=0)\n",
214-
" # The occupancies from the solver should be flipped to match the bits in the bitstring matrix.\n",
215-
" occupancies_bitwise = flip_orbital_occupancies(avg_occupancy)\n",
212+
" avg_occupancy = tuple(np.mean(int_occs, axis=0))\n",
216213
"\n",
217214
" # Track optimization history\n",
218215
" e_hist[i, :] = int_e\n",
219216
" s_hist[i, :] = int_s\n",
220-
" occupancy_hist[i, :] = avg_occupancy"
217+
" occupancy_hist.append(avg_occupancy)"
221218
]
222219
},
223220
{
@@ -350,9 +347,9 @@
350347
"name": "stdout",
351348
"output_type": "stream",
352349
"text": [
353-
"Exact energy: -109.04667177808028\n",
350+
"Exact energy: -109.04667177808032\n",
354351
"SQD energy: -108.7531706601421\n",
355-
"Energy after OO: -108.80400806164369\n"
352+
"Energy after OO: -108.80400806164377\n"
356353
]
357354
}
358355
],

docs/tutorials/01_chemistry_hamiltonian.ipynb

+15-21
Large diffs are not rendered by default.

qiskit_addon_sqd/configuration_recovery.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import warnings
1819
from collections import defaultdict
1920
from collections.abc import Sequence
2021

@@ -51,7 +52,7 @@ def post_select_by_hamming_weight(
5152
def recover_configurations(
5253
bitstring_matrix: np.ndarray,
5354
probabilities: Sequence[float],
54-
avg_occupancies: np.ndarray,
55+
avg_occupancies: tuple[np.ndarray, np.ndarray],
5556
num_elec_a: int,
5657
num_elec_b: int,
5758
rand_seed: np.random.Generator | int | None = None,
@@ -72,9 +73,9 @@ def recover_configurations(
7273
bitstring_matrix: A 2D array of ``bool`` representations of bit
7374
values such that each row represents a single bitstring
7475
probabilities: A 1D array specifying a probability distribution over the bitstrings
75-
avg_occupancies: A 1D array containing the mean occupancy of each orbital. It is assumed
76-
that ``avg_occupancies[i]`` corresponds to the orbital represented by column
77-
``i`` in ``bitstring_matrix``.
76+
avg_occupancies: A length-2 tuple of arrays holding the mean occupancy of the spin-up
77+
and spin-down orbitals, respectively. The occupancies should be formatted as:
78+
``(array([occ_a_0, ..., occ_a_N]), array([occ_b_0, ..., occ_b_N]))``
7879
num_elec_a: The number of spin-up electrons in the system.
7980
num_elec_b: The number of spin-down electrons in the system.
8081
rand_seed: A seed for controlling randomness
@@ -85,20 +86,28 @@ def recover_configurations(
8586
References:
8687
[1]: J. Robledo-Moreno, et al., `Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer <https://arxiv.org/abs/2405.05068>`_,
8788
arXiv:2405.05068 [quant-ph].
88-
8989
"""
9090
rng = np.random.default_rng(rand_seed)
9191

92+
occ_dims = len(np.array(avg_occupancies).shape)
93+
if occ_dims == 1:
94+
warnings.warn(
95+
"Passing avg_occupancies as a 1D array is deprecated. Pass a length-2 tuple containing the spin-up and spin-down occupancies respectively.",
96+
DeprecationWarning,
97+
stacklevel=2,
98+
)
99+
norb = bitstring_matrix.shape[1] // 2
100+
avg_occupancies = (np.flip(avg_occupancies[norb:]), np.flip(avg_occupancies[:norb]))
101+
92102
if num_elec_a < 0 or num_elec_b < 0:
93103
raise ValueError("The numbers of electrons must be specified as non-negative integers.")
94104

95-
# First, we need to flip the orbitals such that
96-
97105
corrected_dict: defaultdict[str, float] = defaultdict(float)
106+
occs_array = np.flip(avg_occupancies).flatten()
98107
for bitstring, freq in zip(bitstring_matrix, probabilities):
99108
bs_corrected = _bipartite_bitstring_correcting(
100109
bitstring,
101-
avg_occupancies,
110+
occs_array,
102111
num_elec_a,
103112
num_elec_b,
104113
rng=rng,

qiskit_addon_sqd/fermion.py

+7-29
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def solve_fermion(
7979
spin_sq: float | None = None,
8080
max_davidson: int = 100,
8181
verbose: int | None = None,
82-
) -> tuple[float, SCIState, list[np.ndarray], float]:
82+
) -> tuple[float, SCIState, tuple[np.ndarray, np.ndarray], float]:
8383
"""Approximate the ground state given molecular integrals and a set of electronic configurations.
8484
8585
Args:
@@ -107,7 +107,7 @@ def solve_fermion(
107107
Returns:
108108
- Minimum energy from SCI calculation
109109
- The SCI ground state
110-
- Average occupancy of the alpha and beta orbitals, respectively
110+
- Tuple containing orbital occupancies for spin-up and spin-down orbitals. Formatted as: ``(array([occ_a_0, ..., occ_a_N]), array([occ_b_0, ..., occ_b_N]))``
111111
- Expectation value of spin-squared
112112
113113
"""
@@ -145,7 +145,7 @@ def solve_fermion(
145145

146146
# Calculate the avg occupancy of each orbital
147147
dm1 = myci.make_rdm1s(sci_vec, norb, (num_up, num_dn))
148-
avg_occupancy = [np.diagonal(dm1[0]), np.diagonal(dm1[1])]
148+
avg_occupancy = (np.diagonal(dm1[0]), np.diagonal(dm1[1]))
149149

150150
# Compute total spin
151151
spin_squared = myci.spin_square(sci_vec, norb, (num_up, num_dn))[0]
@@ -175,7 +175,7 @@ def optimize_orbitals(
175175
num_steps_grad: int = 10_000,
176176
learning_rate: float = 0.01,
177177
max_davidson: int = 100,
178-
) -> tuple[float, np.ndarray, list[np.ndarray]]:
178+
) -> tuple[float, np.ndarray, tuple[np.ndarray, np.ndarray]]:
179179
"""Optimize orbitals to produce a minimal ground state.
180180
181181
The process involves iterating over 3 steps:
@@ -219,7 +219,7 @@ def optimize_orbitals(
219219
Returns:
220220
- The groundstate energy found during the last optimization iteration
221221
- An optimized 1D array defining the orbital transform
222-
- Average orbital occupancy
222+
- Tuple containing orbital occupancies for spin-up and spin-down orbitals. Formatted as: ``(array([occ_a_0, ..., occ_a_N]), array([occ_b_0, ..., occ_b_N]))``
223223
224224
"""
225225
norb = hcore.shape[0]
@@ -270,14 +270,15 @@ def optimize_orbitals(
270270
dm1, dm2_chem = myci.make_rdm12(amplitudes, norb, (num_up, num_dn))
271271
dm2 = np.asarray(dm2_chem.transpose(0, 2, 3, 1), order="C")
272272
dm1a, dm1b = myci.make_rdm1s(amplitudes, norb, (num_up, num_dn))
273+
avg_occupancy = (np.diagonal(dm1a), np.diagonal(dm1b))
273274

274275
# TODO: Expose the momentum parameter as an input option
275276
# Optimize the basis rotations
276277
_optimize_orbitals_sci(
277278
k_flat, learning_rate, 0.9, num_steps_grad, dm1, dm2, hcore, eri_phys
278279
)
279280

280-
return e_qsci, k_flat, [np.diagonal(dm1a), np.diagonal(dm1b)]
281+
return e_qsci, k_flat, avg_occupancy
281282

282283

283284
def rotate_integrals(
@@ -320,29 +321,6 @@ def rotate_integrals(
320321
return np.array(hcore_rot), np.array(eri_rot)
321322

322323

323-
def flip_orbital_occupancies(occupancies: np.ndarray) -> np.ndarray:
324-
"""Flip an orbital occupancy array to match the indexing of a bitstring.
325-
326-
This function reformats a 1D array of spin-orbital occupancies formatted like:
327-
328-
``[occ_a_1, occ_a_2, ..., occ_a_N, occ_b_1, ..., occ_b_N]``
329-
330-
To an array formatted like:
331-
332-
``[occ_a_N, ..., occ_a_1, occ_b_N, ..., occ_b_1]``
333-
334-
where ``N`` is the number of spatial orbitals.
335-
"""
336-
norb = occupancies.shape[0] // 2
337-
occ_up = occupancies[:norb]
338-
occ_dn = occupancies[norb:]
339-
occ_out = np.zeros(2 * norb)
340-
occ_out[:norb] = np.flip(occ_up)
341-
occ_out[norb:] = np.flip(occ_dn)
342-
343-
return occ_out
344-
345-
346324
@deprecate_func(
347325
removal_timeline="no sooner than qiskit-addon-sqd 0.10.0",
348326
since="0.6.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
upgrade:
3+
- |
4+
Removed :func:`qiskit_addon_sqd.fermion.flip_orbital_occupancies`. Users no longer need to flip the orbital occupancies output from :func:`qiskit_addon_sqd.fermion.solve_fermion` and :func:`qiskit_addon_sqd.fermion.optimize_orbitals`; they will be output in the order expected by :func:`qiskit_addon_sqd.configuration_recovery.recover_configurations`: ``tuple(array([occ_a_0, ..., occ_a_N]), array([occ_b_0, ..., occ_b_N]))``.
5+
deprecations:
6+
- |
7+
The ``avg_occupancies`` argument to :func:`qiskit_addon_sqd.configuration_recovery.recover_configurations` should now be a length-2 tuple containing the spin-up and spin-down occupancies, respectively.
8+
9+
Old format: ``array([occ_b_N, ..., occ_b_0, occ_a_N, ..., occ_a_0])``
10+
11+
New format: ``tuple(array([occ_a_0, ..., occ_a_N]), array([occ_b_0, ..., occ_b_N]))``

0 commit comments

Comments
 (0)