diff --git a/docs/tutorials/01_chemistry_hamiltonian.ipynb b/docs/tutorials/01_chemistry_hamiltonian.ipynb index 9e452ce..acb3e7f 100644 --- a/docs/tutorials/01_chemistry_hamiltonian.ipynb +++ b/docs/tutorials/01_chemistry_hamiltonian.ipynb @@ -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)``." ] }, { diff --git a/qiskit_addon_sqd/configuration_recovery.py b/qiskit_addon_sqd/configuration_recovery.py index f8fa7cc..97e5ba3 100644 --- a/qiskit_addon_sqd/configuration_recovery.py +++ b/qiskit_addon_sqd/configuration_recovery.py @@ -30,7 +30,7 @@ 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. @@ -38,8 +38,8 @@ def post_select_by_hamming_weight( 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. @@ -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 @@ -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 @@ -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] @@ -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: """ @@ -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: diff --git a/qiskit_addon_sqd/counts.py b/qiskit_addon_sqd/counts.py index 1a1d453..7ca402c 100644 --- a/qiskit_addon_sqd/counts.py +++ b/qiskit_addon_sqd/counts.py @@ -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]: """ @@ -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: @@ -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] diff --git a/qiskit_addon_sqd/subsampling.py b/qiskit_addon_sqd/subsampling.py index 67c1c88..0a68ca6 100644 --- a/qiskit_addon_sqd/subsampling.py +++ b/qiskit_addon_sqd/subsampling.py @@ -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, @@ -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 diff --git a/releasenotes/notes/subsample-hamming-76674dbaf6f411c2.yaml b/releasenotes/notes/subsample-hamming-76674dbaf6f411c2.yaml new file mode 100644 index 0000000..c0baa63 --- /dev/null +++ b/releasenotes/notes/subsample-hamming-76674dbaf6f411c2.yaml @@ -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, + ) diff --git a/test/test_counts.py b/test/test_counts.py index a48f446..18874b2 100644 --- a/test/test_counts.py +++ b/test/test_counts.py @@ -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: @@ -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] @@ -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.", @@ -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.", @@ -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] diff --git a/test/test_subsampling.py b/test/test_subsampling.py index 174715d..5eeadfe 100644 --- a/test/test_subsampling.py +++ b/test/test_subsampling.py @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, )