Skip to content

Commit

Permalink
Adjust API to be more consistent in specifying # spin electrons and t…
Browse files Browse the repository at this point in the history
…arget hamming weights (#11)

* Adjust API to be more consistent in specifying the numbers of spin
electrons and target hamming weights.

Co-authored-by: Kevin J. Sung <[email protected]>

* Fix docstring

* Beef up release note to cover the patch release

* Adjust bipartite hamming counts generation interface

* Fix tests

* Clarify config recovery docstring

---------

Co-authored-by: Kevin J. Sung <[email protected]>
  • Loading branch information
caleb-johnson and kevinsung committed Sep 13, 2024
1 parent 1e1bc86 commit 0748ad2
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 35 deletions.
6 changes: 5 additions & 1 deletion docs/tutorials/01_chemistry_hamiltonian.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,11 @@
"id": "851bc98e-9c08-4e78-9472-36301abc11d8",
"metadata": {},
"source": [
"First, we will transform the counts into a bitstring matrix and probability array for post-processing. Each row in the matrix represents one unique bitstring. Since qubits are normally indexed from the right of a bitstring, column ``0`` of the matrix represents qubit ``N``, and column ``N`` represents qubit ``0``."
"First, we will transform the counts into a bitstring matrix and probability array for post-processing.\n",
"\n",
"Each row in the matrix represents one unique bitstring. Since qubits are normally indexed from the right of a bitstring, column ``0`` represents qubit ``N-1``, and column ``N-1`` represents qubit ``0``, where ``N`` is the number of qubits.\n",
"\n",
"The alpha particles are represented in the column range ``[N / 2, N]``, and the beta particles are represented in the column range ``[0, N / 2)``."
]
},
{
Expand Down
29 changes: 15 additions & 14 deletions qiskit_addon_sqd/configuration_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@


def post_select_by_hamming_weight(
bitstring_matrix: np.ndarray, hamming_left: int, hamming_right: int
bitstring_matrix: np.ndarray, hamming_right: int, hamming_left: int
) -> np.ndarray:
"""
Post-select bitstrings based on the hamming weight of each half.
Args:
bitstring_matrix: A 2D array of ``bool`` representations of bit
values such that each row represents a single bitstring
hamming_left: The target hamming weight of the left half of bitstrings
hamming_right: The target hamming weight of the right half of bitstrings
hamming_left: The target hamming weight of the left half of bitstrings
Returns:
A mask signifying which samples were selected from the input matrix.
Expand All @@ -60,16 +60,17 @@ def recover_configurations(
bitstring_matrix: np.ndarray,
probabilities: Sequence[float],
avg_occupancies: np.ndarray,
hamming_left: int,
hamming_right: int,
num_elec_a: int,
num_elec_b: int,
*,
rand_seed: int | None = None,
) -> tuple[np.ndarray, np.ndarray]:
"""
Refine bitstrings based on average orbital occupancy and a target hamming weight.
This function makes the assumption that bit ``i`` represents the same orbital as
bit ``i + # orbitals`` in all input bitstrings, s.t. ``i < # orbitals``.
This function makes the assumption that bit ``i`` represents the spin-down orbital
corresponding to the spin-up orbital in bit ``i + N`` where ``N`` is the number of
spatial orbitals and ``i < N``.
Args:
bitstring_matrix: A 2D array of ``bool`` representations of bit
Expand All @@ -78,15 +79,15 @@ def recover_configurations(
avg_occupancies: A 1D array containing the mean occupancy of each orbital. It is assumed
that ``avg_occupancies[i]`` corresponds to the orbital represented by column
``i`` in ``bitstring_matrix``.
hamming_left: The target hamming weight used for the left half of the bitstring
hamming_right: The target hamming weight used for the right half of the bitstring
num_elec_a: The number of spin-up electrons in the system.
num_elec_b: The number of spin-down electrons in the system.
rand_seed: A seed to control random behavior
Returns:
A corrected bitstring matrix and an updated probability array
"""
if hamming_left < 0 or hamming_right < 0:
raise ValueError("Hamming weights must be non-negative integers.")
if num_elec_a < 0 or num_elec_b < 0:
raise ValueError("The numbers of electrons must be specified as non-negative integers.")

# First, we need to flip the orbitals such that

Expand All @@ -95,8 +96,8 @@ def recover_configurations(
bs_corrected = _bipartite_bitstring_correcting(
bitstring,
avg_occupancies,
hamming_left,
hamming_right,
num_elec_a,
num_elec_b,
rand_seed=rand_seed,
)
bs_str = np.array2string(bs_corrected.astype(int), separator="")[1:-1]
Expand Down Expand Up @@ -170,8 +171,8 @@ def _p_flip_1_to_0(ratio_exp: float, occ: float, eps: float = 0.01) -> float:
def _bipartite_bitstring_correcting(
bit_array: np.ndarray,
avg_occupancies: np.ndarray,
hamming_left: int,
hamming_right: int,
hamming_left: int,
rand_seed: int | None = None,
) -> np.ndarray:
"""
Expand All @@ -180,8 +181,8 @@ def _bipartite_bitstring_correcting(
Args:
bit_array: A 1D array of ``bool`` representations of bit values
avg_occupancies: A 1D array containing the mean occupancy of each orbital.
hamming_left: The target hamming weight used for the left half of the bitstring
hamming_right: The target hamming weight used for the right half of the bitstring
hamming_left: The target hamming weight used for the left half of the bitstring
rand_seed: A seed to control random behavior
Returns:
Expand Down
12 changes: 6 additions & 6 deletions qiskit_addon_sqd/counts.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ def generate_counts_uniform(
def generate_counts_bipartite_hamming(
num_samples: int,
num_bits: int,
hamming_left: int,
hamming_right: int,
hamming_left: int,
rand_seed: None | int = None,
) -> dict[str, int]:
"""
Expand All @@ -100,8 +100,8 @@ def generate_counts_bipartite_hamming(
Args:
num_samples: The number of samples to draw
num_bits: The number of bits in the bitstrings
hamming_left: The hamming weight on the left half of each bitstring
hamming_right: The hamming weight on the right half of each bitstring
hamming_left: The hamming weight on the left half of each bitstring
rand_seed: A seed for controlling randomness
Returns:
Expand All @@ -128,17 +128,17 @@ def generate_counts_bipartite_hamming(
sample_dict: dict[str, int] = {}
for _ in range(num_samples):
# Pick random bits to flip such that the left and right hamming weights are correct
up_flips = np.random.choice(np.arange(num_bits // 2), hamming_left, replace=False).astype(
up_flips = np.random.choice(np.arange(num_bits // 2), hamming_right, replace=False).astype(
"int"
)
dn_flips = np.random.choice(np.arange(num_bits // 2), hamming_right, replace=False).astype(
dn_flips = np.random.choice(np.arange(num_bits // 2), hamming_left, replace=False).astype(
"int"
)

# Create a bitstring with the chosen bits flipped
bts_arr = np.zeros(num_bits)
bts_arr[up_flips] = 1
bts_arr[dn_flips + num_bits // 2] = 1
bts_arr[dn_flips] = 1
bts_arr[up_flips + num_bits // 2] = 1
bts_arr = bts_arr.astype("int")
bts = np.array2string(bts_arr, separator="")[1:-1]

Expand Down
4 changes: 2 additions & 2 deletions qiskit_addon_sqd/subsampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
def postselect_and_subsample(
bitstring_matrix: np.ndarray,
probabilities: np.ndarray,
hamming_left: int,
hamming_right: int,
hamming_left: int,
samples_per_batch: int,
num_batches: int,
rand_seed: int | None = None,
Expand All @@ -52,8 +52,8 @@ def postselect_and_subsample(
bitstring_matrix: A 2D array of ``bool`` representations of bit
values such that each row represents a single bitstring.
probabilities: A 1D array specifying a probability distribution over the bitstrings
hamming_left: The target hamming weight for the left half of sampled bitstrings
hamming_right: The target hamming weight for the right half of sampled bitstrings
hamming_left: The target hamming weight for the left half of sampled bitstrings
samples_per_batch: The number of samples to draw for each batch
num_batches: The number of batches to generate
rand_seed: A seed to control random behavior
Expand Down
56 changes: 56 additions & 0 deletions releasenotes/notes/subsample-hamming-76674dbaf6f411c2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
prelude: >
This is a patch release which introduces a couple of small, but important, breaking changes to to the API. These changes allow for a more consistent pattern in specifying the number of alpha and beta electrons throughout both the chemistry and non-chemistry functions in the API.
upgrade:
- |
The :func:`qiskit_addon_sqd.counts.generate_counts_bipartite_hamming`, :func:`qiskit_addon_sqd.subsampling.postselect_and_subsample`, and :func:`qiskit_addon_sqd.configuration_recovery.post_select_by_hamming_weight` now take the ``hamming_right`` positional argument before the ``hamming_left`` argument to better match the rest of the workflow.
To upgrade
.. code-block:: python
from qiskit_addon_sqd.configuration_recovery import post_select_by_hamming_weight
from qiskit_addon_sqd.subsampling import postselect_and_subsample
from qiskit_addon_sqd.counts import generate_counts_bipartite_hamming
counts = generate_counts_bipartite_hamming(num_samples, num_bits, num_elec_b, num_elec_a)
...
bs_mat = post_select_by_hamming_weight(bs_mat_full, num_elec_b, num_elec_a)
...
batches = postselect_and_subsample(
bs_mat,
probs_arr,
num_elec_b,
num_elec_a,
samples_per_batch,
n_batches,
)
should be changed to
.. code-block:: python
from qiskit_addon_sqd.configuration_recovery import post_select_by_hamming_weight
from qiskit_addon_sqd.subsampling import postselect_and_subsample
from qiskit_addon_sqd.counts import generate_counts_bipartite_hamming
counts = generate_counts_bipartite_hamming(num_samples, num_bits, num_elec_a, num_elec_b)
bs_mat = post_select_by_hamming_weight(bs_mat_full, num_elec_a, num_elec_b)
...
batches = postselect_and_subsample(
bs_mat,
probs_arr,
num_elec_a,
num_elec_b,
samples_per_batch,
n_batches,
)
10 changes: 5 additions & 5 deletions test/test_counts.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_generate_counts_bipartite_hamming(self):
hamming_left = 3
hamming_right = 2
counts = generate_counts_bipartite_hamming(
num_samples, num_bits, hamming_left, hamming_right
num_samples, num_bits, hamming_right, hamming_left
)
self.assertLessEqual(len(counts), num_samples)
for bs in counts:
Expand All @@ -92,7 +92,7 @@ def test_generate_counts_bipartite_hamming(self):
hamming_right = 2
with pytest.raises(ValueError) as e_info:
generate_counts_bipartite_hamming(
num_samples, num_bits, hamming_left, hamming_right
num_samples, num_bits, hamming_right, hamming_left
)
self.assertEqual(
"The number of bits must be specified with an even integer.", e_info.value.args[0]
Expand All @@ -104,7 +104,7 @@ def test_generate_counts_bipartite_hamming(self):
hamming_right = 2
with pytest.raises(ValueError) as e_info:
generate_counts_bipartite_hamming(
num_samples, num_bits, hamming_left, hamming_right
num_samples, num_bits, hamming_right, hamming_left
)
self.assertEqual(
"The number of samples must be specified with a positive integer.",
Expand All @@ -117,7 +117,7 @@ def test_generate_counts_bipartite_hamming(self):
hamming_right = 2
with pytest.raises(ValueError) as e_info:
generate_counts_bipartite_hamming(
num_samples, num_bits, hamming_left, hamming_right
num_samples, num_bits, hamming_right, hamming_left
)
self.assertEqual(
"The number of bits must be specified with a positive integer.",
Expand All @@ -130,7 +130,7 @@ def test_generate_counts_bipartite_hamming(self):
hamming_right = -1
with pytest.raises(ValueError) as e_info:
generate_counts_bipartite_hamming(
num_samples, num_bits, hamming_left, hamming_right
num_samples, num_bits, hamming_right, hamming_left
)
self.assertEqual(
"Hamming weights must be specified as non-negative integers.", e_info.value.args[0]
Expand Down
14 changes: 7 additions & 7 deletions test/test_subsampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ def test_postselect_and_subsample(self):
batches = postselect_and_subsample(
self.bitstring_matrix,
self.uniform_probs,
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand All @@ -158,8 +158,8 @@ def test_postselect_and_subsample(self):
batches = postselect_and_subsample(
self.bitstring_matrix,
self.uniform_probs,
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand All @@ -178,8 +178,8 @@ def test_postselect_and_subsample(self):
batches = postselect_and_subsample(
self.bitstring_matrix[1:],
self.uniform_probs[1:],
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand All @@ -195,8 +195,8 @@ def test_postselect_and_subsample(self):
postselect_and_subsample(
self.bitstring_matrix,
self.uniform_probs,
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand All @@ -213,8 +213,8 @@ def test_postselect_and_subsample(self):
postselect_and_subsample(
self.bitstring_matrix,
self.uniform_probs,
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand All @@ -231,8 +231,8 @@ def test_postselect_and_subsample(self):
postselect_and_subsample(
self.bitstring_matrix,
self.uniform_probs,
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand All @@ -249,8 +249,8 @@ def test_postselect_and_subsample(self):
postselect_and_subsample(
self.bitstring_matrix,
np.array([]),
hamming_left,
hamming_right,
hamming_left,
samples_per_batch,
num_batches,
)
Expand Down

0 comments on commit 0748ad2

Please sign in to comment.