What’s New in Version 2.0

Version 2.0 reworks the core data structures around a single symmetry-blocked storage model (see The Symmetry-Blocked Data Model), consolidates the wavefunction containers, and standardizes the fermion-to-qubit mapping workflow. Building on those foundations, it adds nuclear gradients and Hessians, more robust SCF convergence, and a broader set of quantum-simulation primitives: block-encoding / LCU qubitization, real-time dynamics for time-dependent Hamiltonians, and direct integration with the QDK Quantum Resource Estimator.

Most v1 code keeps working through tested facades and deprecated aliases; the Breaking changes and migration section covers the cases that need changes and the new APIs that replace them.

Highlights

  • Symmetry-blocked storage. Orbital coefficients and energies, Hamiltonian integrals, and index sets share one symmetry-blocked representation, so restricted and unrestricted data use the same accessors (discussed in The Symmetry-Blocked Data Model).

  • Consolidated wavefunction containers. The five v1 containers collapse into StateVectorContainer and AmplitudeContainer.

  • Nuclear gradients and Hessians. A new nuclear-derivative algorithm returns energies, gradients, and optionally a Hessian and the converged wavefunction.

  • More robust SCF. A stabilized SCF solver follows internal and external instabilities to a stable reference, and ROHF gains geometric direct minimization.

  • New simulation primitives. Block-encoding / LCU qubitization, the Hadamard test, Zassenhaus product formulas, and real-time dynamics for driven Hamiltonians join the existing Trotter-Suzuki builders.

  • Fermion-to-qubit mapping as data. Encodings are carried by MajoranaMapping, including a Verstraete-Cirac local encoding.

  • Explicit term grouping and expectation estimation. Pauli grouping is now an opt-in term_grouper step, and the estimator generalizes to any QubitOperator.

  • Resource estimation integration. Generated circuits hand off directly to the QDK Quantum Resource Estimator via Circuit.estimate() and Circuit.get_qre_application() (see Resource Estimation).

The Symmetry-Blocked Data Model

Orbital coefficients and energies, Hamiltonian integrals, and index sets are now backed by symmetry-blocked containers (SymmetryBlockedTensorRank2, SymmetryBlockedTensorRank1, and SymmetryBlockedIndexSet). Each block carries its spin (and, in general, other single-particle symmetries) explicitly, so one representation serves both restricted and unrestricted data, and a spin channel is selected with a SymmetryLabel.

The v1 dense accessors on Orbitals remain available as tested facades over this storage, but now emit a DeprecationWarning. See Migrating Orbitals accessors for the replacement API.

Consolidated Wavefunction Containers

The five v1 wavefunction containers were merged into two. The single-determinant, complete-active-space, and selected-configuration-interaction containers were structurally identical apart from a type tag, and are now the single StateVectorContainer, which stores a coefficient vector over a set of determinants and records the physical sector it describes. The Møller-Plesset and coupled-cluster containers are now the single AmplitudeContainer, which carries an AmplitudeType tag (moller_plesset or coupled_cluster) recording how its amplitudes should be interpreted. Containers of the same type are therefore distinguished by their stored contents rather than by their class.

The removed names remain importable as deprecated aliases, and get_container_type() now returns "state_vector" or "amplitude" instead of the former "sd" / "cas" / "sci" / "mp2" / "cc" strings. See Consolidated container names for the mapping.

Nuclear Gradients and Hessians

A new nuclear_derivative_calculator algorithm computes nuclear derivatives of the electronic energy directly from a Structure. Two variants are available: an analytic qdk path built on the internal SCF gradients, and a qdk_finite_difference path that evaluates central differences and can additionally assemble a Hessian.

# Analytic gradients
grad_calc = create("nuclear_derivative_calculator", "qdk")
energy, gradients, _hessian, wavefunction = grad_calc.run(
    structure, charge=0, spin_multiplicity=1, seed_or_basis="sto-3g"
)

# Numeric Hessian (central finite differences)
hess_calc = create(
    "nuclear_derivative_calculator", "qdk_finite_difference", compute_hessian=True
)
energy, gradients, hessian, wavefunction = hess_calc.run(
    structure, charge=0, spin_multiplicity=1, seed_or_basis="sto-3g"
)

The call returns the energy, the nuclear gradients, and optionally a Hessian and the converged wavefunction.

Stabilized SCF and ROHF Direct Minimization

A stabilized SCF solver wraps an inner solver and a stability checker: after convergence it tests the wavefunction for internal and external instabilities, rotates the orbitals along any instability direction, and re-runs until the reference is stable or an iteration limit is reached.

scf_solver = create("scf_solver", "qdk_stabilized")
energy, wavefunction = scf_solver.run(structure, 0, 1, "sto-3g")

Its behavior is controlled through settings (which stability checks to run, the iteration limit, and how to follow an external instability).

The QDK SCF backend also gains geometric direct minimization (GDM), including a DIIS-to-GDM hybrid path, for ROHF references that are difficult to converge with DIIS alone.

New Simulation Primitives

Several new primitives complement the existing Trotter-Suzuki time-evolution builders.

Block encoding / LCU. A linear-combination-of-unitaries (LCU) builder produces a PREPARE-SELECT block encoding of a Hermitian QubitOperator, and can wrap it as a qubitization walk operator for phase estimation:

builder = create("hamiltonian_unitary_builder", "lcu", quantum_walk=True)
walk = builder.run(qubit_operator)

Hadamard test. The hadamard_test algorithm (create("hadamard_test", "qdk")) maps a target UnitaryRepresentation into a controlled circuit and returns the measurement data for an overlap or expectation value.

Zassenhaus product formulas. A Zassenhaus builder expands the time-evolution operator with higher-order commutator corrections; the step count is set with num_divisions or selected from a target_accuracy:

builder = create(
    "hamiltonian_unitary_builder", "zassenhaus", order=2, num_divisions=4, time=1.0
)
unitary = builder.run(qubit_operator)

Real-time dynamics. Driven, time-dependent Hamiltonians (TimeDependentQubitHamiltonian) can be evolved with the Euler evolution-circuit builder (evolution_circuit_builder / euler) and a first-order Magnus propagator (propagator / magnus); a companion hamiltonian_simulation / euler_integrator measures observables on the evolved state. See examples/time_evolve_and_measure.ipynb and examples/estimation_ising_2d.ipynb.

Fermion-to-Qubit Mapping as Data

Fermion-to-qubit encodings are now first-class data. A MajoranaMapping describes how fermionic Majorana operators map to Pauli strings, and is passed to the qubit mapper at run() time instead of selecting an encoding by string:

mapping = MajoranaMapping.jordan_wigner(num_modes=n_spin_orbitals)
mapper = create("qubit_mapper", "qdk")
qubit_operator = mapper.run(hamiltonian, mapping)

Built-in constructors cover Jordan-Wigner, Bravyi-Kitaev, Bravyi-Kitaev tree, parity, and symmetry-conserving Bravyi-Kitaev encodings, plus a new Verstraete-Cirac local encoding built from a LatticeGraph for lattice models. The QDK mapper dispatches to specialized native paths for sparse (lattice-model) and Cholesky-factorized Hamiltonians, avoiding dense four-center expansion without changing results. See Fermion-to-qubit mapping for the migration from the v1 encoding-string workflow.

Term Grouping and Expectation Estimation

Pauli-term grouping is now an explicit term_grouper algorithm rather than an implicit step inside the estimator. A grouper attaches a TermPartition (a FlatPartition or LayeredPartition) to a QubitOperator for downstream consumers:

grouper = create("term_grouper", "qubit_wise_commuting")
grouped = grouper.run(qubit_operator)

Built-in variants are qubit_wise_commuting, commuting, and identity (one term per group); a NetworkX plugin adds graph-coloring variants. The ExpectationEstimator (registry key "expectation_estimator") estimates the expectation value of any QubitOperator from the grouping it is given; pre-group its input to recover the grouping v1 applied automatically.

Resource Estimation

Circuits produced by the circuit builders integrate directly with the QDK Quantum Resource Estimator. A Circuit exposes estimate(), which runs the estimator and returns an EstimatorResult, and get_qre_application(), which wraps the circuit as a qdk.qre application (Q#, OpenQASM, or QIR) for use with qdk.qre.estimate() and custom hardware and factory models. Resource estimation is enabled by the optional qre extra:

pip install 'qdk-chemistry[qre]'

Circuits from any of the circuit builders can be estimated this way. See examples/estimation_ising_2d.ipynb for an end-to-end dynamics resource-estimation walkthrough, including the impact of lattice-aware term grouping on fault-tolerant cost.

Other Improvements

  • Composable phase estimation. Standard and iterative QPE split into a reusable circuit builder and executor, composed via nested algorithm references; the builder can also generate circuits on its own for resource estimation (create("qpe_circuit_builder", "qdk_iterative")). See examples/qpe_stretched_n2.ipynb.

  • Natural-orbital localizer. orbital_localizer / qdk_natural_orbitals diagonalizes the active-space one-particle RDM to produce occupation-ordered natural orbitals.

  • Cholesky MO integrals persisted. Cholesky Hamiltonians now serialize their MO three-center integrals, so factorized Hamiltonians round-trip without expanding to dense tensors.

  • Declarative algorithm nesting. An algorithm can hold sub-algorithms as declarative (type, variant, settings) references in its Settings, resolved by the factory at run time.

  • Dense solver path for MACIS CAS. Complete-active-space calculations can use a dense eigensolver in addition to the iterative path.

  • Double-d-shell valence selection. Automated valence active-space selection can optionally include the double-d-shell orbitals important for transition-metal chemistry.

  • Qubit loss and richer noise profiles. The QDK sparse-state simulator accepts per-gate noise configuration, and QuantumErrorProfile adds a qubit-loss error type.

Breaking Changes and Migration

Migrating Orbitals accessors

Coefficients and energies are returned as symmetry-blocked tensors. Select a spin channel with a SymmetryLabel and read the block (for tensors) or the stored indices (for index sets) directly.

from qdk_chemistry.data.symmetry import SymmetryLabel, axes

# v1 (deprecated): tuple of dense alpha/beta arrays
alpha_c, beta_c = orbitals.get_coefficients()
alpha_e, beta_e = orbitals.get_energies()
alpha_active, beta_active = orbitals.get_active_space_indices()

# v2: symmetry-blocked tensors, selected per spin channel by SymmetryLabel
alpha = SymmetryLabel([axes.alpha()])
beta = SymmetryLabel([axes.beta()])
coefficients = orbitals.coefficients()          # SymmetryBlockedTensorRank2
energies = orbitals.energies()                  # SymmetryBlockedTensorRank1
alpha_c = coefficients.block([alpha, alpha])    # alpha block
beta_c = coefficients.block([beta, beta])       # beta block
alpha_e = energies.block([alpha])
alpha_active = orbitals.active_indices().indices(alpha)

For a model (symmetry-free) system the trivial label SymmetryLabel([]) selects the single block instead of an alpha/beta label. For the common case of counting active or inactive orbitals, take the length of the per-channel index list:

# v1 (deprecated)
n_active = len(orbitals.get_active_space_indices()[0])

# v2
n_active = len(orbitals.active_indices().indices(SymmetryLabel([axes.alpha()])))

Consolidated container names

The removed container names remain importable as deprecated aliases:

# v1 (deprecated alias, emits DeprecationWarning)
from qdk_chemistry.data import CasWavefunctionContainer

# v2
from qdk_chemistry.data import StateVectorContainer

SlaterDeterminantContainer, CasWavefunctionContainer, and SciWavefunctionContainer map to StateVectorContainer; MP2Container and CoupledClusterContainer map to AmplitudeContainer.

Two-particle RDM spin ordering (action required)

Warning

This is a silent change: the same accessor returns differently ordered data.

The Wavefunction spin-resolved two-particle RDM accessor get_active_two_rdm_spin_dependent() now returns its blocks as (aaaa, aabb, bbbb) (previously (aabb, aaaa, bbbb)), and the aabb block is now stored in alpha-alpha-beta-beta index order (previously alpha-beta-alpha-beta). This reorders the blocks to match the two-electron integral block order used throughout Hamiltonian (also (aaaa, aabb, bbbb) in alpha-alpha-beta-beta index order), so integrals and RDMs now share one spin-block convention. The Wavefunction constructor takes the blocks in the same new order. Update any positional unpacking:

# v1
aabb, aaaa, bbbb = wavefunction.get_active_two_rdm_spin_dependent()

# v2
aaaa, aabb, bbbb = wavefunction.get_active_two_rdm_spin_dependent()

Configuration factories

The string and bitset constructors were replaced by explicit factory methods:

from qdk_chemistry.data import Configuration

# v1
# config = Configuration("2020")

# v2
config = Configuration.from_spin_half_string("2020")

The Python factories are from_spin_half_string() and from_bitstring().

Fermion-to-qubit mapping

The qubit mapper no longer takes an encoding string. Build a MajoranaMapping and pass it to run():

# v1
# mapper = create("qubit_mapper", "qdk")  # configured via encoding="jordan-wigner"
# qubit_operator = mapper.run(hamiltonian)

# v2
mapping = MajoranaMapping.jordan_wigner(num_modes)
mapper = create("qubit_mapper", "qdk")
qubit_operator = mapper.run(hamiltonian, mapping)

The standalone EncodingMismatchError and validate_encoding_compatibility helpers were removed in favor of this mapping-centric flow; importing them still works but emits a DeprecationWarning.

Renamed classes and keys

The following names changed. The former names remain importable or usable as deprecated aliases that emit a DeprecationWarning:

  • QubitHamiltonianQubitOperator. The class now represents any weighted sum of Pauli strings, not only molecular Hamiltonians.

  • EnergyEstimator / QdkEnergyEstimatorExpectationEstimator / QdkExpectationEstimator, and the "energy_estimator" registry key → "expectation_estimator".

  • TimeEvolutionUnitaryUnitaryRepresentation, and TimeEvolutionUnitaryContainerUnitaryContainer.

Serialization format

Note

The following on-disk schemas were bumped from 0.1.0 to 0.2.0: Orbitals (and ModelOrbitals); the CanonicalFourCenterHamiltonianContainer, SparseHamiltonianContainer, and CholeskyHamiltonianContainer Hamiltonian containers; Wavefunction together with its StateVectorContainer; and UnitaryRepresentation. The AmplitudeContainer wavefunction container, the top-level Hamiltonian, and all other schemas are unchanged at 0.1.0. An affected file no longer loads directly from data written by an earlier release; upgrade it with the shipped converter (python -m qdk_chemistry.migrate old.hamiltonian.h5 new.hamiltonian.h5; see Migrating data files between serialization versions) or re-generate it.

C++ API changes

These changes affect the C++ library directly; Python users are insulated by the bindings above.

  • The redundant private Hamiltonian::_to_fcidump_file was removed (the public Hamiltonian::to_fcidump_file(...) remains), and HamiltonianContainer::to_fcidump_file became a concrete virtual.

  • The Cholesky Hamiltonian container now derives from HamiltonianContainer (no longer from the four-center container) and its dense constructor takes three-center integrals plus optional AO Cholesky vectors.

  • ModelOrbitals(size_t, bool) was replaced by a std::shared_ptr<const SymmetryProduct>-based constructor and a SymmetryBlockedIndexSet-based constructor.

  • The NuclearDerivativeCalculator run signature takes a single n_inactive_orbitals argument in place of the earlier active-electron pair.

Deprecation Policy

Deprecated v1 accessors and import aliases continue to work in this release and are covered by tests; using them emits a DeprecationWarning (for renamed classes such as QubitHamiltonian, on construction) to flag the migration path. They are transitional and scheduled for removal in a future release.

Bug Fixes

  • Fixed an int32 overflow in two-body integral and RDM indexing for large active spaces.

  • Added missing effective-core-potential (ECP) contributions in the affected integral path.

  • Fixed a bug in the SCF stability checker, and a separate ROHF stability-check bug in the solver.

Infrastructure and Packaging

  • Windows support. Native builds with MSVC / clang-cl via vcpkg, plus Windows CI coverage.

  • Algorithm result caching. Passing cache= to run() content-addresses a run and returns a stored result on a hit (FolderCache, TieredCache; force_rerun=True bypasses), enabling checkpoint/restart-style reuse across a pipeline.

  • Data-file migration tool. python -m qdk_chemistry.migrate old.hamiltonian.h5 new.hamiltonian.h5 (and qdk_chemistry.migrate.convert_file) upgrade serialized files across serialization versions and between JSON and HDF5.

  • Updated backend compatibility. Support for newer qdk (Q# 1.29+) API migrations and newer PyQIR versions.

  • Release examples bundle. The release pipeline packages the examples/ directory as a downloadable archive.