Operator averaging for molecular HamiltoniansΒΆ

Download Notebook - operator_averaging.ipynb

"""Example showing operator averaging on the H2 STO-3G Hartree-Fock state."""

from __future__ import annotations

from typing import no_type_check

import zixy.qubit.pauli as zqp
from zixy.fermion.mappings import JordanWignerMapper
from zixy.fermion.operator.general import String as FermionString
from guppylang import guppy
from guppylang.std.builtins import array, comptime
from guppylang.std.quantum import discard_array, qubit, x

from guppyalgos.primitives.measurement import (
    estimate_expectation_from_binary_samples,
    estimate_pauli_observable_expectation_from_binary_samples,
    make_hadamard_test_pauli,
)
from guppyalgos.utils import qarray
H2_STO3G_HF_ENERGY = -1.1175058842043306
H2_STO3G_XY_SHOTS = 512
H2_STO3G_SHOTS = 64
H2_HAM_QUBITS = 4 # for parsing paulis
H2_TOTAL_QUBITS = H2_HAM_QUBITS + 1  # for emulator

H2_STO3G_FERMION_OPERATOR_ZIXY = [
    # is mode-indexed
    (0.7430177069924181, ()),
    (-1.2702927243904383, ((0, 1), (0, 0))),  # F0^ F0
    (-1.2702927243904380, ((1, 1), (1, 0))),
    (-0.45680735030941033, ((2, 1), (2, 0))),
    (-0.45680735030941020, ((3, 1), (3, 0))),
    (0.6800618575841273, ((0, 1), (0, 0), (1, 1), (1, 0))),  # F0^ F0 F1^ F1
    (0.48890859745047305, ((0, 1), (0, 0), (2, 1), (2, 0))),
    (0.6685772770134887, ((0, 1), (0, 0), (3, 1), (3, 0))),
    (0.6685772770134887, ((1, 1), (1, 0), (2, 1), (2, 0))),
    (0.48890859745047305, ((1, 1), (1, 0), (3, 1), (3, 0))),
    (0.7028135332762804, ((2, 1), (2, 0), (3, 1), (3, 0))),
    (-0.35933735912603115, ((0, 1), (1, 1), (2, 0), (3, 0))),  # F0^ F1^ F2 F3
    (-0.35933735912603115, ((0, 1), (1, 0), (3, 1), (2, 0))),  # F0^ F1 F3^ F2
]
@guppy
@no_type_check
def prepare_h2_sto3g_hf(qreg: array[qubit, comptime(4)]) -> None:
    """Prepare the H2 STO-3G Hartree-Fock reference state.

    The spin-orbitals are ordered from lowest to highest orbital energy,
    matching `H2_STO3G_FERMION_OPERATOR_ZIXY`. Modes 0 and 1 are occupied.
    """
    x(qreg[0])
    x(qreg[1])


def build_hadamard_program(pauli_string: zqp.String, size: int, prepare_state):
    hadamard_test_pauli = make_hadamard_test_pauli(pauli_string, size)

    @guppy
    @no_type_check
    def program() -> None:
        qreg = qarray(comptime(size))
        prepare_state(qreg)
        hadamard_test_pauli(qreg)
        discard_array(qreg)

    return program
def _shots_for_term(term: zqp.RealTerm) -> int:
    """Use a larger shot budget for the off-diagonal exchange terms."""
    if any(label in (zqp.X, zqp.Y) for label in term.string.get_dict().values()):
        return H2_STO3G_XY_SHOTS
    return H2_STO3G_SHOTS
"""Run the operator averaging demo."""
single_pauli = zqp.String.from_str("Z0", H2_HAM_QUBITS)
single_program = build_hadamard_program(
    single_pauli,
    H2_HAM_QUBITS,
    prepare_h2_sto3g_hf,
)
result = single_program.emulator(n_qubits=H2_TOTAL_QUBITS).with_shots(10).run()
ancilla_samples = [bool(sample["ancilla"][0]) for sample in result.collated_shots()]
single_estimate = estimate_expectation_from_binary_samples(ancilla_samples)

print(
    f"<{single_pauli}> on HF 1100 = {single_estimate.expectation:.6f} "
    f"+/- {single_estimate.standard_error:.6f}"
)
<Z0> on HF 1100 = -1.000000 +/- 0.000000
jw = JordanWignerMapper(H2_HAM_QUBITS, mode_ordering=None)
h2_ham_op = zqp.RealTermSum.from_str("(0.0, I0)", H2_HAM_QUBITS)

for coeff, ops in H2_STO3G_FERMION_OPERATOR_ZIXY:
    if len(ops) == 0:
        h2_ham_op += zqp.RealTermSum.from_str(f"({coeff}, I0)", H2_HAM_QUBITS)
    else:
        fermion_string = FermionString(
            H2_HAM_QUBITS, [(mode, bool(adj)) for mode, adj in ops]
        )
        h2_ham_op += jw.encode_real(fermion_string, coeff)

operator = h2_ham_op

#print(operator)
operator_samples: dict[str, list[bool]] = {}

for term in operator.to_terms():
    if term.string.is_identity():
        continue

    term_program = build_hadamard_program(
        term.string,
        H2_HAM_QUBITS,
        prepare_h2_sto3g_hf,
    )
    result = term_program.emulator(n_qubits=H2_TOTAL_QUBITS).with_shots(_shots_for_term(term)).run()
    operator_samples[str(term.string)] = [bool(sample["ancilla"][0]) for sample in result.collated_shots()]


operator_estimate = estimate_pauli_observable_expectation_from_binary_samples(operator, operator_samples)
operator_estimate.print_terms()
print(
    f"\nH2 STO-3G energy on HF 1100 = {operator_estimate.expectation:.6f} "
    f"+/- {operator_estimate.standard_error:.6f}"
)
print(f"Reference HF energy = {H2_STO3G_HF_ENERGY:.12f}")
term                exp         var      stderr  shots     +     -
I             +1.000000    0.000000    0.000000      0     0     0
X0 X1 Y2 Y3   +0.023438    0.001952    0.044182    512   262   250
X0 Y1 Y2 X3   -0.027344    0.001952    0.044178    512   249   263
Y0 X1 X2 Y3   +0.019531    0.001952    0.044186    512   261   251
Y0 Y1 X2 X3   +0.031250    0.001951    0.044173    512   264   248
Z0            -1.000000    0.000000    0.000000     64     0    64
Z0 Z1         +1.000000    0.000000    0.000000     64    64     0
Z0 Z2         -1.000000    0.000000    0.000000     64     0    64
Z0 Z3         -1.000000    0.000000    0.000000     64     0    64
Z1            -1.000000    0.000000    0.000000     64     0    64
Z1 Z2         -1.000000    0.000000    0.000000     64     0    64
Z1 Z3         -1.000000    0.000000    0.000000     64     0    64
Z2            +1.000000    0.000000    0.000000     64    64     0
Z2 Z3         +1.000000    0.000000    0.000000     64    64     0
Z3            +1.000000    0.000000    0.000000     64    64     0

H2 STO-3G energy on HF 1100 = -1.120313 +/- 0.003969
Reference HF energy = -1.117505884204