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.

Quick start

pip install dense-evolution[pennylane]
from dashboard_core.vqe import run_vqe

result = run_vqe(
    symbols=["H", "H"],
    geometry=[[0, 0, 0], [0, 0, 0.7414]],
    ansatz_type="hardware_efficient",
    n_layers=4,
    maxiter=200,
)
print(result["vqe_energy_hartree"])  # -1.137270 (verified against exact_energy_hartree)

run_vqe needs the optional pennylane extra (used internally to build the molecular Hamiltonian and Hartree-Fock reference state — the native UCCSD ansatz circuits themselves don't need PennyLane, see dense_evolution.circuits.uccsd, but Hamiltonian construction still does). ansatz_type="uccsd" uses that native implementation instead of "hardware_efficient"'s generic layered ansatz; n_layers is then ignored, since UCCSD's parameter count comes from the molecule's own occupied/virtual orbital structure.

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 (dense_evolution.find_excitations -- pure combinatorics, verified to reproduce qml.qchem.excitations exactly), applied to the Hartree-Fock reference via dense_evolution.single_excitation_ops/ double_excitation_ops -- exact closed-form circuits derived directly against dense_evolution's own Jordan-Wigner mapping (physics.fermions.majorana_pauli_terms), not PennyLane's decomposition; see dense_evolution/circuits/uccsd.py for the derivation and the exact scope of the closed form vs. its (also verified exact) per-term fallback. 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 these excitation circuits reuse the same weight across several RY/RZ gates per excitation (single_excitation_ops' CRY is 2 RY gates; double_excitation_ops' per-term path is up to 8 RZ gates), 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 _uccsd_native_ops 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 the real 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, or PennyLane import of any kind, 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.

PennyLane's only remaining role anywhere in this module is the real Hartree-Fock + Jordan-Wigner Hamiltonian construction itself (dashboard_core.hamiltonians), which isn't something worth reimplementing (see research/quantum_chemistry_vqe_pipeline.md) -- the ansatz circuits themselves (hardware-efficient and UCCSD alike) never touch PennyLane at all.

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.

Requires the optional pennylane extra (used internally to build the molecular Hamiltonian and Hartree-Fock reference state -- the native UCCSD ansatz circuits themselves, see dense_evolution.circuits.uccsd, do not need PennyLane, but Hamiltonian construction still does): pip install dense-evolution[pennylane].

Parameters:

Name Type Description Default
symbols list of str

Atomic symbols, e.g. ["H", "H"].

required
geometry list of [float, float, float]

Cartesian coordinates in Angstrom, one triplet per atom, same order as symbols.

required
charge int

Molecular charge. Defaults to 0.

0
ansatz_type str

"hardware_efficient" (generic, n_layers deep) or "uccsd" (chemically motivated; n_layers is ignored, the parameter count comes from the molecule's own occupied/virtual orbital structure). Defaults to "hardware_efficient".

'hardware_efficient'
n_layers int

Ansatz depth (hardware_efficient only). Defaults to 8.

8
maxiter int

Adam iterations. Defaults to 200.

200
step_size float

Adam hyperparameters (learning rate, first/second moment decay).

0.1
beta1 float

Adam hyperparameters (learning rate, first/second moment decay).

0.1
beta2 float

Adam hyperparameters (learning rate, first/second moment decay).

0.1
active_electrons int

Active-space restriction, forwarded to PennyLane's Hamiltonian builder. Defaults to the molecule's full space.

None
active_orbitals int

Active-space restriction, forwarded to PennyLane's Hamiltonian builder. Defaults to the molecule's full space.

None
seed int

RNG seed for the initial ansatz parameters. Defaults to 0.

0

Returns:

Type Description
dict

vqe_energy_hartree (final variational energy), exact_energy_hartree (dense-diagonalization ground state, for comparison), energy_history (per-iteration trace), qasm (the converged circuit as OpenQASM 2.0), plus n_qubits, n_params, ansatz_type, n_layers, hf_occupation.

Examples:

>>> from dashboard_core.vqe import run_vqe
>>> result = run_vqe(
...     symbols=["H", "H"],
...     geometry=[[0, 0, 0], [0, 0, 0.7414]],
...     ansatz_type="hardware_efficient",
...     n_layers=4,
...     maxiter=200,
... )
>>> round(result["vqe_energy_hartree"], 4)
-1.1373
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.

    Requires the optional `pennylane` extra (used internally to build the
    molecular Hamiltonian and Hartree-Fock reference state -- the native
    UCCSD ansatz circuits themselves, see `dense_evolution.circuits.uccsd`,
    do not need PennyLane, but Hamiltonian construction still does):
    `pip install dense-evolution[pennylane]`.

    Parameters
    ----------
    symbols : list of str
        Atomic symbols, e.g. `["H", "H"]`.
    geometry : list of [float, float, float]
        Cartesian coordinates in Angstrom, one triplet per atom, same
        order as `symbols`.
    charge : int, optional
        Molecular charge. Defaults to 0.
    ansatz_type : str, optional
        `"hardware_efficient"` (generic, `n_layers` deep) or `"uccsd"`
        (chemically motivated; `n_layers` is ignored, the parameter count
        comes from the molecule's own occupied/virtual orbital
        structure). Defaults to `"hardware_efficient"`.
    n_layers : int, optional
        Ansatz depth (`hardware_efficient` only). Defaults to 8.
    maxiter : int, optional
        Adam iterations. Defaults to 200.
    step_size, beta1, beta2 : float, optional
        Adam hyperparameters (learning rate, first/second moment decay).
    active_electrons, active_orbitals : int, optional
        Active-space restriction, forwarded to PennyLane's Hamiltonian
        builder. Defaults to the molecule's full space.
    seed : int, optional
        RNG seed for the initial ansatz parameters. Defaults to 0.

    Returns
    -------
    dict
        `vqe_energy_hartree` (final variational energy),
        `exact_energy_hartree` (dense-diagonalization ground state, for
        comparison), `energy_history` (per-iteration trace), `qasm` (the
        converged circuit as OpenQASM 2.0), plus `n_qubits`, `n_params`,
        `ansatz_type`, `n_layers`, `hf_occupation`.

    Examples
    --------
    >>> from dashboard_core.vqe import run_vqe
    >>> result = run_vqe(
    ...     symbols=["H", "H"],
    ...     geometry=[[0, 0, 0], [0, 0, 0.7414]],
    ...     ansatz_type="hardware_efficient",
    ...     n_layers=4,
    ...     maxiter=200,
    ... )
    >>> round(result["vqe_energy_hartree"], 4)  # doctest: +SKIP
    -1.1373
    """
    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)

    singles = doubles = None
    if ansatz_type == "uccsd":
        singles, doubles = _uccsd_excitations(electrons, n_qubits)
        n_params = len(singles) + len(doubles)
    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, singles, doubles, 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
        # Both the uccsd and hardware_efficient-with-params branches above
        # already built H_dense with these exact same arguments -- only
        # the n_params==0 (Hartree-Fock-only, no ansatz built at all)
        # branch never did. Reusing it there instead of a second
        # "H_dense_check" call was previously a redundant cache lookup
        # every time, not a bug (build_molecular_hamiltonian is cached,
        # so it returned the identical matrix either way), but confusing
        # flow (prog.txt, dashboard_core audit point 3c).
        if n_params == 0:
            H_dense, _ = build_molecular_hamiltonian(symbols, geometry, charge, "jordan_wigner",
                                                       active_electrons, active_orbitals)
        exact_energy = float(np.linalg.eigvalsh(H_dense).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, singles, doubles, 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,
        'params': params.tolist(),
    }

scan_hardware_efficient_energy_landscape

scan_hardware_efficient_energy_landscape(
    symbols,
    geometry,
    charge,
    n_layers,
    hf_occupation,
    base_params,
    param_i,
    param_j,
    values_i,
    values_j,
    active_electrons=None,
    active_orbitals=None,
)

Real 2D energy-landscape scan around a converged hardware_efficient VQE result: re-evaluates on dense_evolution's own circuit_to_energy_fn for every (values_i, values_j) grid point, holding every parameter except param_i/param_j fixed at its converged value from base_params -- the same real Hamiltonian and ansatz circuit run_vqe itself used for this molecule, not a separate or approximate model.

hardware_efficient only: UCCSD's parameter space is an affine expansion over full per-gate values (_uccsd_native_expansion), not a direct one-parameter-per-rotation-gate mapping, so "parameter i" doesn't correspond to a single rotation angle the way it does here.

Returns a (len(values_i), len(values_j)) numpy array of energies in Hartree.

Source code in tools/dashboard/core/vqe.py
def scan_hardware_efficient_energy_landscape(
    symbols, geometry, charge, n_layers, hf_occupation, base_params,
    param_i, param_j, values_i, values_j,
    active_electrons=None, active_orbitals=None,
):
    """Real 2D energy-landscape scan around a converged hardware_efficient
    VQE result: re-evaluates <psi(theta)|H|psi(theta)> on
    dense_evolution's own circuit_to_energy_fn for every (values_i,
    values_j) grid point, holding every parameter except param_i/param_j
    fixed at its converged value from base_params -- the same real
    Hamiltonian and ansatz circuit run_vqe itself used for this molecule,
    not a separate or approximate model.

    hardware_efficient only: UCCSD's parameter space is an affine
    expansion over full per-gate values (_uccsd_native_expansion), not a
    direct one-parameter-per-rotation-gate mapping, so "parameter i"
    doesn't correspond to a single rotation angle the way it does here.

    Returns a (len(values_i), len(values_j)) numpy array of energies in
    Hartree.
    """
    import jax
    import jax.numpy as jnp

    n_qubits = len(hf_occupation)
    n_params = n_qubits * n_layers
    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:
        raise ValueError(
            f"circuit_to_energy_fn found {n_params_native} parametric gates, expected {n_params}"
        )

    h_matrix = jnp.array(H_dense)
    base = jnp.array(base_params)
    energy_fn_jit = jax.jit(energy_fn)

    energies = np.zeros((len(values_i), len(values_j)))
    for a, vi in enumerate(values_i):
        theta_a = base.at[param_i].set(vi)
        for b, vj in enumerate(values_j):
            theta_ab = theta_a.at[param_j].set(vj)
            energy, _sv = energy_fn_jit(theta_ab, h_matrix)
            energies[a, b] = float(energy)
    return energies

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