Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust API to be more consistent in specifying # spin electrons and target hamming weights #11

Merged
merged 6 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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