Przejdź do głównej treści
Ten projekt jest w wersji beta. Znalazłeś błąd? Otwórz zgłoszenie na GitHub. Masz pytanie lub pomysł? Zapytaj w Discussions — chętnie poznamy Twoją opinię!
Jak oceniasz jakość tłumaczenia tej strony?

Uruchamiaj obciążenia kwantowe za pomocą QRMI

Szacowany czas: poniżej jednej minuty na sprzęcie IBM Quantum® dla sekcji SQD. Ten szacunek nie uwzględnia czasu oczekiwania w kolejce ani przetwarzania klasycznego; rzeczywisty czas wykonania może się różnić.

Efekty uczenia​

  1. Rola, jaką QRMI odgrywa jako oprogramowanie pośredniczące między harmonogramami HPC a sprzętem IBM Quantum

  2. Jak używać podstawowego cyklu życia QRMI (acquire → task_start → task_status → task_result → release) w odniesieniu do rzeczywistego backendu IBM®

  3. Jak używać wysokopoziomowych wrapperów Qiskit SamplerV2 i QRMIService na bazie QRMI

  4. Jak harmonogramy HPC (Slurm) wstrzykują zasoby kwantowe za pomocą zmiennych środowiskowych i jak aplikacje z nich korzystają

  5. Jak uruchomić kompletny przepływ pracy chemii SQD (Sample-based Quantum Diagonalization) dla N2_2, używając sprzętu IBM poprzez QRMI

Wymagania wstępne​

Kontekst​

Wyzwanie integracji kwantowo-HPC​

Obliczenia wysokiej wydajności (HPC) często wymagają płynnej koordynacji między klasycznymi klastrami obliczeniowymi a jednostkami przetwarzania kwantowego (QPU). Różne kwantowe backendy sprzętowe i usługi udostępniają odmienne mechanizmy uwierzytelniania, formaty przesyłania danych oraz interfejsy API cyklu życia zadań. Integracja systemów IBM Quantum z menedżerami obciążeń HPC (takimi jak Slurm) wymaga czystego, standardowego interfejsu do pozyskiwania zasobów, wykonywania zadań i zarządzania sesjami.

Czym jest QRMI​

Quantum Resource Management Interface (QRMI) to biblioteka pośrednicząca (middleware) napisana w języku Rust, która standaryzuje dostęp do sprzętu kwantowego z poziomu harmonogramów zadań HPC i aplikacji klasycznych. Udostępnia pojedynczy, ujednolicony interfejs API cyklu życia:

┌─────────────────────────────────────────────────────────────────┐
│ 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

QRMI jest publikowane jako projekt open source pod adresem github.com/qiskit-community/qrmi i zostało opisane w artykule przeglądowym arXiv:2506.10052.

Kluczowe decyzje projektowe​

Cykl życia zasobu, a nie kompilacja obwodu. QRMI obsługuje wyłącznie cykl życia acquire/submit/poll/release. Kompilacja obwodu, optymalizacja i transpilacja pozostają w warstwie aplikacji (na przykład w Qiskit). Dzięki temu interfejs pozostaje minimalny i modułowy.

Model przenośności między dostawcami. Choć QRMI zapewnia wspólne wywołania zarządzania zadaniami (acquire, task_start, task_status, task_result, release) dla obsługiwanych backendów sprzętowych, zmiana dostawcy wymaga również innych przebiegów kompilacji, budowy ładunku (payload) specyficznego dla dostawcy oraz dekodowania wyników w warstwie aplikacji.

Natywny format ładunku IBM. Dla backendów IBM Quantum, QRMI używa ładunków JSON w formacie OpenQASM 3 (QiskitPrimitive), zgodnych ze schematem Qiskit Runtime.

Konfiguracja za pomocą zmiennych środowiskowych. Dane uwierzytelniające i adresy URL punktów końcowych są odczytywane ze zmiennych środowiskowych w czasie działania. W klastrze HPC wtyczka Slurm QRMI SPANK ustawia je automatycznie podczas wysyłania zadania. W notatniku lub sesji interaktywnej wczytujesz je z pliku .env. Kod aplikacji nigdy nie zawiera zakodowanych na stałe danych uwierzytelniających ani adresów URL punktów końcowych.

Integracja z harmonogramem HPC za pomocą GRES. Gdy zadanie Slurm żąda zasobów kwantowych za pomocą interfejsu wtyczki QRMI SPANK (#SBATCH --gres=qpu:1 oraz #SBATCH --qpu=ibm_kingston), wtyczka wstrzykuje QRMI_JOB_QPU_RESOURCES i QRMI_JOB_QPU_TYPES do środowiska zadania. Aplikacje wywołują get_job_qpu_resources_and_types(), aby odkryć, które zasoby zostały przydzielone — bez potrzeby podawania nazw backendów na stałe. QRMIService opakowuje ten wzorzec dla użytkowników Qiskit.

Podstawowe wywołania API​

WywołanieCel
qrmi.acquire()Uzyskuje dostęp do zasobu (na przykład otwiera dedykowaną sesję); zwraca token blokady
qrmi.target()Pobiera możliwości backendu (kubity, bramki, mapę sprzężeń) w formacie JSON
qrmi.task_start(payload)Wysyła zadanie kwantowe; zwraca identyfikator zadania
qrmi.task_status(job_id)Odpytuje o status zadania (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Pobiera wyniki ukończonego zadania jako surowy ciąg JSON
qrmi.task_stop(job_id)Anuluje lub czyści zadanie
qrmi.release(lock)Zwalnia blokadę zasobu (na przykład zamyka sesję)

Co obejmuje ten samouczek​

Ten samouczek podzielony jest na dwie części:

Kroki 1–3 (przykłady na małą skalę): Wprowadzają API QRMI za pomocą prostej demonstracji obwodu stanu Bella na sprzęcie IBM Quantum, obejmując zarówno bezpośrednie użycie niskopoziomowych prymitywów, jak i integrację wysokopoziomową za pomocą QRMIService i SamplerV2.

Przykład na dużą skalę na sprzęcie rzeczywistym: Kompletny przepływ pracy SQD dla cząsteczki N2_2 przy długości wiązania 1.0 A˚\AA (przestrzeń aktywna bazy cc-pVDZ, 26 orbitali przestrzennych / 52 kubity), wykonany na sprzęcie IBM Quantum za pośrednictwem QRMI. SQD łączy próbkowanie kwantowe ansatzu LUCJ skonstruowanego za pomocą ffsim z samouzgodnionym odzyskiwaniem konfiguracji za pomocą 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")

Wymagania​

Zanim rozpoczniesz ten samouczek, upewnij się, że masz zainstalowane następujące elementy.

Konfiguracja środowiska Python​

Gotowe pakiety binarne (wheels) są dostępne dla systemu Linux w PyPI, dzięki czemu standardowe polecenie pip install działa bezpośrednio w systemach 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"
Platformy bez gotowych pakietów binarnych

Jeśli pip buduje QRMI ze źródeł, upewnij się, że masz nowoczesny zestaw narzędzi Rust (Rust ≥ 1.91.1 zainstalowany za pomocą rustup z rustup.rs).

Wybierz jądro QRMI IBM w Jupyter, następnie uruchom je ponownie i wykonaj komórki notatnika po kolei. Zapisane wyniki pochodzą z uruchomienia na sprzęcie wykonanego przez współautora; polecenia instalacyjne nie określają dokładnych wersji użytych podczas tego uruchomienia.

Wymagane dane uwierzytelniające​

W przypadku samodzielnego uruchomienia utwórz plik .env obok tego notatnika z następującymi wartościami, zastępując symbole zastępcze danych uwierzytelniających. Zachowaj ten plik jako prywatny. Jeśli wybierzesz inny backend, zaktualizuj zarówno jego nazwę, jak i prefiksy zmiennych środowiskowych.

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

W przypadku alokacji Slurm użyj ustawień zasobów i danych uwierzytelniających dostarczonych przez klaster. Notatnik zachowuje istniejące wartości środowiskowe.

Konfiguracja​

Zaimportuj zależności i wczytaj konfigurację zasobów.

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.

Przykłady na małą skalę​

Kroki 1–3 wprowadzają API QRMI za pomocą prostych obwodów. Każdy krok odpowiada podstawowej fazie cyklu życia QRMI w odniesieniu do sprzętu IBM Quantum.

Ładunkiem dla tych początkowych kroków jest mały obwód stanu Bella, wybrany tak, aby jego wykonanie było szybkie i tanie.

Te przykłady wykorzystują rzeczywisty sprzęt, ponieważ demonstrują zdalną alokację zasobów i zarządzanie zadaniami. Lokalny symulator obwodów nie weryfikuje usługi QRMI ani integracji z harmonogramem zadań. Uruchomienie tego notatnika wysyła zadania IBM Quantum i wymaga dostępu do skonfigurowanego backendu.

Krok 1: Odwzorowanie problemu klasycznego na zasób kwantowy​

Pierwszym krokiem w każdym przepływie pracy QRMI jest utworzenie obiektu QuantumResource i sprawdzenie, czy jest dostępny.

get_target() pobiera opis sprzętowy backendu (liczbę kubitów, bramki bazowe, mapę sprzężeń) i pakuje go jako obiekt Qiskit Target, którego transpiler używa w Kroku 2.

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym​

Przed wysłaniem użyj Qiskit, aby stranspilować obwód do architektury zestawu instrukcji (ISA) backendu, korzystając z obiektu Target pobranego w Kroku 1.

Przykład następnie buduje Payload.QiskitPrimitive, który opakowuje ciąg obwodu OpenQASM 3 i metadane zadania w schemat prymitywu IBM.

Krok 3: Wykonanie za pomocą prymitywów QRMI​

Po zbudowaniu ładunku przykład wysyła zadanie i odpytuje o jego ukończenie. task_start() natychmiast zwraca identyfikator zadania; task_status() jest odpytywane, dopóki status nie przestanie być Queued/Running. Wyniki są pobierane jako surowy ciąg JSON i analizowane w celu wyodrębnienia próbek pomiarowych.

Poniższa komórka łączy pozyskanie zasobu, wykonanie i sprzątanie w jednym miejscu, dzięki czemu nawet w razie błędu po pozyskaniu zasobu sesja należąca do notatnika zostanie zwolniona.

# ── 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.

Interfejs Qiskit wyższego poziomu: QRMIService i SamplerV2​

Powyższy surowy cykl życia zapewnia jawną kontrolę nad każdym wywołaniem. Dla standardowych przepływów pracy Qiskit, QRMI udostępnia prymityw SamplerV2 implementujący BaseSamplerV2.

Zarządzanie cyklem życia

SamplerV2 obsługuje serializację ładunku, wysyłanie (task_start), odpytywanie oraz dekodowanie wyników. W środowisku wsadowym HPC (na przykład ze Slurm), alokacja i zwalnianie są zarządzane przez harmonogram zadań i wtyczkę SPANK. W interaktywnej sesji Python korzystającej bezpośrednio z niskopoziomowych obiektów API, do jawnego zarządzania dedykowanymi sesjami można użyć acquire() i release().

# 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}

Kontekst HPC: wstrzykiwanie zasobów Slurm​

W klastrze HPC użytkownicy żądają zasobów kwantowych za pomocą składni Slurm GRES wraz z opcjami wtyczki QRMI SPANK. Wtyczka automatycznie obsługuje wstrzykiwanie danych uwierzytelniających i zasobów:

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

Kod aplikacji odkrywa przydzielone mu zasoby w czasie działania — bez zakodowanych na stałe nazw backendów:

# 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

Przykład na dużą skalę na sprzęcie rzeczywistym: SQD dla N2_2​

Tutaj łączymy wszystkie elementy w kompletny przepływ pracy z zakresu chemii kwantowej na większą skalę, wykonywany na rzeczywistym sprzęcie IBM Quantum za pośrednictwem QRMI.

SQD łączy w sobie następujące elementy:

  1. Próbkowanie kwantowe ansatzu Local Unitary Cluster Jastrow (LUCJ) skonstruowanego za pomocą ffsim i zainicjowanego amplitudami CCSD
  2. Transpilację uwzględniającą sprzęt, dopasowaną do topologii sieci heavy-hex za pomocą generate_lucj_pass_manager
  3. Wykonanie próbkowania na sprzęcie IBM Quantum zarządzanym za pomocą QRMIService oraz QRMI SamplerV2
  4. Klasyczne przetwarzanie końcowe: samouzgodnione odzyskiwanie konfiguracji i iteracyjną diagonalizację podprzestrzeni za pomocą qiskit-addon-sqd

Stosujemy SQD do N2_2 przy długości wiązania 1.0 A˚\AA z przestrzenią aktywną wyprowadzoną z zestawu bazowego cc-pVDZ (26 orbitali przestrzennych, odpowiadających 52 spinorbitalom/kubitom).

Energia odniesienia dla przestrzeni aktywnej N2_2 /cc-pVDZ (długość wiązania 1.0 A˚\AA):

  • Energia odniesienia (oddzielne obliczenie SCI): −109.22802922 Ha
Dokładność zapisanego uruchomienia

Poniższe uruchomienie SQD demonstruje pomyślne wykonanie QRMI od początku do końca na sprzęcie IBM Quantum. Przy jednym powtórzeniu LUCJ i 100 000 strzałów końcowy wynik leży około 23,7 kcal/mol powyżej energii odniesienia i nie osiąga dokładności chemicznej (≤ 1 kcal/mol). Zmiana n_reps, liczby strzałów lub liczby iteracji SQD może poprawić dokładność, ale wymaga dalszych testów.

W zapisanym uruchomieniu menedżer przebiegów ffsim usunął interakcje przeciwnych spinów (24, 24) i (20, 20), ponieważ backend nie mógł ich uwzględnić. Zgłoszone wyniki wykorzystują ten dostosowany obwód.

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

Kolejne kroki​

Zalecenia

Jeśli ta praca Cię zainteresowała, mogą Cię zainteresować następujące materiały: