Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Запуск квантових робочих навантажень за допомогою QRMI

Оцінка часу виконання: менше однієї хвилини на апаратному забезпеченні IBM Quantum® для розділу SQD. Ця оцінка не включає час очікування в черзі та класичну обробку; час виконання може відрізнятися.

Результати навчання​

  1. Роль, яку QRMI відіграє як проміжне програмне забезпечення між планувальниками HPC та апаратним забезпеченням IBM Quantum

  2. Як використовувати основний життєвий цикл QRMI (acquire → task_start → task_status → task_result → release) на реальному бекенді IBM®

  3. Як використовувати обгортки вищого рівня Qiskit SamplerV2 і QRMIService поверх QRMI

  4. Як планувальники HPC (Slurm) передають квантові ресурси через змінні середовища і як застосунки їх споживають

  5. Як запустити повний хімічний робочий процес SQD (Sample-based Quantum Diagonalization) на N2_2, використовуючи апаратне забезпечення IBM через QRMI

Попередні вимоги​

Загальні відомості​

Виклик інтеграції квантових обчислень і HPC​

Робочі процеси високопродуктивних обчислень (HPC) часто потребують безшовної координації між класичними обчислювальними кластерами та квантовими процесорами (QPU). Різні квантові апаратні бекенди та сервіси надають різні механізми автентифікації, формати передачі даних та API життєвого циклу завдань. Інтеграція систем IBM Quantum у менеджери робочих навантажень HPC (наприклад, Slurm) вимагає чіткого стандартного інтерфейсу для отримання ресурсів, виконання завдань та керування сесіями.

Що таке QRMI​

Quantum Resource Management Interface (QRMI) — це проміжна бібліотека (middleware), написана мовою Rust, яка стандартизує доступ до квантового апаратного забезпечення з боку HPC-планувальників та класичних застосунків. Вона надає єдиний уніфікований 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

QRMI опубліковано як проєкт з відкритим кодом на github.com/qiskit-community/qrmi, і його описано в оглядовій статті arXiv:2506.10052.

Ключові рішення щодо дизайну​

Життєвий цикл ресурсу, а не компіляція схеми. QRMI обробляє лише життєвий цикл acquire/submit/poll/release і нічого більше. Компіляція схеми, оптимізація та транспіляція залишаються на рівні застосунку (наприклад, у Qiskit). Це робить інтерфейс мінімальним та композиційним.

Модель переносимості між постачальниками. Хоча QRMI надає спільні виклики керування завданнями (acquire, task_start, task_status, task_result, release) для всіх підтримуваних апаратних бекендів, зміна постачальника також вимагає інших проходів компіляції, специфічної для постачальника побудови корисного навантаження (payload) та декодування результатів на рівні застосунку.

Власний формат корисного навантаження IBM. Для бекендів IBM Quantum QRMI використовує JSON-навантаження OpenQASM 3 (QiskitPrimitive), що відповідають схемі Qiskit Runtime.

Конфігурація через змінні середовища. Облікові дані та URL-адреси кінцевих точок зчитуються зі змінних середовища під час виконання. У кластері HPC плагін Slurm QRMI SPANK встановлює їх автоматично, коли завдання запускається на виконання. У ноутбуці або інтерактивній сесії ти завантажуєш їх із файлу .env. Код застосунку ніколи не містить жорстко закодованих облікових даних чи URL-адрес кінцевих точок.

Інтеграція з планувальником HPC через GRES. Коли завдання Slurm запитує квантові ресурси за допомогою інтерфейсу плагіна QRMI SPANK (#SBATCH --gres=qpu:1 та #SBATCH --qpu=ibm_kingston), плагін додає QRMI_JOB_QPU_RESOURCES та QRMI_JOB_QPU_TYPES до середовища завдання. Застосунки викликають get_job_qpu_resources_and_types(), щоб дізнатися, які ресурси було виділено — без потреби в жорстко закодованих назвах бекендів. QRMIService інкапсулює цей шаблон для користувачів Qiskit.

Основні виклики API​

ВикликПризначення
qrmi.acquire()Отримати доступ до ресурсу (наприклад, відкриває виділену сесію); повертає токен блокування
qrmi.target()Отримати можливості бекенда (кубіти, вентилі, карту з'єднань) у форматі JSON
qrmi.task_start(payload)Надіслати квантове завдання; повертає ID завдання
qrmi.task_status(job_id)Опитати статус завдання (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Отримати результати завершеного завдання у вигляді необробленого рядка JSON
qrmi.task_stop(job_id)Скасувати або очистити завдання
qrmi.release(lock)Звільнити блокування ресурсу (наприклад, закриває сесію)

Що охоплює цей туторіал​

Цей туторіал складається з двох частин:

Кроки 1–3 (приклади малого масштабу): Знайомлять з API QRMI на прикладі демонстрації простої схеми стану Белла на апаратному забезпеченні IBM Quantum, охоплюючи як пряме використання низькорівневих примітивів, так і високорівневу інтеграцію QRMIService та SamplerV2.

Приклад великого масштабу на апаратному забезпеченні: Повний робочий процес SQD для молекули N2_2 на відстані зв'язку 1.0 A˚\AA (активний простір базису cc-pVDZ, 26 просторових орбіталей / 52 кубіти), виконаний на апаратному забезпеченні IBM Quantum через QRMI. SQD поєднує квантове семплювання анзацу LUCJ, побудованого за допомогою ffsim, із самоузгодженим відновленням конфігурацій за допомогою 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")

Вимоги​

Перш ніж почати цей туторіал, переконайся, що в тебе встановлено таке.

Налаштування середовища Python​

Попередньо зібрані бінарні wheel-пакети доступні для Linux на PyPI, тому стандартна команда pip install працює безпосередньо в системах 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"
Платформи без попередньо зібраних wheel-пакетів

Якщо pip збирає QRMI з вихідного коду, переконайся, що в тебе встановлено сучасний набір інструментів Rust (Rust ≥ 1.91.1, встановлений через rustup з rustup.rs).

Виберіть ядро QRMI IBM у Jupyter, потім перезапустіть його та виконайте комірки ноутбука по порядку. Збережені результати отримано з апаратного запуску контриб'ютора; команди встановлення не вказують точні версії, використані для цього запуску.

Необхідні облікові дані​

Для автономного виконання створіть файл .env поруч із цим ноутбуком із наведеними нижче значеннями, замінивши заповнювачі облікових даних. Тримайте цей файл приватним. Якщо ви оберете інший бекенд, оновіть як його назву, так і префікси змінних середовища.

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

Для виділення ресурсів через Slurm використовуйте налаштування ресурсів та облікові дані, надані кластером. Ноутбук зберігає наявні значення середовища.

Налаштування​

Імпортуйте залежності та завантажте конфігурацію ресурсу.

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.

Приклади малого масштабу​

Кроки 1–3 знайомлять з API QRMI за допомогою простих схем. Кожен крок відповідає основному етапу життєвого циклу QRMI на апаратному забезпеченні IBM Quantum.

Корисним навантаженням для цих початкових кроків є невелика схема стану Белла, обрана так, щоб виконуватися швидко та недорого.

Ці приклади використовують апаратне забезпечення, оскільки вони демонструють віддалене виділення ресурсів та керування завданнями. Локальний симулятор схем не перевіряє інтеграцію сервісу QRMI та планувальника. Виконання цього ноутбука надсилає завдання IBM Quantum і вимагає доступу до налаштованого бекенда.

Крок 1: Зіставлення класичної задачі з квантовим ресурсом​

Перший крок у будь-якому робочому процесі QRMI — створити об'єкт QuantumResource і перевірити його доступність.

get_target() отримує апаратний опис бекенда (кількість кубітів, базові вентилі, карту з'єднань) і пакує його як об'єкт Qiskit Target, який транспілятор використовує на кроці 2.

Крок 2: Оптимізація задачі для виконання на квантовому апаратному забезпеченні​

Перед надсиланням використайте Qiskit, щоб транспілювати схему до архітектури набору інструкцій (ISA) бекенда, використовуючи об'єкт Target, отриманий на кроці 1.

Далі приклад будує Payload.QiskitPrimitive, який обгортає рядок схеми OpenQASM 3 та метадані завдання у схему примітивів IBM.

Крок 3: Виконання за допомогою примітивів QRMI​

Після побудови корисного навантаження приклад надсилає завдання та опитує його до завершення. task_start() одразу повертає ID завдання; task_status() опитується, доки статус не перестане бути Queued/Running. Результати отримуються у вигляді необробленого рядка JSON і розбираються для видобування вибірок вимірювань.

Наступна комірка об'єднує отримання ресурсу, виконання та очищення разом, щоб збої після отримання ресурсу все одно звільняли сесію, що належить ноутбуку.

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

Високорівневий інтерфейс Qiskit: QRMIService та SamplerV2​

Наведений вище необроблений життєвий цикл забезпечує явний контроль над кожним викликом. Для стандартних робочих процесів Qiskit QRMI надає примітив SamplerV2, що реалізує BaseSamplerV2.

Керування життєвим циклом

SamplerV2 обробляє серіалізацію корисного навантаження, надсилання (task_start), опитування та декодування результатів. У пакетному налаштуванні HPC (наприклад, зі Slurm) виділенням і звільненням керують планувальник та плагін SPANK. В інтерактивній сесії Python з використанням прямих низькорівневих об'єктів API acquire() та 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}

Контекст HPC: впровадження ресурсів Slurm​

У кластері HPC користувачі запитують квантові ресурси за допомогою синтаксису Slurm GRES разом з опціями плагіна QRMI SPANK. Плагін автоматично обробляє впровадження облікових даних та ресурсів:

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

Код застосунку виявляє свої виділені ресурси під час виконання — без жорстко закодованих назв бекендів:

# 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

Приклад великого масштабу на апаратному забезпеченні: SQD на N2_2​

Тут ми об'єднуємо всі компоненти в повний робочий процес квантової хімії більшого масштабу, виконаний на реальному апаратному забезпеченні IBM Quantum через QRMI.

SQD поєднує таке:

  1. Квантове семплювання анзацу Local Unitary Cluster Jastrow (LUCJ), побудованого за допомогою ffsim та ініціалізованого з амплітуд CCSD
  2. Апаратно-орієнтована транспіляція, що відповідає топології решітки heavy-hex, через generate_lucj_pass_manager
  3. Виконання семплювання на апаратному забезпеченні IBM Quantum, кероване через QRMIService та QRMI SamplerV2
  4. Класична постобробка: самоузгоджене відновлення конфігурацій та ітеративна діагоналізація підпростору за допомогою qiskit-addon-sqd

Ми застосовуємо SQD до N2_2 на відстані зв'язку 1.0 A˚\AA з активним простором, отриманим з базисного набору cc-pVDZ (26 просторових орбіталей, що відповідає 52 спін-орбіталям/кубітам).

Референтна енергія для активного простору N2_2 /cc-pVDZ (відстань зв'язку 1.0 A˚\AA):

  • Референтна енергія (окремий розрахунок SCI): −109.22802922 Ha
Точність збереженого запуску

Наведений нижче запуск SQD демонструє успішне наскрізне виконання QRMI на апаратному забезпеченні IBM Quantum. З одним повторенням LUCJ і 100 000 запусків (shots) отримана енергія виявляється приблизно на 23,7 ккал/моль вищою за референтну і не досягає хімічної точності (≤ 1 ккал/моль). Зміна n_reps, кількості запусків (shots) або кількості ітерацій SQD може покращити точність, але потребує подальшого тестування.

У збереженому запуску менеджер проходів ffsim видалив взаємодії протилежних спінів (24, 24) та (20, 20), оскільки бекенд не міг їх обробити. Наведені результати використовують цю скориговану схему.

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

Наступні кроки​

Рекомендації

Якщо ця робота зацікавила тебе, тебе також можуть зацікавити такі матеріали: