Skip to content

Dashboard Core — VQE

Real, dynamically-generated VQE ansatz circuits for molecular Hamiltonians — no fixed/hardcoded rotation angles. Every circuit this module returns is built from the actual molecule's own qubit count and Hamiltonian, run through dense_evolution.autodiff's gradient engine.

vqe

Real, dynamically-generated VQE ansatz circuits for molecular Hamiltonians -- no fixed/hardcoded rotation angles. Every circuit this module returns is produced by an actual classical optimization run against the real molecular Hamiltonian for the requested geometry/mapping, not a stored constant.

Two real ansatz families:

  • hardware-efficient (Kandala et al., Nature 2017): a Hartree-Fock computational-basis initial state, then n_layers of single-qubit RY rotations followed by a linear CNOT entangling ladder. Generic -- doesn't know anything about the molecule's own fermionic structure, just a NISQ-friendly template. Optimized entirely on dense_evolution's own engine: the ansatz is built as real OpenQASM, parsed with dense_evolution.QASMParser, and turned into a JAX-differentiable energy function via dense_evolution.autodiff.circuit_to_energy_fn (the exact pattern already used and tested in this project's own feature/streamlit-dashboard history, dashboard_core/vqe_engine.py -- reused here without its unrelated QM/MM-telemetry code, not reinvented). A hand-rolled Adam loop (jax.value_and_grad, jax.jit) optimizes it -- no PennyLane device/QNode/optimizer involved at all for this ansatz; PennyLane's only remaining role anywhere in this module is the real Hartree-Fock + Jordan-Wigner mapping itself (dashboard_core.hamiltonians), which isn't something worth reimplementing (see research/quantum_chemistry_vqe_pipeline.md). Verified to match the PennyLane-optimized version's convergence (same order of residual error against the exact energy, same physics).
  • UCCSD (Unitary Coupled-Cluster Singles and Doubles): the standard chemically-motivated VQE ansatz. Built from the molecule's real single/double fermionic excitation operators (qml.qchem.excitations), applied to the Hartree-Fock reference via qml.UCCSD (which internally exponentiates each excitation as a FermionicSingleExcitation / FermionicDoubleExcitation -- Givens-rotation-equivalent operators, not a generic template). Fewer parameters than hardware-efficient for the same molecule (H2: 3 vs 32), and converges to the exact energy faster because the ansatz form actually matches the physics. Also optimized entirely on dense_evolution's own engine, same as hardware-efficient -- the obstacle was that PennyLane's own decomposition of qml.UCCSD reuses each of the few real weights across several RX/RZ gates per excitation (Trotter exponentiation of that excitation's several Pauli-string terms), whereas circuit_to_energy_fn treats every parametric gate occurrence as an independent free parameter. Solved with an affine parameter expansion (_uccsd_native_expansion): probing PennyLane's own decomposition at weights=0 and at each basis vector gives a fixed (baseline, expansion_matrix) pair such that full_gate_values = baseline + expansion_matrix @ real_weights exactly reproduces PennyLane's own per-gate values for any weights (verified by direct probing, not derived from theory) -- composed with circuit_to_energy_fn this is still JAX-differentiable in the small real weight vector by ordinary chain rule, so the same hand-rolled Adam loop optimizes it with no PennyLane device/QNode/optimizer involved.

The Hartree-Fock initial state (computed via qml.qchem.hf_state) only has a simple X-gate encoding under the Jordan-Wigner mapping, so VQE generation here is JW-only. Bravyi-Kitaev stays available for exact ground-state-energy queries in hamiltonians.py, where the eigenvalue spectrum is mapping-invariant.

UCCSD's FermionicSingleExcitation/FermionicDoubleExcitation don't have a one-line OpenQASM equivalent, so the converged circuit is decomposed via PennyLane's own tape.expand() into RX/RY/RZ/CNOT (verified: executing the resulting QASM on dense_evolution.DenseSVSimulator and recomputing matches PennyLane's own reported energy to 1e-14, i.e. pure floating-point noise, not an approximation) and translated gate-by-gate.

run_vqe

run_vqe(
    symbols,
    geometry,
    charge=0,
    ansatz_type="hardware_efficient",
    n_layers=8,
    maxiter=200,
    step_size=0.1,
    beta1=0.9,
    beta2=0.999,
    active_electrons=None,
    active_orbitals=None,
    seed=0,
)

Runs a real VQE optimization (hand-rolled Adam over dense_evolution's own JAX-differentiable circuit_to_energy_fn, no PennyLane optimizer/device involved) for the molecule's Jordan-Wigner qubit Hamiltonian. step_size/beta1/beta2 are Adam's own real hyperparameters (learning rate and first/second moment decay), not cosmetic -- they change the real optimization trajectory computed below, the same way they would in any other Adam implementation. ansatz_type is "hardware_efficient" (generic, n_layers deep) or "uccsd" (chemically motivated, real fermionic single/double excitations -- n_layers is ignored, the parameter count comes from the molecule's own occupied/ virtual orbital structure). Returns a dict with the real energy convergence trace, the final variational energy, the exact ground- state energy (dense diagonalization -- feasible for every qubit count this function is meant to be called with, capped by the caller's active-space choice), and the OpenQASM circuit for the converged parameters.

maxiter=0 (or, for hardware_efficient, n_layers=0) is a real fast path, not a special case faked up separately: with zero ansatz parameters there's nothing for Adam to optimize, so this returns the bare Hartree-Fock reference circuit and its (real, exact) HF energy immediately -- the "pick a molecule, get a circuit" mechanic the UI uses before committing to a minutes-long optimization.

Source code in tools/dashboard_core/vqe.py
def run_vqe(symbols, geometry, charge=0, ansatz_type="hardware_efficient", n_layers=8, maxiter=200,
            step_size=0.1, beta1=0.9, beta2=0.999, active_electrons=None, active_orbitals=None, seed=0):
    """Runs a real VQE optimization (hand-rolled Adam over
    dense_evolution's own JAX-differentiable circuit_to_energy_fn, no
    PennyLane optimizer/device involved) for the molecule's
    Jordan-Wigner qubit Hamiltonian. step_size/beta1/beta2 are Adam's own
    real hyperparameters (learning rate and first/second moment decay),
    not cosmetic -- they change the real optimization trajectory computed
    below, the same way they would in any other Adam implementation.
    ansatz_type is
    "hardware_efficient" (generic, n_layers deep) or "uccsd" (chemically
    motivated, real fermionic single/double excitations -- n_layers is
    ignored, the parameter count comes from the molecule's own occupied/
    virtual orbital structure). Returns a dict with the real energy
    convergence trace, the final variational energy, the exact ground-
    state energy (dense diagonalization -- feasible for every qubit
    count this function is meant to be called with, capped by the
    caller's active-space choice), and the OpenQASM circuit for the
    converged parameters.

    maxiter=0 (or, for hardware_efficient, n_layers=0) is a real fast
    path, not a special case faked up separately: with zero ansatz
    parameters there's nothing for Adam to optimize, so this returns the
    bare Hartree-Fock reference circuit and its (real, exact) HF energy
    immediately -- the "pick a molecule, get a circuit" mechanic the UI
    uses before committing to a minutes-long optimization."""
    import pennylane as qml

    H, n_qubits = _get_pennylane_hamiltonian(symbols, geometry, charge, "jordan_wigner",
                                              active_electrons, active_orbitals)

    # Molecule() is cheap (~15ms, no HF solve) -- just needed here for its
    # real total-electron count, to pick the right Hartree-Fock occupation
    # when the caller didn't already fix it via an active-space choice.
    if active_electrons is not None:
        electrons = active_electrons
    else:
        molecule = qml.qchem.Molecule(symbols, np.asarray(geometry), charge=charge, unit="angstrom")
        electrons = molecule.n_electrons
    hf_occupation = qml.qchem.hf_state(electrons, n_qubits)

    s_wires = d_wires = None
    if ansatz_type == "uccsd":
        s_wires, d_wires = _uccsd_excitations(electrons, n_qubits)
        n_params = len(s_wires) + len(d_wires)
    else:
        n_params = n_qubits * n_layers

    rng = np.random.default_rng(seed)

    if n_params == 0 or maxiter == 0:
        dev = qml.device("lightning.qubit", wires=n_qubits)

        @qml.qnode(dev)
        def hf_energy_fn():
            _hardware_efficient_ansatz(np.zeros(0), n_qubits, 0, hf_occupation)
            return qml.expval(H)

        final_energy = float(hf_energy_fn())
        energy_history = [final_energy]
        params = np.zeros(0)
        n_layers = 0
        n_params = 0
    elif ansatz_type == "uccsd":
        import jax
        import jax.numpy as jnp

        H_dense, _ = build_molecular_hamiltonian(symbols, geometry, charge, "jordan_wigner",
                                                   active_electrons, active_orbitals)
        qasm_structure, baseline, expansion_matrix = _uccsd_native_expansion(
            n_qubits, s_wires, d_wires, hf_occupation, n_params,
        )
        parsed = de.QASMParser().parse(qasm_structure)
        energy_fn_full, n_params_full = de.circuit_to_energy_fn(parsed, n_qubits)
        if n_params_full != len(baseline):
            # BUG FIX: was `assert`, silently stripped under python -O,
            # letting a UCCSD/circuit_to_energy_fn desync through to a
            # shape mismatch far downstream instead of failing here with
            # a clear cause.
            raise ValueError(
                f"circuit_to_energy_fn found {n_params_full} parametric gates, "
                f"expected {len(baseline)} from the UCCSD decomposition probe"
            )

        h_matrix = jnp.array(H_dense)
        baseline_jax = jnp.array(baseline)
        expansion_matrix_jax = jnp.array(expansion_matrix)

        def real_energy_fn(real_theta, h_mat):
            theta_full = baseline_jax + expansion_matrix_jax @ real_theta
            return energy_fn_full(theta_full, h_mat)

        theta = jnp.array(rng.uniform(-0.1, 0.1, size=n_params))
        m_moment = jnp.zeros(n_params)
        v_moment = jnp.zeros(n_params)
        eps = 1e-8
        energy_and_grad = jax.jit(jax.value_and_grad(real_energy_fn, argnums=0, has_aux=True))

        energy_history = []
        for t in range(1, maxiter + 1):
            (energy, _sv), grad = energy_and_grad(theta, h_matrix)
            m_moment = beta1 * m_moment + (1 - beta1) * grad
            v_moment = beta2 * v_moment + (1 - beta2) * (grad ** 2)
            m_hat = m_moment / (1 - beta1 ** t)
            v_hat = v_moment / (1 - beta2 ** t)
            theta = theta - step_size * m_hat / (jnp.sqrt(v_hat) + eps)
            energy_history.append(float(energy))
        final_energy_jax, _sv_final = real_energy_fn(theta, h_matrix)
        final_energy = float(final_energy_jax)
        energy_history.append(final_energy)
        params = np.asarray(theta)
    else:
        import jax
        import jax.numpy as jnp

        H_dense, _ = build_molecular_hamiltonian(symbols, geometry, charge, "jordan_wigner",
                                                   active_electrons, active_orbitals)
        qasm_template = _hardware_efficient_qasm(np.zeros(n_params), n_qubits, n_layers, hf_occupation)
        parsed = de.QASMParser().parse(qasm_template)
        energy_fn, n_params_native = de.circuit_to_energy_fn(parsed, n_qubits)
        if n_params_native != n_params:
            # BUG FIX: was `assert`, silently stripped under python -O.
            raise ValueError(
                f"circuit_to_energy_fn found {n_params_native} parametric gates, expected {n_params}"
            )

        h_matrix = jnp.array(H_dense)
        theta = jnp.array(rng.uniform(-0.1, 0.1, size=n_params))
        m_moment = jnp.zeros(n_params)
        v_moment = jnp.zeros(n_params)
        eps = 1e-8
        energy_and_grad = jax.jit(jax.value_and_grad(energy_fn, argnums=0, has_aux=True))

        energy_history = []
        for t in range(1, maxiter + 1):
            (energy, _sv), grad = energy_and_grad(theta, h_matrix)
            m_moment = beta1 * m_moment + (1 - beta1) * grad
            v_moment = beta2 * v_moment + (1 - beta2) * (grad ** 2)
            m_hat = m_moment / (1 - beta1 ** t)
            v_hat = v_moment / (1 - beta2 ** t)
            theta = theta - step_size * m_hat / (jnp.sqrt(v_hat) + eps)
            energy_history.append(float(energy))
        final_energy_jax, _sv_final = energy_fn(theta, h_matrix)
        final_energy = float(final_energy_jax)
        energy_history.append(final_energy)
        params = np.asarray(theta)

    exact_energy = None
    dim = 2 ** n_qubits
    if dim <= 4096:  # dense diagonalization budget: 4096^2 complex128 = 128 MB
        if ansatz_type == "hardware_efficient" and n_params > 0:
            exact_energy = float(np.linalg.eigvalsh(H_dense).min())
        else:
            H_dense_check, _ = build_molecular_hamiltonian(symbols, geometry, charge, "jordan_wigner",
                                                             active_electrons, active_orbitals)
            exact_energy = float(np.linalg.eigvalsh(H_dense_check).min())

    if n_params == 0:
        qasm = _hardware_efficient_qasm(params, n_qubits, 0, hf_occupation)
    elif ansatz_type == "uccsd":
        qasm = _uccsd_tape_to_qasm(params, n_qubits, s_wires, d_wires, hf_occupation)
    else:
        qasm = _hardware_efficient_qasm(params, n_qubits, n_layers, hf_occupation)

    return {
        'n_qubits': n_qubits,
        'ansatz_type': ansatz_type if n_params > 0 else 'hartree_fock',
        'n_layers': n_layers if ansatz_type != "uccsd" else None,
        'n_params': n_params,
        'hf_occupation': [int(b) for b in hf_occupation],
        'energy_history': energy_history,
        'vqe_energy_hartree': final_energy,
        'exact_energy_hartree': exact_energy,
        'qasm': qasm,
    }

See also: dashboard_core.hamiltonians for where the molecular Hamiltonian this ansatz optimizes against comes from.