Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

Execute cargas de trabalho quânticas com o QRMI

Estimativa de uso: menos de um minuto em hardware IBM Quantum® para a seção de SQD. Esta estimativa exclui o tempo de fila e o processamento clássico; o tempo de execução pode variar.

Resultados de aprendizado​

  1. O papel que o QRMI desempenha como middleware entre agendadores HPC e o hardware IBM Quantum

  2. Como usar o ciclo de vida principal do QRMI (acquire → task_start → task_status → task_result → release) em um backend IBM® real

  3. Como usar os wrappers de alto nível SamplerV2 e QRMIService do Qiskit sobre o QRMI

  4. Como os agendadores 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 (Sample-based Quantum Diagonalization) em N2_2 usando hardware da IBM por meio do QRMI

Pré-requisitos​

Contexto​

O desafio da integração quântica-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 backends e serviços de hardware quântico expõem mecanismos de autenticação, formatos de transmissão e APIs de ciclo de vida de jobs distintos. Integrar sistemas IBM Quantum a gerenciadores de carga de trabalho HPC (como o Slurm) requer uma interface limpa e padronizada para aquisição de recursos, execução de jobs 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 a hardware quântico a partir de agendadores HPC e aplicações clássicas. Ela expõe uma única API de ciclo de vida unificada:

┌─────────────────────────────────────────────────────────────────┐
│ 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, não compilação de circuitos. O QRMI lida com o ciclo de vida de acquire/submit/poll/release e nada mais. A compilação, otimização e transpilação de circuitos permanecem na camada de aplicação (por exemplo, Qiskit). Isso mantém a interface mínima e composável.

Modelo de portabilidade entre fornecedores. Embora o QRMI forneça chamadas comuns de gerenciamento de jobs (acquire, task_start, task_status, task_result, release) entre os backends de hardware suportados, mudar de fornecedor também exige diferentes passes de compilação, construção de payload específica do fornecedor e decodificação de resultados na camada de aplicação.

Formato de payload nativo da IBM. Para os backends do IBM Quantum, o QRMI usa payloads JSON do OpenQASM 3 (QiskitPrimitive) em conformidade com o schema do Qiskit Runtime.

Configuração por meio de variáveis de ambiente. As credenciais e as URLs de endpoint são lidas a partir de variáveis de ambiente em tempo de execução. Em um cluster HPC, o plugin QRMI SPANK do Slurm as define automaticamente quando um job é despachado. Em um notebook ou sessão interativa, você as carrega a partir de um arquivo .env. O código da aplicação nunca contém credenciais ou URLs de endpoint fixas no código.

Integração com o agendador HPC por meio de GRES. Quando um job do Slurm solicita recursos quânticos usando a interface do plugin QRMI SPANK (#SBATCH --gres=qpu:1 e #SBATCH --qpu=ibm_kingston), o plugin injeta QRMI_JOB_QPU_RESOURCES e QRMI_JOB_QPU_TYPES no ambiente do job. As aplicações chamam get_job_qpu_resources_and_types() para descobrir quais recursos foram alocados — sem necessidade de nomes de backend fixos no código. O QRMIService encapsula esse padrão para usuários do Qiskit.

As chamadas centrais da API​

ChamadaFinalidade
qrmi.acquire()Adquire acesso ao recurso (por exemplo, abre uma sessão dedicada); retorna um token de bloqueio
qrmi.target()Recupera as capacidades do backend (qubits, portas, mapa de acoplamento) como JSON
qrmi.task_start(payload)Envia um job quântico; retorna um ID de job
qrmi.task_status(job_id)Consulta o status do job (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Recupera os resultados do job concluído como uma string JSON bruta
qrmi.task_stop(job_id)Cancela ou limpa um job
qrmi.release(lock)Libera o bloqueio do recurso (por exemplo, encerra a sessão)

O que este tutorial aborda​

Este tutorial está estruturado em duas partes:

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

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

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"dotenv": "python-dotenv", "ffsim": "ffsim", "matplotlib": "matplotlib", "numpy": "numpy", "pyscf": "pyscf", "qiskit": "qiskit", "qiskit_addon_sqd": "qiskit-addon-sqd", "qrmi": "qrmi"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")

Requisitos​

Antes de iniciar este tutorial, certifique-se de ter o seguinte instalado.

Configuração do ambiente Python​

Wheels binários pré-compilados estão disponíveis para Linux no PyPI, portanto o pip install padrão 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 wheels pré-compilados

Se o pip compilar o QRMI a partir do código-fonte, certifique-se de ter um toolchain moderno do Rust (Rust ≥ 1.91.1 instalado via rustup a partir de rustup.rs).

Selecione o kernel QRMI IBM no Jupyter, reinicie-o e execute as células do notebook em ordem. As saídas salvas são da execução em hardware do contribuidor; os comandos de instalação não especificam as versões exatas usadas nessa execução.

Credenciais necessárias​

Para execução independente, crie um arquivo .env ao lado deste notebook com os seguintes valores, substituindo os espaços reservados de credenciais. Mantenha esse arquivo privado. Se você selecionar um backend diferente, atualize tanto o nome 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, use as configurações de recursos e as credenciais fornecidas pelo cluster. O notebook preserva os valores de ambiente existentes.

Configuração​

Importe as dependências e carregue a configuração do recurso.

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.")
Backend: ibm_kingston
Environment ready.

Exemplos em pequena escala​

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

O payload dessas etapas iniciais é um pequeno circuito de estado de Bell escolhido por ser rápido e barato de executar.

Esses exemplos usam hardware porque demonstram a alocação remota de recursos e o gerenciamento de jobs. Um simulador de circuitos local não valida a integração do serviço QRMI e do agendador. Executar este notebook envia jobs ao IBM Quantum e requer acesso ao backend configurado.

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

A primeira etapa 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 empacota como um objeto Target do Qiskit, que o transpilador usa na Etapa 2.

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

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

Em seguida, o exemplo constrói um Payload.QiskitPrimitive, que envolve a string do circuito OpenQASM 3 e os metadados do job no schema de primitivas da IBM.

Etapa 3: Execute usando primitivas do QRMI​

Com o payload construído, o exemplo envia o job e consulta seu andamento até a conclusão. task_start() retorna um ID de job imediatamente; task_status() é consultado até que o status deixe de ser Queued/Running. Os resultados são recuperados como uma string JSON bruta e analisados para extrair as amostras de medição.

A célula a seguir mantém a aquisição, a execução e a limpeza juntas, de forma que falhas após a aquisição ainda liberem uma sessão pertencente ao notebook.

# ── 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.")
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 Qiskit de nível mais alto: QRMIService e SamplerV2​

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

Gerenciamento do ciclo de vida

O SamplerV2 cuida da serialização do payload, do envio (task_start), da consulta de status e da decodificação dos resultados. Em um ambiente de lote HPC (por exemplo, com Slurm), a alocação e a liberação são gerenciadas pelo agendador e pelo plugin SPANK. Em uma sessão interativa do Python usando objetos diretos de 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()}")
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 HPC: injeção de recursos do Slurm​

Em um cluster HPC, os usuários solicitam recursos quânticos usando a sintaxe GRES do Slurm junto com as opções do plugin QRMI SPANK. O plugin 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 da aplicação descobre os recursos alocados a ele em tempo de execução — sem nomes de backend fixos no código:

# 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()}"
)
Resources allocated by scheduler:
ibm_kingston (ibm-quantum-compute-service)

QRMIService found: ibm_kingston accessible=True

Exemplo de hardware em larga escala: SQD em N2_2​

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

O SQD combina o seguinte:

  1. Amostragem quântica de um ansatz Local Unitary Cluster Jastrow (LUCJ) construído usando ffsim e inicializado a partir de amplitudes CCSD
  2. Transpilação adaptada ao hardware, correspondendo à topologia de rede heavy-hex por meio de generate_lucj_pass_manager
  3. Execução de amostragem em hardware IBM Quantum gerenciada por meio de QRMIService e SamplerV2 do QRMI
  4. Pós-processamento clássico: recuperação de configuração autoconsistente e diagonalização iterativa de subespaço usando qiskit-addon-sqd

Aplicamos o SQD a N2_2 a uma distância de ligação de 1,0 A˚\AA com um espaço ativo derivado do conjunto de base cc-pVDZ (26 orbitais espaciais, correspondendo a 52 spin-orbitais/qubits).

Energia de referência para o espaço ativo N2_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 de SQD abaixo demonstra uma execução completa e bem-sucedida do QRMI em hardware IBM Quantum. Com uma única repetição de LUCJ e 100.000 shots, o resultado termina aproximadamente 23,7 kcal/mol acima da energia de referência e não atinge a precisão química (≤ 1 kcal/mol). Alterar n_reps, o número de shots ou o número de iterações do SQD pode melhorar a precisão, mas requer testes adicionais.

Na execução salva, o pass manager do ffsim removeu as interações de spin oposto (24, 24) e (20, 20) porque o backend não conseguiu acomodá-las. Os resultados relatados usam 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()
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óximos passos​

Recomendações

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