Magpatakbo ng mga quantum workload gamit ang QRMI
Tinatayang paggamit: wala pang isang minuto sa IBM Quantumยฎ hardware para sa seksyon ng SQD. Hindi kasama sa pagtatantyang ito ang queue time at classical processing; maaaring mag-iba ang runtime.
Mga inaasahang matutunanโ
-
Ang papel na ginagampanan ng QRMI bilang middleware sa pagitan ng HPC schedulers at IBM Quantum hardware
-
Kung paano gamitin ang core QRMI lifecycle (
acquireโtask_startโtask_statusโtask_resultโrelease) laban sa isang tunay na IBMยฎ backend -
Kung paano gamitin ang mas mataas na antas na Qiskit
SamplerV2atQRMIServicewrappers sa ibabaw ng QRMI -
Kung paano nag-i-inject ang mga HPC scheduler (Slurm) ng quantum resources sa pamamagitan ng environment variables at kung paano ito ginagamit ng mga application
-
Kung paano magpatakbo ng isang kumpletong SQD (Sample-based Quantum Diagonalization) na chemistry workflow sa N gamit ang IBM hardware sa pamamagitan ng QRMI
Mga Paunang Kailanganโ
-
Pangunahing kaalaman sa Python virtual environments at quantum chemistry
Kaligiranโ
Ang hamon sa integrasyon ng quantum-HPCโ
Ang mga high-performance computing (HPC) workflow ay kadalasang nangangailangan ng maayos na koordinasyon sa pagitan ng mga classical compute cluster at quantum processing unit (QPU). Ang iba't ibang quantum hardware backend at serbisyo ay may kanya-kanyang authentication mechanism, wire format, at job lifecycle API. Ang pag-integrate ng mga IBM Quantum system sa mga HPC workload manager (tulad ng Slurm) ay nangangailangan ng malinis at standard na interface para sa resource acquisition, job execution, at session management.
Ano ang QRMIโ
Ang Quantum Resource Management Interface (QRMI) ay isang middleware library na isinulat sa Rust na nagpapa-standardize ng access sa quantum hardware mula sa mga HPC scheduler at classical application. Naglalantad ito ng iisang unified lifecycle API:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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
Ang QRMI ay pinapublish bilang isang open-source project sa github.com/qiskit-community/qrmi at inilalarawan sa overview paper na arXiv:2506.10052.
Mga pangunahing desisyon sa disenyoโ
Resource lifecycle, hindi circuit compilation. Ang QRMI ay humahawak lamang ng acquire/submit/poll/release lifecycle at wala nang iba pa. Ang Circuit compilation, optimization, at transpilation ay nananatili sa application layer (halimbawa, Qiskit). Pinapanatili nito ang interface na minimal at composable.
Vendor portability model. Bagama't ang QRMI ay nagbibigay ng karaniwang job-management call (acquire, task_start, task_status, task_result, release) sa lahat ng suportadong hardware backend, ang pagpalit ng vendor ay nangangailangan din ng ibang compilation pass, vendor-specific payload construction, at result decoding sa application layer.
Native na IBM payload format. Para sa mga IBM Quantum backend, gumagamit ang QRMI ng OpenQASM 3 JSON payload (QiskitPrimitive) na sumusunod sa Qiskit Runtime schema.
Configuration sa pamamagitan ng environment variable. Ang mga credential at endpoint URL ay binabasa mula sa environment variable sa runtime. Sa isang HPC cluster, awtomatikong itinatakda ito ng Slurm QRMI SPANK plugin kapag na-dispatch ang isang job. Sa isang notebook o interactive session, ilo-load mo ang mga ito mula sa isang .env file. Ang application code ay hindi kailanman naglalaman ng hardcoded na credential o endpoint URL.
Integrasyon ng HPC scheduler sa pamamagitan ng GRES. Kapag humiling ang isang Slurm job ng quantum resource gamit ang QRMI SPANK plugin interface (#SBATCH --gres=qpu:1 at #SBATCH --qpu=ibm_kingston), ini-inject ng plugin ang QRMI_JOB_QPU_RESOURCES at QRMI_JOB_QPU_TYPES sa job environment. Tinatawag ng mga application ang get_job_qpu_resources_and_types() upang malaman kung aling mga resource ang na-allocate โ walang kinakailangang hardcoded na pangalan ng backend. Ang QRMIService ay bumabalot sa pattern na ito para sa mga user ng Qiskit.
Ang mga pangunahing API callโ
| Tawag | Layunin |
|---|---|
qrmi.acquire() | Kumuha ng access sa resource (halimbawa, nagbubukas ng dedicated session); nagbabalik ng lock token |
qrmi.target() | Kumuha ng mga kakayahan ng backend (qubits, gates, coupling map) bilang JSON |
qrmi.task_start(payload) | Magsumite ng quantum job; nagbabalik ng job ID |
qrmi.task_status(job_id) | I-poll ang status ng job (Queued, Running, Completed, Failed) |
qrmi.task_result(job_id) | Kumuha ng nakumpletong resulta ng job bilang raw JSON string |
qrmi.task_stop(job_id) | Kanselahin o linisin ang isang job |
qrmi.release(lock) | I-release ang resource lock (halimbawa, isinasara ang session) |
Ano ang sakop ng tutorial na itoโ
Ang tutorial na ito ay nakaayos sa dalawang bahagi:
Hakbang 1โ3 (maliliit na halimbawa): Ipinakikilala ang QRMI API gamit ang isang simpleng demonstrasyon ng Bell state circuit sa IBM Quantum hardware, na sumasaklaw sa direktang paggamit ng low-level primitive gayundin sa high-level integrasyon ng QRMIService at SamplerV2.
Malaking halimbawa sa hardware: Isang kumpletong SQD workflow para sa molekulang N sa bond distance na 1.0 (cc-pVDZ basis active space, 26 spatial orbitals / 52 qubits), isinagawa sa IBM Quantum hardware sa pamamagitan ng QRMI. Pinagsasama ng SQD ang quantum sampling ng isang LUCJ ansatz na binuo gamit ang ffsim at self-consistent configuration recovery gamit ang 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")
Mga Kinakailanganโ
Bago simulan ang tutorial na ito, tiyaking naka-install na ang mga sumusunod.
Pag-setup ng Python environmentโ
Available ang mga pre-built binary wheel para sa Linux sa PyPI, kaya gumagana nang direkta ang standard na pip install sa mga Linux/HPC system.
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"
Kung bubuuin ng pip ang QRMI mula sa source, tiyaking mayroon kang modernong Rust toolchain (Rust โฅ 1.91.1 na naka-install gamit ang rustup mula sa rustup.rs).
Piliin ang QRMI IBM kernel sa Jupyter, pagkatapos ay i-restart ito at patakbuhin ang mga notebook cell nang sunud-sunod. Ang mga naka-save na output ay mula sa hardware run ng contributor; hindi tinutukoy ng mga installation command ang eksaktong mga bersyon na ginamit para sa run na iyon.
Mga kinakailangang credentialโ
- IBM Quantum: IAM API key at Service CRN mula sa IBM Quantum Platform
Para sa standalone execution, gumawa ng .env file sa tabi ng notebook na ito na may mga sumusunod na value, na papalitan ang mga credential placeholder. Panatilihing pribado ang file na ito. Kung pipili ka ng ibang backend, i-update ang parehong pangalan nito at ang mga environment variable prefix.
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 sa isang Slurm allocation, gamitin ang mga resource setting at credential na ibinigay ng cluster. Pinapanatili ng notebook ang mga umiiral na environment value.
Pag-setupโ
I-import ang mga dependency at i-load ang resource configuration.
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.
Mga maliliit na halimbawaโ
Ipinakikilala ng Hakbang 1โ3 ang QRMI API gamit ang mga simpleng circuit. Bawat hakbang ay tumutugma sa isang pangunahing yugto ng QRMI lifecycle laban sa IBM Quantum hardware.
Ang payload para sa mga paunang hakbang na ito ay isang maliit na Bell state circuit na pinili dahil mabilis at mura itong patakbuhin.
Gumagamit ng hardware ang mga halimbawang ito dahil ipinapakita nila ang remote resource allocation at job management. Ang isang local circuit simulator ay hindi nagpapatunay ng integrasyon ng QRMI service at scheduler. Ang pagpapatakbo ng notebook na ito ay nagsusumite ng mga IBM Quantum job at nangangailangan ng access sa configured na backend.
Hakbang 1: I-map ang classical problem sa isang quantum resourceโ
Ang unang hakbang sa anumang QRMI workflow ay ang paggawa ng QuantumResource object at pagpapatunay na naa-access ito.
Kinukuha ng get_target() ang hardware description ng backend (bilang ng qubit, basis gates, coupling map) at ibinabalot ito bilang isang Qiskit Target object, na ginagamit ng transpiler sa Hakbang 2.
Hakbang 2: I-optimize ang problema para sa quantum hardware executionโ
Bago magsumite, gamitin ang Qiskit upang i-transpile ang circuit patungo sa instruction set architecture (ISA) ng backend, gamit ang Target object na nakuha sa Hakbang 1.
Pagkatapos ay bumubuo ang halimbawa ng Payload.QiskitPrimitive, na binabalot ang OpenQASM 3 circuit string at job metadata sa loob ng IBM primitive schema.
Hakbang 3: Isagawa gamit ang QRMI primitivesโ
Matapos mabuo ang payload, isinusumite ng halimbawa ang job at nagpo-poll para sa completion. Agad na nagbabalik ang task_start() ng job ID; pino-poll ang task_status() hanggang hindi na Queued/Running ang status. Ang mga resulta ay kinukuha bilang raw JSON string at pino-parse upang makuha ang mga measurement sample.
Pinapanatili ng sumusunod na cell ang acquisition, execution, at cleanup nang magkasama upang kahit may pumalya pagkatapos ng acquisition, mare-release pa rin ang session na pag-aari ng 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.
Mas mataas na antas na Qiskit interface: QRMIService at SamplerV2โ
Ang raw lifecycle sa itaas ay nagbibigay ng eksplisitong kontrol sa bawat tawag. Para sa mga standard na Qiskit workflow, nagbibigay ang QRMI ng SamplerV2 primitive na nagpapatupad ng BaseSamplerV2.
Hinahawakan ng SamplerV2 ang payload serialization, submission (task_start), polling, at result decoding. Sa isang HPC batch setting (halimbawa, gamit ang Slurm), ang allocation at release ay pinamamahalaan ng scheduler at ng SPANK plugin. Sa isang interactive na Python session na gumagamit ng direktang low-level na API object, maaaring gamitin ang acquire() at release() upang eksplisitong pamahalaan ang mga dedicated session.
# 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}
Konteksto ng HPC: Slurm resource injectionโ
Sa isang HPC cluster, humihiling ang mga user ng quantum resource gamit ang Slurm GRES syntax kasama ng mga opsyon ng QRMI SPANK plugin. Awtomatikong hinahawakan ng plugin ang credential at resource injection:
#SBATCH --gres=qpu:1
#SBATCH --qpu=ibm_kingston
python my_workflow.py # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already set
Natutuklasan ng application code ang mga na-allocate nitong resource sa runtime โ walang hardcoded na pangalan ng backend:
# 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
Malaking halimbawa sa hardware: SQD sa Nโ
Dito pinagsasama-sama natin ang lahat ng bahagi sa isang kumpletong quantum chemistry workflow sa mas malaking sukat, na isinagawa sa aktwal na IBM Quantum hardware sa pamamagitan ng QRMI.
Pinagsasama ng SQD ang mga sumusunod:
- Quantum sampling ng isang Local Unitary Cluster Jastrow (LUCJ) ansatz na binuo gamit ang
ffsimat ini-initialize mula sa CCSD amplitude - Hardware-aware transpilation na tumutugma sa heavy-hex lattice topology sa pamamagitan ng
generate_lucj_pass_manager - Sampling execution sa IBM Quantum hardware na pinamamahalaan sa pamamagitan ng
QRMIServiceat QRMISamplerV2 - Classical post-processing: self-consistent configuration recovery at iterative subspace diagonalization gamit ang
qiskit-addon-sqd
Inilalapat natin ang SQD sa N sa bond distance na 1.0 na may active space na hinango mula sa cc-pVDZ basis set (26 spatial orbitals, katumbas ng 52 spin-orbitals/qubits).
Reference energy para sa N /cc-pVDZ active space (bond distance 1.0 ):
- Reference energy (hiwalay na SCI calculation): โ109.22802922 Ha
Ipinapakita ng SQD run sa ibaba ang matagumpay na end-to-end na QRMI execution sa IBM Quantum hardware. Gamit ang iisang LUCJ repetition at 100,000 shots, natatapos ang resulta nang humigit-kumulang 23.7 kcal/mol sa itaas ng reference energy at hindi nakakamit ang chemical accuracy (โค 1 kcal/mol). Ang pagbabago ng n_reps, ng shot count, o ng bilang ng SQD iteration ay maaaring magpabuti ng katumpakan, ngunit nangangailangan ito ng karagdagang pagsubok.
Sa naka-save na run, inalis ng ffsim pass manager ang mga opposite-spin interaction na (24, 24) at (20, 20) dahil hindi ito kayang tanggapin ng backend. Ang binagong circuit na ito ang ginamit sa mga iniulat na resulta.
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)

Mga susunod na hakbangโ
Kung nakita mong kawili-wili ang gawaing ito, maaari kang maging interesado sa mga sumusunod na materyal:
-
Tutorial sa sample-based quantum diagonalization โ ang kumpletong SQD chemistry workflow sa IBM Quantum Platform, kasama ang mas malalaking molekula at basis set
-
Sample-based Krylov quantum diagonalization โ isang kaugnay na paraan na gumagamit ng time evolution circuit para sa fermionic lattice models
-
qiskit-addon-sqddocumentation โ kumpletong API reference at karagdagang mga tutorial para sa SQD post-processing library -
QRMI GitHub repository โ source code, karagdagang mga halimbawa ng backend (CUDA-Q, C, Lua)
-
QRMI overview paper โ teknikal na paglalarawan ng QRMI architecture at HPC integration
-
Gabay sa mga session ng IBM Quantum Compute Service โ kung paano nauugnay ang mga session sa QRMI
acquire/releaselifecycle para sa mga IBM backend