Release Notes

0.14.0

New Features

  • Adds a guide showing how to use fulqrum as a subspace solver for fermionic systems. To install the optional dependency, use the fulqrum extra:

    pip install 'qiskit-addon-sqd[fulqrum]'
    
  • Adds a guide showing how to use the SBD eigensolver as a subspace solver for fermionic systems. To install the optional dependency, use the sbd extra:

    pip install 'qiskit-addon-sqd[sbd]'
    
  • Adds the qiskit_addon_sqd.trim module, containing TrimPolicy. This implements trim SQD, a schedule that treats the subsampled batches as candidates to be screened rather than as independent estimates: each batch is diagonalized and trimmed down to the CI strings carrying the largest weight, and the survivors of all the batches are merged and diagonalized together. The merged diagonalization is what the iteration reports, and its own trimmed strings seed the next one.

    An iteration therefore costs num_batches + 1 diagonalizations rather than num_batches. Screening only selects among batches that differ, so TrimPolicy draws them as a single pool of samples_per_batch * num_batches bitstrings divided among the batches, leaving no bitstring in more than one. That follows from what the schedule does and is not separately configurable.

  • Adds a guide covering trim SQD, including how to bound the subspace that trimming produces and how to check whether the screening step is helping on a given system.

  • diagonalize_fermionic_hamiltonian() has a new policy argument, which selects the schedule each iteration follows: how the sampled configurations become subspaces, whether one round of diagonalizations leads to another, which result the iteration reports, and which CI strings seed the next one. Trim SQD is chosen by passing a TrimPolicy. The argument defaults to StandardPolicy, the behavior this function has always had, so omitting it changes nothing.

    A policy is described by the SubspacePolicy protocol and receives its inputs as a SubspaceRequest. It returns the subspaces and the loop calls sci_solver with them, so a schedule and an eigensolver are chosen independently and any combination works.

    A policy chooses which strings a subspace holds. The constraints on the shape of a subspace remain the caller’s: the configurations requested through include_configurations go into every subspace, spin symmetrization is enforced where it was asked for, and each spin sector is truncated to max_dim.

  • An sci_solver may now report the configurations it wants carried into the next iteration, and may skip returning the eigenvector altogether. This is what lets trim SQD scale: an eigensolver that ranks and trims its own subspace already knows which configurations survive, and the eigenvector it would otherwise have to hand back is of size len(strings_a) * len(strings_b) – the one quantity a large distributed calculation cannot afford to move between processes.

    Two additions to SCIResult provide this. The new optional field carryover holds a pair (strings_a, strings_b) of CI strings; when a solver sets it, those strings are used in place of the ones that would otherwise be selected from the eigenvector amplitudes using carryover_threshold. And sci_state may now be None, so that a solver which never materializes the eigenvector does not have to fabricate one. Such a solver must set carryover, since there would otherwise be nothing to build the next subspace from; a result with neither now raises ValueError.

    The solvers built into this package always return an eigenvector, so nothing changes for callers using them.

  • Adds partition_subsample(), which draws a pool of bitstrings and divides it among the batches so that they are disjoint. This is what TrimPolicy uses, exposed for callers writing a policy of their own.

  • Adds batch_to_ci_strings(), which converts a batch of bitstrings into a pair of per-spin CI string arrays ranked by the number of times each string was sampled. This is the step that turns subsampled bitstrings into a subspace, exposed so that a policy does not have to reimplement it.

Upgrade Notes

  • NumPy 2.0 and SciPy 1.14.1 are now the minimum required versions. Support for NumPy 1.x has been dropped, following Qiskit’s adoption of SPEC 0.

  • The minimum version of Qiskit is now 2.0.

  • Code that reads sci_state from a result produced by a custom sci_solver should be prepared for None, as the field is now optional. Results from the solvers built into this package always populate it, so callers using the default solver are unaffected.

  • carryover_threshold is now a setting of the default policy, which is what chooses the carryover by thresholding amplitudes. It remains an argument of diagonalize_fermionic_hamiltonian() with the same meaning and the same default, but passing it together with an explicit policy raises ValueError rather than being silently ignored, since another policy chooses the carryover in its own way. Pass the threshold to StandardPolicy when constructing one explicitly.

Bug Fixes

  • Fixed the ranking of sampled CI strings when symmetrize_spin=True in qiskit_addon_sqd.fermion.diagonalize_fermionic_hamiltonian(). If the same string was sampled in both spin sectors, its alpha and beta counts were previously ranked separately rather than being added together. This could cause a lower-weight string to be retained when max_dim truncated the subspace.

0.13.0

New Features

  • Added support for Python 3.13 and Python 3.14. No code changes were necessary, so older releases are expected to work on Python 3.13 and 3.14 too.

  • qiskit_addon_sqd.fermion.diagonalize_fermionic_hamiltonian() can now be called collectively from all processes of a multi-process (SPMD) execution, for example under MPI. When invoked this way, the sci_solver step runs on every process, so a custom, distributed sci_solver can spread each diagonalization across the available processes; the rest of the configuration-recovery loop runs on the control process alone, and the return value is the same on every process. Single-process behavior is unchanged. See this guide for details on the package’s support for multi-process and multi-threaded acceleration.

Deprecation Notes

0.12.1

Upgrade Notes

  • Python 3.10 is now the minimum required version. Support for Python 3.9 has been dropped.

0.12.0

New Features

Upgrade Notes

Deprecation Notes

Bug Fixes

0.11.0

New Features

Upgrade Notes

Bug Fixes

  • Fixed a float to integer type conversion affecting the qubit module

0.10.0

Upgrade Notes

Deprecation Notes

  • The avg_occupancies argument to qiskit_addon_sqd.configuration_recovery.recover_configurations() should now be a length-2 tuple containing the spin-up and spin-down occupancies, respectively.

    Old format: array([occ_b_N, ..., occ_b_0, occ_a_N, ..., occ_a_0])

    New format: tuple(array([occ_a_0, ..., occ_a_N]), array([occ_b_0, ..., occ_b_N]))

Bug Fixes

0.9.0

Upgrade Notes

Bug Fixes

  • Fixed an indexing error in qiskit_addon_sqd.configuration_recovery() which caused bits in the right half of the bitstring to be flipped with respect to the occupancies of the oritals associated with the left half of the bitstring.

  • pyscf is no longer considered a dependency on Windows, where it fails to install. However, Windows remains an unsupported platform.

0.8.0

New Features

  • Added support for Python 3.9.

  • All functions which take a rand_seed argument now also accept a numpy.random.Generator instance as the rand_seed.

Upgrade Notes

0.7.0

Bug Fixes

0.6.0

Upgrade Notes

  • Specifying addresses as a keyword argument to qiskit_addon_sqd.fermion.solve_fermion() and qiskit_addon_sqd.fermion.optimize_orbitals() is no longer supported. Users may still pass addresses as the first positional argument; however, this usage is deprecated. Users are encouraged to pass the bitstring matrix defining the subspace as the first positional arguments to these functions, as shown below.

    To upgrade, change this code

    # DEPRECATED CODE
    from qiskit_addon_sqd.fermion import (
        bitstring_matrix_to_sorted_addresses,
        solve_fermion,
        optimize_orbitals,
    )
    
    bitstring_matrix = ...
    addresses = bitstring_matrix_to_sorted_addresses(bitstring_matrix, open_shell=open_shell)
    energy, coeffs, occs, spin = solve_fermion(
                                     addresses=addresses,
                                     hcore=hcore,
                                     eri=eri,
                                 )
    ...
    e_oo, rotation, occs_oo = optimize_orbitals(
                                  addresses=addresses,
                                  hcore=hcore,
                                  eri=eri,
                              )
    
    ### SHOULD BECOME ###
    
    # NEW CODE
    from qiskit_addon_sqd.fermion import solve_fermion, optimize_orbitals
    
    bitstring_matrix = ...
    energy, coeffs, occs, spin = solve_fermion(
                                     bitstring_matrix,
                                     hcore=hcore,
                                     eri=eri,
                                 )
    ...
    e_oo, rotation, occs_oo = optimize_orbitals(
                                  bitstring_matrix,
                                  hcore=hcore,
                                  eri=eri,
                              )
    

Deprecation Notes

  • The qiskit_addon_sqd.fermion.bitstring_matrix_to_sorted_addresses() function has been deprecated in favor of qiskit_addon_sqd.fermion.bitstring_matrix_to_ci_strs(). These two functions behave the same with one key exception – qiskit_addon_sqd.fermion.bitstring_matrix_to_sorted_addresses() returns the configurations as tuple(spin_dn, spin_up); whereas, qiskit_addon_sqd.fermion.bitstring_matrix_to_ci_strs() returns the configurations as tuple(spin_up, spin_dn).

    To migrate

    from qiskit_addon_sqd.fermion import (
        bitstring_matrix_to_sorted_addresses,
        bitstring_matrix_to_ci_strs,
    )
    
    # DEPRECATED CODE
    bs_matrix = ...
    addr_dn, addr_up = bitstring_matrix_to_sorted_addresses(bs_matrix, open_shell=True)
    
    ### SHOULD BECOME ###
    
    # NEW CODE
    bs_matrix = ...
    ci_strs_up, ci_strs_dn = bitstring_matrix_to_ci_strs(bs_matrix, open_shell=True)
    
  • The addresses argument to qiskit_addon_sqd.fermion.solve_fermion() and qiskit_addon_sqd.fermion.optimize_orbitals() has been deprecated in favor of bitstring_matrix. Users are no longer required to convert their configurations to integers; instead, they should now pass in the bitstring matrix specifying the subspace onto which to project and diagonalize the Hamiltonian. The conversion to the integer representation of determinants will be done internally.

Bug Fixes

  • Fixes a bug that caused configuration recovery to fail on bitstrings of length greater than 72.

0.5.0

Upgrade Notes

  • The qiskit_addon_sqd.counts.generate_counts_bipartite_hamming(), qiskit_addon_sqd.subsampling.postselect_and_subsample(), and qiskit_addon_sqd.configuration_recovery.post_select_by_hamming_weight() now require the hamming_right and hamming_left arguments to be specified as keyword arguments. Additionally, the samples_per_batch and n_batches arguments to qiskit_addon_sqd.subsampling.postselect_and_subsample() should now be passed as keyword arguments.

    To upgrade

    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,
        num_batches,
    )
    

    should be changed to

    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, hamming_right=num_elec_a, hamming_left=num_elec_b)
    
    ...
    
    bs_mat = post_select_by_hamming_weight(bs_mat_full, hamming_right=num_elec_a, hamming_left=num_elec_b)
    
    ...
    
    batches = postselect_and_subsample(
        bs_mat,
        probs_arr,
        hamming_right=num_elec_a,
        hamming_left=num_elec_b,
        samples_per_batch=samples_per_batch,
        num_batches=num_batches,
    )
    

0.4.0

Prelude

This is a minor 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 Notes

  • The qiskit_addon_sqd.counts.generate_counts_bipartite_hamming(), qiskit_addon_sqd.subsampling.postselect_and_subsample(), and 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

    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

    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,
    )