Skip to main content
IBM Quantum Platform

Execute cargas de trabalho quânticas com o QRMI

Estimativa de tempo de execução: menos de um minuto em um hardware d IBM Quantum® e para a seção SQD. Essa estimativa não inclui o tempo de fila nem o processamento clássico; o tempo de execução pode variar.


Resultados do aprendizado

  1. O papel que o QRMI desempenha como middleware entre os agendadores de HPC e o hardwar IBM Quantum
  2. Como utilizar o ciclo de vida básico do QRMI (acquire → task_start → task_status → task_result → release) em um backend real do IBM®
  3. Como usar o Qiskit de nível superior e SamplerV2 os wrappers QRMIService sobre o QRMI
  4. Como os agendadores de HPC (Slurm) injetam recursos quânticos por meio de variáveis de ambiente e como as aplicações os consomem
  5. Como executar um fluxo de trabalho completo de química SQD (Diagonalização Quântica Baseada em Amostras) em N 2_2, utilizando o hardware d IBM, por meio do QRMI

Pré-requisitos


Segundo plano

O desafio da integração entre computação quântica e HPC

Os fluxos de trabalho de computação de alto desempenho (HPC) geralmente exigem uma coordenação perfeita entre clusters de computação clássicos e unidades de processamento quântico (QPUs). Diferentes back-ends e serviços de hardware quântico oferecem mecanismos de autenticação, formatos de transmissão e APIs de ciclo de vida de tarefas distintos. A integração de sistemas de computação em nuvem ( IBM Quantum ) a gerenciadores de cargas de trabalho de HPC (como o Slurm) requer uma interface simples e padronizada para aquisição de recursos, execução de tarefas e gerenciamento de sessões.

O que é o QRMI

A Quantum Resource Management Interface (QRMI) é uma biblioteca de middleware escrita em Rust que padroniza o acesso ao hardware quântico a partir de agendadores de HPC e aplicativos clássicos. Ele expõe uma única API unificada para o ciclo de vida:

┌─────────────────────────────────────────────────────────────────┐
│                     HPC Application Layer                       │
│          (Slurm job script / Python workflow / CUDA-Q)          │
└───────────────────────────┬─────────────────────────────────────┘
                            │  QRMI API
                            │  acquire() / task_start() / task_result() / release()
┌───────────────────────────▼─────────────────────────────────────┐
│                        QRMI Core (Rust)                         │
│            Python bindings · C bindings · Lua bindings          │
└───────────────────────────┬─────────────────────────────────────┘
                            │
               IBM Quantum Compute Service / IBM Quantum System

O QRMI é publicado como um projeto de código aberto em github.com/qiskit-community/qrmi e é descrito no artigo de visão geral arXiv:2506.10052.

Principais escolhas de design

Ciclo de vida do recurso, e não compilação do circuito. O QRMI lida com o ciclo de vida de aquisição/envio/consulta/liberação e nada mais. A compilação, a otimização e a transpilagem de circuitos continuam ocorrendo na camada de aplicação (por exemplo, no Qiskit). Isso mantém a interface minimalista e modular.

Modelo de portabilidade de fornecedores. Embora o QRMI forneça chamadas comuns de gerenciamento de tarefas (acquire, task_start, task_status, task_result, release) em todos os back-ends de hardware compatíveis, a troca de fornecedores também exige diferentes etapas de compilação, construção de carga útil específica para cada fornecedor e decodificação dos resultados na camada de aplicação.

Formato de carga útil nativo do IBM. Para back-ends do tipo “ IBM Quantum ”, o QRMI utiliza cargas de dados JSON do tipo “ OpenQASM 3” (QiskitPrimitive) em conformidade com o esquema Qiskit Runtime.

Configuração por meio de variáveis de ambiente. As credenciais e os URLs dos endpoints são lidos a partir de variáveis de ambiente durante a execução. Em um cluster de HPC, o plug-in Slurm QRMI SPANK define esses parâmetros automaticamente quando uma tarefa é enviada. Em um notebook ou em uma sessão interativa, você os carrega a partir de um arquivo .env . O código do aplicativo nunca contém credenciais ou URLs de endpoints codificadas de forma estática.

Integração do agendador de HPC por meio do GRES. Quando um trabalho do Slurm solicita recursos do Quantum por meio da interface do plug-in QRMI SPANK (#SBATCH --gres=qpu:1 e #SBATCH --qpu=ibm_kingston), o plug-in insere QRMI_JOB_QPU_RESOURCES e QRMI_JOB_QPU_TYPES no ambiente do trabalho. Os aplicativos fazem chamadas para get_job_qpu_resources_and_types() descobrir quais recursos foram alocados — não é necessário usar nomes de back-end fixos. QRMIService encapsula esse padrão para usuários do Qiskit.

As chamadas à API principal

Chamada
Propósito
qrmi.acquire()Obtém acesso ao recurso (por exemplo, abre uma sessão dedicada); retorna um token de bloqueio
qrmi.target()Recuperar as capacidades do backend (qubits, portas, mapa de acoplamento) como JSON
qrmi.task_start(payload)Enviar um trabalho quântico; retorna um ID do trabalho
qrmi.task_status(job_id)Verificar o status da tarefa (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Recuperar os resultados de tarefas concluídas como uma string JSON não formatada
qrmi.task_stop(job_id)Cancelar ou limpar uma tarefa
qrmi.release(lock)Liberar o bloqueio do recurso (por exemplo, encerrar a sessão)

O que este tutorial aborda

Este tutorial está dividido em duas partes:

Etapas 1–3 (exemplos em pequena escala): Apresentar a API do QRMI por meio de uma demonstração simples de um circuito no estado de Bell no hardwar IBM Quantum, abordando tanto o uso direto de primitivas de baixo nível quanto a programação de alto nível QRMIService e a integração SamplerV2 .

Exemplo de hardware em grande escala: um fluxo de trabalho SQD completo para a molécula N 2_2, com distância entre ligações de 1.0 A˚\AA (espaço ativo com base em cc-pVDZ, 26 orbitais espaciais / 52 qubits), executado em hardware IBM Quantum por meio do QRMI. O SQD combina a amostragem quântica de um ansatz LUCJ construído com e ffsim a recuperação de configuração autoconsistente com qiskit-addon-sqd.


Requisitos

Antes de começar este tutorial, certifique-se de que os seguintes itens estejam instalados.

Python configuração do ambiente

Estão disponíveis pacotes binários pré-compilados para o Linux no site PyPI,, de modo que a versão padrão pip install funciona diretamente em sistemas Linux /HPC.

python3 -m venv ~/.venvs/qrmi-ibm
source ~/.venvs/qrmi-ibm/bin/activate
python -m pip install "qrmi[ibm]" python-dotenv pyscf ffsim qiskit-addon-sqd matplotlib ipykernel
python -m ipykernel install --user --name qrmi-ibm --display-name "QRMI IBM"
Plataformas sem rodas pré-montadas

Se você compilar pip o QRMI a partir do código-fonte, certifique-se de ter uma cadeia de ferramentas Rust atualizada (Rust ≥ 1.91.1, instalada por meio rustup do link rustup.rs ).

Selecione o kernel “ IBM ” do QRMI no Jupyter, reinicie-o e execute as células do notebook em ordem. Os resultados salvos são provenientes da execução no hardware do colaborador; os comandos de instalação não especificam as versões exatas utilizadas nessa execução.

Credenciais Necessárias

  • IBM Quantum : chave da API do IAM e endereço do serviço CRN, disponível em IBM Quantum Platform

Para execução autônoma, crie um arquivo .env ao lado deste notebook com os seguintes valores, substituindo os espaços reservados para as credenciais. Mantenha este arquivo em sigilo. Se você selecionar um backend diferente, atualize tanto o nome dele quanto os prefixos das variáveis de ambiente.

ibm_kingston_QRMI_IBM_QCS_ENDPOINT=https://quantum.cloud.ibm.com/api/v1
ibm_kingston_QRMI_IBM_QCS_IAM_ENDPOINT=https://iam.cloud.ibm.com
ibm_kingston_QRMI_IBM_QCS_IAM_APIKEY=<your-iam-api-key>
ibm_kingston_QRMI_IBM_QCS_SERVICE_CRN=<your-crn-starting-with-crn:v1:>
ibm_kingston_QRMI_IBM_QCS_SESSION_MODE=dedicated
ibm_kingston_QRMI_IBM_QCS_SESSION_MAX_TTL=28800
QRMI_JOB_QPU_RESOURCES=ibm_kingston
QRMI_JOB_QPU_TYPES=ibm-quantum-compute-service

Para uma alocação do Slurm, utilize as configurações de recursos e as credenciais fornecidas pelo cluster. O caderno mantém os valores ambientais existentes.


Instalação

Importe as dependências e carregue a configuração dos recursos.

import os
import time
import json
import numpy as np
from dotenv import load_dotenv

from qrmi import (
    QuantumResource,
    ResourceType,
    Payload,
    TaskStatus,
    get_job_qpu_resources_and_types,
)
from qrmi.primitives import QRMIService
from qrmi.primitives.ibm import SamplerV2, get_target

from qiskit import QuantumCircuit, qasm3
from qiskit.circuit.library import efficient_su2
from qiskit.primitives.containers.sampler_pub import SamplerPub
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# Load credentials from .env without overriding already-set scheduler environment variables
load_dotenv(override=False)

# Preserve resources if injected by Slurm SPANK plugin; fallback to default for interactive run
BACKEND_NAME = os.environ.get("QRMI_JOB_QPU_RESOURCES", "ibm_kingston")
os.environ.setdefault("QRMI_JOB_QPU_RESOURCES", BACKEND_NAME)
os.environ.setdefault("QRMI_JOB_QPU_TYPES", "ibm-quantum-compute-service")

print(f"Backend: {BACKEND_NAME}")
print("Environment ready.")

Output:

Backend: ibm_kingston
Environment ready.

Exemplos em pequena escala

As etapas 1 a 3 apresentam a API do QRMI por meio de circuitos simples. Cada etapa corresponde a uma fase principal do ciclo de vida do QRMI em relação ao hardware d IBM Quantum.

A carga útil para essas etapas iniciais é um pequeno circuito no estado de Bell, escolhido por ser rápido e de baixo custo para ser executado.

Esses exemplos utilizam hardware porque demonstram a alocação remota de recursos e o gerenciamento de tarefas. Um simulador de circuito local não valida a integração do serviço QRMI com o agendador. A execução deste notebook envia tarefas d IBM Quantum e requer acesso ao backend configurado.

Etapa 1: Mapeie o problema clássico para um recurso quântico

O primeiro passo em qualquer fluxo de trabalho do QRMI é criar um objeto QuantumResource e verificar se ele está acessível.

get_target() recupera a descrição de hardware do backend (número de qubits, portas de base, mapa de acoplamento) e a organiza como um Target objeto Qiskit, que o transpiler utiliza na Etapa 2.

Etapa 2: Otimizar o problema para execução em hardware quântico

Antes do envio, use o Qiskit para transpilar o circuito para a arquitetura do conjunto de instruções (ISA) do backend, utilizando o objeto Target recuperado na Etapa 1.

O exemplo, então, cria um Payload.QiskitPrimitive, que encapsula a sequência de circuitos OpenQASM 3 e os metadados da tarefa no esquema primitivo IBM.

Etapa 3: Executar usando primitivas QRMI

Com a carga útil criada, o exemplo envia o trabalho e verifica se ele foi concluído. task_start() retorna um ID de tarefa imediatamente; task_status() é consultado até que o status não seja mais Queued/Running. Os resultados são recuperados como uma string JSON bruta e analisados para extrair amostras de medição.

A célula a seguir agrupa a aquisição, a execução e a limpeza, de modo que, mesmo em caso de falhas após a aquisição, a sessão pertencente ao notebook ainda seja liberada.

# ── IBM Quantum ───────────────────────────────────────────────────────
qrmi = QuantumResource(BACKEND_NAME, ResourceType.IBMQuantumComputeService)
# ResourceType.IBMQuantumSystem is the alternative for directly provisioned systems

print(f"Resource id:   {qrmi.resource_id()}")
print(f"Resource type: {qrmi.resource_type()}")
print(f"Accessible:    {qrmi.is_accessible()}")

# Acquire exclusive access — open try/finally immediately so every
# subsequent failure (target retrieval, transpilation, submission) is covered.
# Release is skipped when running under Slurm: the SPANK plugin owns the
# session lifecycle and will release it when the job finishes.
lock = qrmi.acquire()
print(f"Lock token:    {lock}")
try:
    # Retrieve backend capabilities
    transpiler_target = get_target(
        qrmi
    )  # calls qrmi.target() and parses the JSON
    target_json = json.loads(qrmi.target().value)
    config = target_json.get("configuration", {})
    print(f"\nBackend: {config.get('backend_name', 'unknown')}")
    print(f"Qubits:  {config.get('n_qubits', 'unknown')}")
    print(f"Gates:   {config.get('basis_gates', [])}")

    # ── IBM Quantum ───────────────────────────────────────────────────

    # Build a Bell state circuit
    qc = QuantumCircuit(2)
    qc.h(0)
    qc.cx(0, 1)
    qc.measure_all()
    print(qc.draw("text"))

    # Transpile to ISA using the target retrieved in Step 1
    pm = generate_preset_pass_manager(
        optimization_level=1, target=transpiler_target
    )
    isa_circuit = pm.run(qc)
    print(f"\nTranspiled gate counts: {isa_circuit.count_ops()}")

    # Build the QRMI payload
    # Payload.QiskitPrimitive wraps the IBM SamplerV2 input schema:
    #   pubs: list of [qasm3_string, parameter_values]  (shots goes at top level)
    #   program_id: "sampler" or "estimator"
    shots = 1024
    pub = SamplerPub.coerce((isa_circuit,), shots)
    qasm3_str = qasm3.dumps(
        pub.circuit,
        disable_constants=True,
        allow_aliasing=True,
        experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1,
    )
    # Parameter values as a flat list (empty for non-parametric circuits)
    param_array = pub.parameter_values.as_array(
        pub.circuit.parameters
    ).tolist()

    input_json = {
        "pubs": [
            [qasm3_str, param_array]
        ],  # list-of-lists; shots at top level
        "version": 2,
        "support_qiskit": False,  # True returns binary-encoded Qiskit result
        "shots": shots,
    }
    payload = Payload.QiskitPrimitive(
        input=json.dumps(input_json), program_id="sampler"
    )
    print("Payload ready")

    # ── IBM Quantum ───────────────────────────────────────────────────

    # Submit the job
    job_id = qrmi.task_start(payload)
    print(f"Job submitted: {job_id}")

    # Poll until complete
    while True:
        status = qrmi.task_status(job_id)
        print(f"  Status: {status}")
        if status not in [TaskStatus.Running, TaskStatus.Queued]:
            break
        time.sleep(5)

    print(f"\nFinal status: {status}")

    # Retrieve results
    # support_qiskit=False → plain JSON; parse directly without ResultDecoder
    if status == TaskStatus.Completed:
        raw = qrmi.task_result(job_id).value
        result = json.loads(raw)
        # IBM QCS plain-JSON result shape: {"results": [{"data": {"meas": {"samples": [...]}}}]}
        # samples is a list of hex-encoded integers; decode to zero-padded bitstrings
        samples = result["results"][0]["data"]["meas"]["samples"]
        num_bits = sum(reg.size for reg in isa_circuit.cregs)
        from collections import Counter

        counts = Counter(format(int(s, 16), f"0{num_bits}b") for s in samples)
        print(f"\nMeasurement counts: {dict(counts.most_common(8))}")
        qrmi.task_stop(job_id)
    else:
        print(f"Job did not complete. Logs:\n{qrmi.task_logs(job_id)}")

finally:
    # Release only in interactive sessions; under Slurm the SPANK plugin
    # manages the session lifecycle and calling release() here would
    # prematurely close a session it does not own.
    if not os.environ.get("SLURM_JOB_ID"):
        qrmi.release(lock)
        print("\nSession released.")

Output:

Resource id:   ibm_kingston
Resource type: ResourceType.IBMQuantumComputeService
Accessible:    True
Lock token:    2ff43011-aed1-4436-a4df-40f37ec588b7

Backend: ibm_kingston
Qubits:  156
Gates:   ['cz', 'id', 'rx', 'rz', 'rzz', 'sx', 'x', 'xslow']
        ┌───┐      ░ ┌─┐   
   q_0: ┤ H ├──■───░─┤M├───
        └───┘┌─┴─┐ ░ └╥┘┌─┐
   q_1: ─────┤ X ├─░──╫─┤M├
             └───┘ ░  ║ └╥┘
meas: 2/══════════════╩══╩═
                      0  1 

Transpiled gate counts: OrderedDict([('rz', 6), ('sx', 3), ('measure', 2), ('cz', 1), ('barrier', 1)])
Payload ready
Job submitted: dai43g8mhr3c73e7a7o0
  Status: TaskStatus.Queued
  Status: TaskStatus.Running
  Status: TaskStatus.Completed

Final status: TaskStatus.Completed

Measurement counts: {'11': 487, '00': 254, '01': 177, '10': 106}

Session released.

Interface de nível superior do Qiskit: QRMIService e SamplerV2

O ciclo de vida “raw” acima oferece controle explícito sobre cada chamada. Para fluxos de trabalho padrão do Qiskit, o QRMI fornece uma primitiva SamplerV2 que implementa BaseSamplerV2.

Gerenciamento de ciclo de vida

SamplerV2 cuida da serialização da carga útil, do envio (task_start), da consulta e da decodificação dos resultados. Em um ambiente de processamento em lote de HPC (por exemplo, com o Slurm), a alocação e a liberação são gerenciadas pelo agendador e pelo plug-in SPANK. Em uma sessão interativa do Python que utiliza objetos diretos da API de baixo nível, acquire() e release() podem ser usados para gerenciar explicitamente sessões dedicadas.

# QRMIService reads QRMI_JOB_QPU_RESOURCES / QRMI_JOB_QPU_TYPES set in Setup or Slurm
service = QRMIService()
qrmi_svc = service.resources()[0]
print(f"Using: {qrmi_svc.resource_id()} ({qrmi_svc.resource_type()})")

# Build an EfficientSU2 circuit
circuit = efficient_su2(5, entanglement="linear")
circuit.measure_all()
param_values = np.random.rand(circuit.num_parameters)

pm = generate_preset_pass_manager(
    optimization_level=1, target=get_target(qrmi_svc)
)
isa_circuit = pm.run(circuit)

# SamplerV2 executes jobs against the QRMI resource and decodes results into primitive containers
sampler = SamplerV2(qrmi_svc, options={"default_shots": 1024})
job = sampler.run([(isa_circuit, param_values)])
print(f"Job ID: {job.job_id()} | Status: {job.status()}")

# Poll with retry — re-raise immediately on permanent failures;
# only retry on transient network/timeout errors (connection resets, 503s).
_TRANSIENT = (
    "503",
    "Service Unavailable",
    "ConnectionError",
    "TimeoutError",
    "timed out",
    "Connection reset",
)
result = None
for attempt in range(60):
    try:
        result = job.result()  # blocks until complete
        break
    except Exception as e:
        if not any(tok in str(e) for tok in _TRANSIENT):
            raise
        print(f"  Transient error on attempt {attempt + 1}: {e}")
        time.sleep(10)

if result is not None:
    counts = result[0].data.meas.get_counts()
    print(f"Counts (first 5): {dict(list(counts.items())[:5])}")
else:
    print("Job did not complete after retries.")

if job.errored():
    print(f"Logs:\n{job.logs()}")

Output:

Using: ibm_kingston (ResourceType.IBMQuantumComputeService)
Job ID: dai43jj9k43c73afhrhg | Status: JobStatus.QUEUED
Counts (first 5): {'00010': 66, '00100': 28, '11000': 71, '00110': 23, '10100': 14}

Contexto de HPC: injeção de recursos no Slurm

Em um cluster de HPC, os usuários solicitam recursos quânticos utilizando a sintaxe do Slurm GRES, juntamente com as opções do plug-in QRMI SPANK. O plug-in lida com a injeção de credenciais e recursos automaticamente:

#SBATCH --gres=qpu:1
#SBATCH --qpu=ibm_kingston
python my_workflow.py   # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already set

O código do aplicativo descobre os recursos alocados a ele em tempo de execução — sem nomes de back-ends codificados:

# get_job_qpu_resources_and_types() reads QRMI_JOB_QPU_RESOURCES / QRMI_JOB_QPU_TYPES
# set by the Slurm SPANK plugin (or manually above in Setup)
qpus, qpu_types = get_job_qpu_resources_and_types()
print("Resources allocated by scheduler:")
for qpu, qpu_type in zip(qpus, qpu_types):
    print(f"  {qpu}  ({qpu_type})")

# QRMIService wraps this into a list of ready QuantumResource objects
for r in QRMIService().resources():
    print(
        f"\nQRMIService found: {r.resource_id()}  accessible={r.is_accessible()}"
    )

Output:

Resources allocated by scheduler:
  ibm_kingston  (ibm-quantum-compute-service)

QRMIService found: ibm_kingston  accessible=True

Exemplo de hardware em grande escala: SQD no N 2_2

Aqui, reunimos todos os componentes em um fluxo de trabalho completo de química quântica em maior escala, executado em hardware real do IBM Quantum por meio do QRMI.

O SQD combina os seguintes elementos:

  1. Amostragem quântica de um modelo Jastrow de aglomerado unitário local (LUCJ) construído utilizando ffsim e inicializado a partir de amplitudes CCSD
  2. Transpilagem orientada para o hardware que se adapta à topologia de rede “heavy-hex” por meio de generate_lucj_pass_manager
  3. Execução de amostragem em hardware d IBM Quantum, gerenciado por meio de e QRMIService QRMI SamplerV2
  4. Pós-processamento clássico: recuperação de configuração autoconsistente e diagonalização iterativa de subespaços utilizando qiskit-addon-sqd

Aplicamos o SQD ao N 2_2, com uma distância de ligação de 1.0 A˚\AA, utilizando um espaço ativo derivado do conjunto de bases cc-pVDZ (26 orbitais espaciais, correspondentes a 52 orbitais de spin/qubits).

Energia de referência para o espaço ativo de N 2_2 /cc-pVDZ (distância de ligação 1.0 A˚\AA ):

  • Energia de referência (cálculo SCI separado): − 109.22802922 Ha
Precisão da execução salva

A execução do SQD abaixo demonstra a execução bem-sucedida do QRMI de ponta a ponta em um hardware d IBM Quantum. Com uma única repetição do LUCJ e 100.000 simulações, o resultado fica aproximadamente 23.7 kcal/mol acima da energia de referência e não atinge a precisão química (≤ 1 kcal/mol). Alterar a n_reps contagem de disparos ou o número de iterações do SQD pode melhorar a precisão, mas requer mais testes.

Na execução salva, o gerenciador de passagens ffsim removeu as interações de spin oposto e (24, 24) (20, 20) porque o backend não conseguia acomodá-las. Os resultados apresentados utilizam esse circuito ajustado.

from qrmi.primitives.ibm import get_backend
import math
import os
import time
from functools import partial
from dotenv import load_dotenv
import numpy as np
import matplotlib.pyplot as plt

import pyscf
import pyscf.gto
import pyscf.scf
import pyscf.cc
import pyscf.mcscf
import pyscf.ao2mo

import ffsim
import ffsim.qiskit
from qiskit import QuantumCircuit, QuantumRegister
from qiskit_addon_sqd.fermion import (
    SCIResult,
    diagonalize_fermionic_hamiltonian,
    solve_sci_batch,
)
from qrmi.primitives import QRMIService
from qrmi.primitives.ibm import SamplerV2, get_target

load_dotenv(override=False)
os.environ.setdefault("QRMI_JOB_QPU_RESOURCES", "ibm_kingston")
os.environ.setdefault("QRMI_JOB_QPU_TYPES", "ibm-quantum-compute-service")

# ── Step 1: Map classical inputs to a quantum problem ─────────────────

# Build N2 molecule at 1.0 Å bond distance
mol = pyscf.gto.Mole()
mol.build(
    atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]],
    basis="cc-pvdz",
    symmetry="Dooh",
)

# Define active space: freeze 2 core orbitals
n_frozen = 2
active_space = range(n_frozen, mol.nao_nr())

# Get molecular integrals
scf = pyscf.scf.RHF(mol).run()
norb = len(active_space)
n_electrons = int(sum(scf.mo_occ[active_space]))
n_alpha = (n_electrons + mol.spin) // 2
n_beta = (n_electrons - mol.spin) // 2
nelec = (n_alpha, n_beta)

cas = pyscf.mcscf.CASCI(scf, norb, nelec)
mo = cas.sort_mo(active_space, base=0)
hcore, nuclear_repulsion_energy = cas.get_h1cas(mo)
eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), norb)

# Reference energy from external SCI calculation
reference_energy = -109.22802921665716

print(
    f"N₂/cc-pVDZ active space: {norb} orbitals ({2 * norb} qubits), {nelec} electrons"
)
print(f"SCF energy:       {scf.e_tot:.8f} Ha")
print(f"Reference energy: {reference_energy:.8f} Ha")

# Get CCSD amplitudes for initializing the LUCJ ansatz
ccsd = pyscf.cc.CCSD(
    scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]
).run()
t1 = ccsd.t1
t2 = ccsd.t2
print(f"CCSD energy:      {ccsd.e_tot:.8f} Ha")

# Discover backend via QRMIService (QRMI_JOB_QPU_RESOURCES set in Setup)
service = QRMIService()
qrmi_sqd = service.resources()[0]
print(f"Using QRMI resource: {qrmi_sqd.resource_id()}")

# get_backend() wraps the QRMI resource as a Qiskit backend for layout synthesis

backend = get_backend(qrmi_sqd)

# Set ansatz properties
n_reps = 1
pairs_aa = [(p, p + 1) for p in range(norb - 1)]
pairs_ab = None

# Create pass manager adapted to hardware heavy-hex topology
pass_manager, pairs_ab = ffsim.qiskit.generate_lucj_pass_manager(
    backend=backend,
    norb=norb,
    connectivity="heavy-hex",
    interaction_pairs=(pairs_aa, pairs_ab),
    optimization_level=3,
)

# Create the compressed LUCJ ansatz operator
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
    t2=t2,
    t1=t1,
    n_reps=n_reps,
    interaction_pairs=(pairs_aa, pairs_ab),
    optimize=True,
    options=dict(maxiter=1000),
)

# Assemble the circuit
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()
print(f"LUCJ circuit: {circuit.num_qubits} qubits, depth {circuit.depth()}")

# ── Step 2: Optimize for quantum hardware execution ───────────────────

isa_circuit = pass_manager.run(circuit)
print(f"Transpiled gate counts: {isa_circuit.count_ops()}")

# ── Step 3: Execute using Qiskit primitives (QRMI SamplerV2) ─────────

sampler = SamplerV2(qrmi_sqd, options={"default_shots": 100_000})
# sampler.options.environment.job_tags = ["TUT_SQD"]
job = sampler.run([(isa_circuit,)])
print(f"Job submitted via QRMI: {job.job_id()} | Status: {job.status()}")
print("Waiting for results from hardware...")

_TRANSIENT = (
    "503",
    "Service Unavailable",
    "ConnectionError",
    "TimeoutError",
    "timed out",
    "Connection reset",
)
primitive_result = None
for attempt in range(120):
    try:
        primitive_result = job.result()
        break
    except Exception as e:
        if not any(tok in str(e) for tok in _TRANSIENT):
            raise
        print(f"  Transient error on attempt {attempt + 1}: {e}")
        time.sleep(10)

if primitive_result is None:
    raise RuntimeError("Job did not complete after retries")

pub_result = primitive_result[0]
bit_array = pub_result.data.meas
print(f"Total shots collected: {bit_array.num_shots}")

# ── Step 4: Post-process and return result in classical format ────────


def is_valid_bitstring(
    bitstring: str, norb: int, nelec: tuple[int, int]
) -> bool:
    n_a, n_b = nelec
    return (
        len(bitstring) == 2 * norb
        and bitstring[norb:].count("1") == n_a
        and bitstring[:norb].count("1") == n_b
    )


num_valid = sum(
    is_valid_bitstring(b, norb, nelec) for b in bit_array.get_bitstrings()
)
valid_fraction = num_valid / bit_array.num_shots
expected_random = (
    math.comb(norb, n_alpha) * math.comb(norb, n_beta) / (2 ** (2 * norb))
)
print(f"Fraction of valid configurations sampled: {valid_fraction:.5f}")
print(f"Expected fraction from uniform random:     {expected_random:.4e}")

# Configure SQD eigensolver
energy_tol = 1e-3
occupancies_tol = 1e-3
max_iterations = 5
num_batches = 3
samples_per_batch = 300
symmetrize_spin = True
carryover_threshold = 1e-4
max_cycle = 200

# Hartree-Fock initial occupancy guess
initial_occupancies = (
    np.array([1] * n_alpha + [0] * (norb - n_alpha)),
    np.array([1] * n_beta + [0] * (norb - n_beta)),
)

sci_solver = partial(solve_sci_batch, spin_sq=0.0, max_cycle=max_cycle)
result_history = []


def callback(results: list[SCIResult]):
    result_history.append(results)
    iteration = len(result_history)
    print(f"Iteration {iteration}")
    for i, res in enumerate(results):
        subspace_dim = np.prod(res.sci_state.amplitudes.shape)
        print(
            f"  Subsample {i}: Energy = {res.energy + nuclear_repulsion_energy:.8f} Ha | Subspace dim = {subspace_dim}"
        )


print("\nRunning SQD post-processing...")
rng = np.random.default_rng(42)
sqd_result = diagonalize_fermionic_hamiltonian(
    hcore,
    eri,
    bit_array,
    samples_per_batch=samples_per_batch,
    norb=norb,
    nelec=nelec,
    num_batches=num_batches,
    energy_tol=energy_tol,
    occupancies_tol=occupancies_tol,
    max_iterations=max_iterations,
    sci_solver=sci_solver,
    symmetrize_spin=symmetrize_spin,
    initial_occupancies=initial_occupancies,
    carryover_threshold=carryover_threshold,
    callback=callback,
    seed=rng,
)

final_energy = sqd_result.energy + nuclear_repulsion_energy
energy_error = final_energy - reference_energy

print("\n=== Energy Summary (N₂/cc-pVDZ active space) ===")
print(f"SCF energy:       {scf.e_tot:.8f} Ha")
print(f"Reference energy: {reference_energy:.8f} Ha")
print(f"Final SQD energy: {final_energy:.8f} Ha")
print(
    f"Energy error:     {energy_error:.8f} Ha ({abs(energy_error) * 627.5:.4f} kcal/mol)"
)

# ── Visualization ─────────────────────────────────────────────────────

x1 = range(len(result_history))
min_e = [
    min(res, key=lambda r: r.energy).energy + nuclear_repulsion_energy
    for res in result_history
]
e_diff = [abs(e - reference_energy) for e in min_e]
chem_accuracy = 0.001  # ~1 mHa / ~0.6 kcal/mol

y2 = np.sum(sqd_result.orbital_occupancies, axis=0)
x2 = range(len(y2))

fig, axs = plt.subplots(1, 2, figsize=(12, 5))

# Energies convergence plot
axs[0].plot(x1, e_diff, label="Energy error", marker="o")
axs[0].set_xticks(list(x1))
axs[0].set_xticklabels(list(x1))
axs[0].set_yscale("log")
axs[0].axhline(
    y=chem_accuracy,
    color="#BF5700",
    linestyle="--",
    label="Chemical accuracy (1 mHa)",
)
axs[0].set_title("SQD Energy Error vs Iteration")
axs[0].set_xlabel("Iteration")
axs[0].set_ylabel("Energy Error (Ha)")
axs[0].legend()

# Spatial orbital occupancy plot
axs[1].bar(x2, y2, width=0.8)
axs[1].set_xticks(list(x2)[::2])
axs[1].set_xticklabels(list(x2)[::2])
axs[1].set_title("Avg Occupancy per Spatial Orbital")
axs[1].set_xlabel("Spatial Orbital Index")
axs[1].set_ylabel("Avg Occupancy")

plt.tight_layout()
plt.show()

Output:


WARN: Unable to to identify input symmetry using original axes.
Different symmetry axes will be used.

converged SCF energy = -108.929838385609
N₂/cc-pVDZ active space: 26 orbitals (52 qubits), (5, 5) electrons
SCF energy:       -108.92983839 Ha
Reference energy: -109.22802922 Ha
E(CCSD) = -109.2177884185545  E_corr = -0.2879500329450047
CCSD energy:      -109.21778842 Ha
Using QRMI resource: ibm_kingston
LUCJ circuit: 52 qubits, depth 3
Transpiled gate counts: OrderedDict([('sx', 7041), ('rz', 6969), ('cz', 1858), ('measure', 52), ('x', 47), ('barrier', 1)])
Job submitted via QRMI: dai43o0mhr3c73e7a81g | Status: JobStatus.QUEUED
Waiting for results from hardware...
Total shots collected: 100000
Fraction of valid configurations sampled: 0.00319
Expected fraction from uniform random:     9.6079e-07

Running SQD post-processing...
Iteration 1
  Subsample 0: Energy = -109.09341960 Ha | Subspace dim = 208849
  Subsample 1: Energy = -109.11738590 Ha | Subspace dim = 204304
  Subsample 2: Energy = -109.09947704 Ha | Subspace dim = 212521
Iteration 2
  Subsample 0: Energy = -109.16015998 Ha | Subspace dim = 332929
  Subsample 1: Energy = -109.16823702 Ha | Subspace dim = 319225
  Subsample 2: Energy = -109.16189785 Ha | Subspace dim = 336400
Iteration 3
  Subsample 0: Energy = -109.17759299 Ha | Subspace dim = 471969
  Subsample 1: Energy = -109.17937442 Ha | Subspace dim = 512656
  Subsample 2: Energy = -109.17970409 Ha | Subspace dim = 504100
Iteration 4
  Subsample 0: Energy = -109.18410905 Ha | Subspace dim = 608400
  Subsample 1: Energy = -109.18265405 Ha | Subspace dim = 636804
  Subsample 2: Energy = -109.18608430 Ha | Subspace dim = 657721
Iteration 5
  Subsample 0: Energy = -109.18870837 Ha | Subspace dim = 846400
  Subsample 1: Energy = -109.18890818 Ha | Subspace dim = 848241
  Subsample 2: Energy = -109.19022232 Ha | Subspace dim = 804609

=== Energy Summary (N₂/cc-pVDZ active space) ===
SCF energy:       -108.92983839 Ha
Reference energy: -109.22802922 Ha
Final SQD energy: -109.19022232 Ha
Energy error:     0.03780690 Ha (23.7238 kcal/mol)
Output of the previous code cell

Próximas etapas

Recomendações

Se você achou este trabalho interessante, talvez se interesse pelo material a seguir:

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