Skip to main content
IBM Quantum Platform

Hamiltonianos para Química Quântica

Vamos começar com uma breve visão geral da função que os hamiltonianos desempenham na VQE.


O Hamiltoniano em VQE Visão geral

A Dra. Victoria Lipinska nos apresenta os Hamiltonianos e como mapeá-los para uso em computação quântica.

Referências

Os artigos a seguir são mencionados no vídeo acima.


Preparando os hamiltonianos para a química quântica

Uma boa primeira etapa para aplicar a computação quântica a um problema químico é definir um Hamiltoniano para o sistema de interesse. Aqui, restringiremos a discussão aos Hamiltonianos da química quântica, pois esses Hamiltonianos exigem algum mapeamento específico para sistemas de férmions idênticos.

Como alguém que trabalha com química quântica, você provavelmente já tem seu software favorito para modelagem de moléculas, que pode gerar um Hamiltoniano que descreve seu sistema de interesse. Aqui, usaremos o código criado exclusivamente com o PySCF, numpy e o Qiskit. Mas o processo de preparação hamiltoniana também é transferido para soluções pré-embaladas. A única diferença entre essa abordagem e outros softwares serão pequenas diferenças de sintaxe; algumas delas são abordadas na subseção "Software de terceiros" para facilitar a integração de fluxos de trabalho existentes.

A geração de um Hamiltoniano de química quântica para uso em IBM Quantum® QPUs envolve as seguintes etapas:

  1. Defina sua molécula (geometria, spin, espaço ativo, etc.)
  2. Gerar o Hamiltoniano fermiônico (operadores de criação e aniquilação)
  3. Mapa do Hamiltoniano fermiônico para um operador bosônico (nesse contexto, usando operadores de Pauli)
  4. Se estiver usando um software de terceiros: trate de quaisquer incompatibilidades de sintaxe entre o software gerador e o Qiskit

O Hamiltoniano fermiônico é escrito em termos de operadores fermiônicos e, em particular, leva em conta que os elétrons são férmions indistinguíveis. Isso significa que eles obedecem a estatísticas completamente diferentes dos qubits distinguíveis e bosônicos. Daí o processo de mapeamento.

Aqueles que já estão familiarizados com esses processos provavelmente podem pular esta seção.

Meta:

O objetivo final é obter um Hamiltoniano da forma:

H = [(1, "XX"), (1, "YY"), (1, "ZZ")]
print(H)

Output:

[(1, 'XX'), (1, 'YY'), (1, 'ZZ')]

Ou

from qiskit.quantum_info import SparsePauliOp

H = SparsePauliOp(["XX", "YY", "ZZ"], coeffs=[1.0 + 0.0j, 1.0 + 0.0j, 1.0 + 0.0j])
print(H)

Output:

SparsePauliOp(['XX', 'YY', 'ZZ'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j])

Começaremos importando alguns pacotes:

import numpy as np
from pyscf import ao2mo, gto, mcscf, scf
  1. Defina sua molécula

Aqui, especificaremos os atributos da molécula de interesse. Neste exemplo, escolhemos o hidrogênio diatômico (porque os Hamiltonianos resultantes são curtos o suficiente para serem exibidos).

O Python -based Simulations of Chemistry Framework ( PySCF ) tem uma ampla coleção de módulos de estrutura eletrônica que podem ser usados para, entre outras coisas, gerar Hamiltonianos moleculares adequados para a computação quântica. O guia de início rápido do site PySCF é um excelente recurso para uma descrição completa de todas as variáveis e funcionalidades. Daremos apenas uma visão geral superficial, pois isso já é familiar para muitos de vocês. Para entendê-los melhor, visite PySCF. Resumidamente:

a distância pode ser usada para moléculas diatômicas, ou simplesmente especificar coordenadas cartesianas para cada átomo. As distâncias estão em unidades de Angstrom.

gpara gerar orbitais do tipo gaussiano.

base refere-se às funções usadas para modelar orbitais moleculares. Aqui, ' sto-6g ' é uma base mínima comum, nomeada para ajustar os orbitais do tipo Slater usando 6 orbitais Gaussianos primitivos.

spin um valor inteiro que indica o número de elétrons desemparelhados (igual a 2S2S ). Observe que alguns softwares usam a multiplicidade em vez disso ( 2S+12S+1 ).

carga a carga da molécula.

symmetry (simetria ) - o grupo de simetria de ponto da molécula, especificado com uma cadeia de caracteres ou detectado automaticamente pela configuração "symmetry = True". Aqui, "Dooh" é o grupo de simetria apropriado para moléculas diatômicas com duas espécies do mesmo átomo.

distance = 0.735
a = distance / 2
mol = gto.Mole()
mol.build(
    verbose=0,
    atom=[
        ["H", (0, 0, -a)],
        ["H", (0, 0, a)],
    ],
    basis="sto-6g",
    spin=0,
    charge=0,
    symmetry="Dooh",
)

Output:

<pyscf.gto.mole.Mole at 0x7fc718f07610>

Lembre-se de que é possível descrever a energia total (que inclui a energia de repulsão nuclear e a eletrônica), a energia orbital eletrônica total ou a energia de algum subconjunto de orbitais eletrônicos (com o subconjunto complementar congelado). No caso específico de H2\text{H}_2, observe as diferentes energias abaixo e note que a energia total menos a energia de repulsão nuclear produz, de fato, a energia eletrônica:

mf = scf.RHF(mol)
mf.scf()

print(
    mf.energy_nuc(),
    mf.energy_elec()[0],
    mf.energy_tot(),
    mf.energy_tot() - mol.energy_nuc(),
)

Output:

0.7199689944489797 -1.8455976628764188 -1.125628668427439 -1.8455976628764188
active_space = range(mol.nelectron // 2 - 1, mol.nelectron // 2 + 1)
  1. Gerar Hamiltoniano fermiônico

scf refere-se a uma ampla gama de métodos de campo autoconsistentes.

rhf como em mf = scf.RHF (mol) em mf é um solucionador que usa o cálculo de Hartree Fock restrito. O núcleo disso (E, abaixo) é a energia total, incluindo a repulsão nuclear e os orbitais moleculares.

o mcscf é um pacote de campos autoconsistentes de várias configurações.

ao2mo é uma transformação de orbitais atômicos em orbitais moleculares.

Também usamos as seguintes variáveis:

ncas : número de orbitais no espaço ativo completo

nelecas : número de elétrons no espaço ativo completo

E1 = mf.kernel()
mx = mcscf.CASCI(mf, ncas=2, nelecas=(1, 1))
mo = mx.sort_mo(active_space, base=0)
E2 = mx.kernel(mo)[:2]

Queremos um Hamiltoniano, e isso geralmente é separado em energia de um núcleo eletrônico (ecore, não envolvido na minimização), operadores de elétron único ( h1e ) e energias de dois elétrons ( h2e ). Elas são explicitamente extraídas abaixo nas duas últimas linhas.

h1e, ecore = mx.get_h1eff()
h2e = ao2mo.restore(1, mx.get_h2eff(), mx.ncas)

Atualmente, esses hamiltonianos são operadores fermiônicos (criação e aniquilação), aplicáveis a sistemas de férmions (indistinguíveis) e, consequentemente, sujeitos à antissimetria sob troca. Isso resulta em uma estática diferente da que se aplicaria a um sistema distinguível ou bosônico. Para executar cálculos em IBM Quantum QPUs, precisamos de um operador bosônico que descreva a energia. O resultado desse mapeamento é convencionalmente escrito em termos de operadores Pauli, já que eles são hermitianos e unitários. Há vários mapeamentos que podem ser usados. Uma das mais simples é a transformação de Jordan Wigner.

  1. Mapeamento do Hamiltoniano

Deve-se observar que há muitas ferramentas disponíveis para mapear um Hamiltoniano químico para um adequado para execução em um computador quântico. Aqui, implementamos o mapeamento de Jordan Wigner diretamente usando apenas PySCF, numpy e Qiskit. Comentaremos abaixo as considerações sobre a sintaxe de outras soluções.

A função Cholesky nos ajuda a obter uma decomposição de baixa classificação dos termos de dois elétrons no Hamiltoniano.

def cholesky(V, eps):
    # see https://arxiv.org/pdf/1711.02242.pdf section B2
    # see https://arxiv.org/abs/1808.02625
    # see https://arxiv.org/abs/2104.08957
    no = V.shape[0]
    chmax, ng = 20 * no, 0
    W = V.reshape(no**2, no**2)
    L = np.zeros((no**2, chmax))
    Dmax = np.diagonal(W).copy()
    nu_max = np.argmax(Dmax)
    vmax = Dmax[nu_max]
    while vmax > eps:
        L[:, ng] = W[:, nu_max]
        if ng > 0:
            L[:, ng] -= np.dot(L[:, 0:ng], (L.T)[0:ng, nu_max])
        L[:, ng] /= np.sqrt(vmax)
        Dmax[: no**2] -= L[: no**2, ng] ** 2
        ng += 1
        nu_max = np.argmax(Dmax)
        vmax = Dmax[nu_max]
    L = L[:, :ng].reshape((no, no, ng))
    print(
        "accuracy of Cholesky decomposition ",
        np.abs(np.einsum("prg,qsg->prqs", L, L) - V).max(),
    )
    return L, ng

As funções identity e creators_destructors substituem os operadores de criação e aniquilação no Hamiltoniano fermiônico por operadores Pauli; creators_destructors usa o mapeamento Jordan-Wigner.

def identity(n):
    return SparsePauliOp.from_list([("I" * n, 1)])


def creators_destructors(n, mapping="jordan_wigner"):
    c_list = []
    if mapping == "jordan_wigner":
        for p in range(n):
            if p == 0:
                ell, r = "I" * (n - 1), ""
            elif p == n - 1:
                ell, r = "", "Z" * (n - 1)
            else:
                ell, r = "I" * (n - p - 1), "Z" * p
            cp = SparsePauliOp.from_list([(ell + "X" + r, 0.5), (ell + "Y" + r, -0.5j)])
            c_list.append(cp)
    else:
        raise ValueError("Unsupported mapping.")
    d_list = [cp.adjoint() for cp in c_list]
    return c_list, d_list

Por fim, o site build_hamiltonian usa as funções cholesky, identity e creators_destructors para criar o Hamiltoniano final adequado para execução em um computador quântico.

def build_hamiltonian(ecore: float, h1e: np.ndarray, h2e: np.ndarray) -> SparsePauliOp:
    ncas, _ = h1e.shape

    C, D = creators_destructors(2 * ncas, mapping="jordan_wigner")
    Exc = []
    for p in range(ncas):
        Excp = [C[p] @ D[p] + C[ncas + p] @ D[ncas + p]]
        for r in range(p + 1, ncas):
            Excp.append(
                C[p] @ D[r]
                + C[ncas + p] @ D[ncas + r]
                + C[r] @ D[p]
                + C[ncas + r] @ D[ncas + p]
            )
        Exc.append(Excp)

    # low-rank decomposition of the Hamiltonian
    Lop, ng = cholesky(h2e, 1e-6)
    t1e = h1e - 0.5 * np.einsum("pxxr->pr", h2e)

    H = ecore * identity(2 * ncas)
    # one-body term
    for p in range(ncas):
        for r in range(p, ncas):
            H += t1e[p, r] * Exc[p][r - p]
    # two-body term
    for g in range(ng):
        Lg = 0 * identity(2 * ncas)
        for p in range(ncas):
            for r in range(p, ncas):
                Lg += Lop[p, r, g] * Exc[p][r - p]
        H += 0.5 * Lg @ Lg

    return H.chop().simplify()

Por fim, usamos o site build_hamiltonian para construir nosso qubit Hamiltoniano a partir de operadores Pauli usando a transformação Jordan-Wigner. Isso também nos dá a precisão da decomposição de Cholesky que usamos.

H = build_hamiltonian(ecore, h1e, h2e)
print(H)

Output:

accuracy of Cholesky decomposition  2.220446049250313e-16
SparsePauliOp(['IIII', 'IIIZ', 'IZII', 'IIZI', 'ZIII', 'IZIZ', 'IIZZ', 'ZIIZ', 'IZZI', 'ZZII', 'ZIZI', 'YYYY', 'XXYY', 'YYXX', 'XXXX'],
              coeffs=[-0.09820182+0.j, -0.1740751 +0.j, -0.1740751 +0.j,  0.2242933 +0.j,
  0.2242933 +0.j,  0.16891402+0.j,  0.1210099 +0.j,  0.16631441+0.j,
  0.16631441+0.j,  0.1210099 +0.j,  0.17504456+0.j,  0.04530451+0.j,
  0.04530451+0.j,  0.04530451+0.j,  0.04530451+0.j])

Esse notebook de molécula de exemplo mostra a configuração e os Hamiltonianos de várias moléculas de complexidade variável; com algumas modificações, isso deve permitir que você examine a maioria das moléculas pequenas.

Vamos observar brevemente dois pontos importantes a serem considerados ao construir os operadores fermiônicos para uma molécula. Conforme o tipo de molécula muda, a simetria muda. Da mesma forma, o número de orbitais com várias simetrias, como a simetria cilíndrica " A1 ", será alterado. Essas alterações são evidentes mesmo com a simples extensão para LiH,, como visto aqui:

distance = 1.56
mol = gto.Mole()
mol.build(
    verbose=0,
    atom=[["Li", (0, 0, 0)], ["H", (0, 0, distance)]],
    basis="sto-6g",
    spin=0,
    charge=0,
    symmetry="Coov",
)
mf = scf.RHF(mol)
E1 = mf.kernel()

# %% ----------------------------------------------------------------------------------------------

mx = mcscf.CASCI(mf, ncas=5, nelecas=(1, 1))
cas_space_symmetry = {"A1": 3, "E1x": 1, "E1y": 1}
mo = mcscf.sort_mo_by_irrep(mx, mf.mo_coeff, cas_space_symmetry)
E2 = mx.kernel(mo)[:2]
h1e, ecore = mx.get_h1eff()
h2e = ao2mo.restore(1, mx.get_h2eff(), mx.ncas)

Também vale a pena observar que é possível perder rapidamente a intuição do Hamiltoniano final resultante. O Hamiltoniano para LiH (usando o mapeador Jordan-Wigner) já consiste em 276 termos.

len(build_hamiltonian(ecore, h1e, h2e))

Output:

accuracy of Cholesky decomposition  1.1102230246251565e-16
276

Em caso de dúvida com relação às simetrias, também é possível gerar algumas informações de simetria para a molécula definindo symmetry = True e verbose = 4:

distance = 1.56
mol = gto.Mole()
mol.build(
    verbose=4,
    atom=[["Li", (0, 0, 0)], ["H", (0, 0, distance)]],
    basis="sto-6g",
    spin=0,
    charge=0,
    symmetry=True,
)

Output:

System: uname_result(system='Linux', node='IBM-R912JTRT', release='5.10.102.1-microsoft-standard-WSL2', version='#1 SMP Wed Mar 2 00:30:59 UTC 2022', machine='x86_64')  Threads 16
Python 3.11.12 (main, May 16 2025, 02:33:32) [GCC 11.4.0]
numpy 2.3.1  scipy 1.16.0  h5py 3.14.0
Date: Mon Jun 30 12:56:55 2025
PySCF version 2.9.0
PySCF path  /home/porter284/.pyenv/versions/3.11.12/lib/python3.11/site-packages/pyscf

[CONFIG] conf_file None
[INPUT] verbose = 4
[INPUT] num. atoms = 2
[INPUT] num. electrons = 4
[INPUT] charge = 0
[INPUT] spin (= nelec alpha-beta = 2S) = 0
[INPUT] symmetry True subgroup None
[INPUT] Mole.unit = angstrom
[INPUT] Symbol           X                Y                Z      unit          X                Y                Z       unit  Magmom
[INPUT]  1 Li     0.000000000000   0.000000000000   0.000000000000 AA    0.000000000000   0.000000000000   0.000000000000 Bohr   0.0
[INPUT]  2 H      0.000000000000   0.000000000000   1.560000000000 AA    0.000000000000   0.000000000000   2.947972754321 Bohr   0.0

nuclear repulsion = 1.01764848253846
point group symmetry = Coov
symmetry origin: [0.         0.         0.73699319]
symmetry axis x: [1. 0. 0.]
symmetry axis y: [0. 1. 0.]
symmetry axis z: [0. 0. 1.]
num. orbitals of irrep A1 = 4
num. orbitals of irrep E1x = 1
num. orbitals of irrep E1y = 1
number of shells = 4
number of NR pGTOs = 36
number of NR cGTOs = 6
basis = sto-6g
ecp = {}
CPU time:         9.85
<pyscf.gto.mole.Mole at 0x7fc719f94850>

Entre outras informações úteis, isso retorna point group symmetry = Coov e também o número de orbitais em cada representação irredutível.

point group symmetry = Coov
num. orbitals of irrep A1 = 4
num. orbitals of irrep E1x = 1
num. orbitals of irrep E1y = 1
number of shells = 4

Isso não necessariamente informa quantos orbitais você deseja incluir no espaço ativo, mas ajuda a ver quais orbitais estão presentes e suas simetrias.

Especificar a simetria e os orbitais geralmente é útil, mas você também pode especificar o número de orbitais que deseja incluir. Considere o caso do eteno, abaixo. Usando o site verbose = 4, podemos imprimir as simetrias dos vários orbitais:

# Replace these variables with correct distances:
a = 1
b = 1
c = 1

# Build
mol = gto.Mole()
mol.build(
    verbose=4,
    atom=[
        ["C", (0, 0, a)],
        ["C", (0, 0, -a)],
        ["H", (0, c, b)],
        ["H", (0, -c, b)],
        ["H", (0, c, -b)],
        ["H", (0, -c, -b)],
    ],
    basis="sto-6g",
    spin=0,
    charge=0,
    symmetry=True,
)

Output:

System: uname_result(system='Linux', node='IBM-R912JTRT', release='5.10.102.1-microsoft-standard-WSL2', version='#1 SMP Wed Mar 2 00:30:59 UTC 2022', machine='x86_64')  Threads 16
Python 3.11.12 (main, May 16 2025, 02:33:32) [GCC 11.4.0]
numpy 2.3.1  scipy 1.16.0  h5py 3.14.0
Date: Mon Jun 30 12:57:07 2025
PySCF version 2.9.0
PySCF path  /home/porter284/.pyenv/versions/3.11.12/lib/python3.11/site-packages/pyscf

[CONFIG] conf_file None
[INPUT] verbose = 4
[INPUT] num. atoms = 6
[INPUT] num. electrons = 16
[INPUT] charge = 0
[INPUT] spin (= nelec alpha-beta = 2S) = 0
[INPUT] symmetry True subgroup None
[INPUT] Mole.unit = angstrom
[INPUT] Symbol           X                Y                Z      unit          X                Y                Z       unit  Magmom
[INPUT]  1 C      0.000000000000   0.000000000000   1.000000000000 AA    0.000000000000   0.000000000000   1.889726124565 Bohr   0.0
[INPUT]  2 C      0.000000000000   0.000000000000  -1.000000000000 AA    0.000000000000   0.000000000000  -1.889726124565 Bohr   0.0
[INPUT]  3 H      0.000000000000   1.000000000000   1.000000000000 AA    0.000000000000   1.889726124565   1.889726124565 Bohr   0.0
[INPUT]  4 H      0.000000000000  -1.000000000000   1.000000000000 AA    0.000000000000  -1.889726124565   1.889726124565 Bohr   0.0
[INPUT]  5 H      0.000000000000   1.000000000000  -1.000000000000 AA    0.000000000000   1.889726124565  -1.889726124565 Bohr   0.0
[INPUT]  6 H      0.000000000000  -1.000000000000  -1.000000000000 AA    0.000000000000  -1.889726124565  -1.889726124565 Bohr   0.0

nuclear repulsion = 29.3377079104231
point group symmetry = D2h
symmetry origin: [0. 0. 0.]
symmetry axis x: [0. 1. 0.]
symmetry axis y: [1. 0. 0.]
symmetry axis z: [-0. -0. -1.]
num. orbitals of irrep Ag = 4
num. orbitals of irrep B2g = 2
num. orbitals of irrep B3g = 1
num. orbitals of irrep B1u = 4
num. orbitals of irrep B2u = 1
num. orbitals of irrep B3u = 2
number of shells = 10
number of NR pGTOs = 84
number of NR cGTOs = 14
basis = sto-6g
ecp = {}
CPU time:         9.92
<pyscf.gto.mole.Mole at 0x7fc719fa9290>

Obtemos:

número de orbitais do irrep Ag = 4

número de orbitais do irrep B2g = 2

número de orbitais do irrep B3g = 1

número de orbitais do irrep B1u = 4

número de orbitais do irrep B2u = 1

número de orbitais do irrep B3u = 2

Mas, em vez de especificar todos os orbitais por simetria, podemos simplesmente escrever:

active_space = range(mol.nelectron // 2 - 2, mol.nelectron // 2 + 2)

Nessa abordagem, usamos vários orbitais próximos ao nível de preenchimento (valência e desocupado). Aqui, 5 orbitais foram selecionados para inclusão no espaço ativo (do 6º ao 10º).

print(
    mol.nelectron // 2 - 2,
    mol.nelectron // 2 + 2,
)

Output:

6 10
  1. software de terceiros

Há vários pacotes de software desenvolvidos para a química quântica, alguns oferecendo vários mapeadores e ferramentas para restringir os espaços ativos. As etapas descritas acima são gerais e também se aplicam a softwares de terceiros. Mas esse outro software pode retornar Hamiltonianos em um formato que não é aceito pelo Qiskit. Por exemplo, alguns softwares retornam Hamiltonianos no formato:

H = -0.042 [] + -0.045 [X0 X1 Y2 Y3] + ... + 0.178 [Z0] + ... + 0.176 [Z2 Z3] + -0.243 [Z3]

Observe, em particular, que as portas são numeradas e os operadores de identidade não são mostrados. Isso contrasta com os hamiltonianos usados no Qiskit, que escreveriam o termo [Z2 Z3] como ZZII (os qubits 0 e 1 agindo pelo operador de identidade, os qubits 2 e 3 agindo pelo operador Z, ordenados com o qubit 0 mais à direita).

Para acomodar qualquer fluxo de trabalho existente, o bloco de código abaixo converte de uma sintaxe para a outra. A função convert_openfermion_to_qiskit recebe como argumentos um Hamiltoniano gerado em OpenFermion ou Tangelo (e já mapeado em operadores Pauli usando qualquer mapeador disponível) e o número de qubits necessários para a molécula.

from openfermion import QubitOperator
from qiskit.quantum_info import SparsePauliOp


def convert_openfermion_to_qiskit(
    openfermion_operator: QubitOperator, num_qubits: int
) -> SparsePauliOp:
    terms = openfermion_operator.terms

    labels = []
    coefficients = []

    for term, constant in terms.items():
        # Default set to identity
        operator = list("I" * num_qubits)

        # Iterate through PauliSum and replace I with Pauli
        for index, pauli in term:
            operator[index] = pauli
        label = "".join(operator)
        labels.append(label)
        coefficients.append(constant)

    return SparsePauliOp(labels, coefficients)

Além disso, este notebook Python contém um código de amostra completo para migrar Hamiltonians de outros fluxos de trabalho de software para o Qiskit, incluindo a conversão acima.

Agora você deve ter um arsenal de ferramentas para obter o Hamiltoniano necessário para realizar cálculos de química quântica em computadores quânticos IBM®.

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.