Implicit solvent calculations using Qiskit Serverless
Această pagină nu a fost încă tradusă în limba română. Vizualizați versiunea originală în limba engleză.
Usage estimate: 2 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime may vary.)
Learning outcomes
After going through this tutorial, users should understand:
- How to configure and run a remote workflow using Qiskit Serverless
- How to compute implicit solvent effects using a quantum computer
Prerequisites
We suggest that users are familiar with the following topic before going through this tutorial:
Background
Implicit solvent calculations are frequently used in computational biophysics. These models describe how a solute compound interacts with a solvent, without modeling the solvent system directly. Instead, an approximation is made wherein the solute system model is wrapped in a mathematical representation of a dielectric medium characterized empirically. This dielectric approximation then interacts with the solute, which is itself modeled directly. The dielectric medium influences characteristics of the solute system, such as its ground state energy, by interacting with its electron field. This is important to biophysical models used, say, in drug discovery, because compounds behave differently in various dielectric environments. Modeling a compound in the air (in vacuo) will describe different behavior than modeling it in water. Since pharmaceutical compounds must enter into the human body, which itself is composed mostly of water, it is helpful to model a compound in a solution like water instead of in vacuo. With implicit solvent models, we can achieve this behavior cheaply, although the end result will generally be more approximate than the counterpart: more computationally costly explicit solvent models that create direct representations of both solute and solvent molecules.
In this tutorial, we demonstrate how a quantum algorithm, Sample-based Quantum Diagonalization (SQD), can be worked into a relatively computationally cheap implicit solvent model. In the example, we describe how methylamine behaves when it dissolves in water. We compare the quantum algorithm to a classical state-of-the-art comparison method called CASCI and demonstrate close agreement between these computations. We showcase a quantum-centric supercomputing architecture in miniature, offloading the computationally expensive classical post-processing of the quantum sampling portion of the routine into a cloud-based environment within Qiskit Serverless. The code also demonstrates parallelization across the remotely available CPU cores to improve computation time.
Qiskit Serverless is a framework for running distributed quantum and classical workloads without managing infrastructure. There is no server provisioning (no spinning up EC2s, clusters, Docker containers), no orchestration tools (Kubernetes, Docker Swarm) and no monitoring/maintenance. Each Serverless job runs in a clean container, executes your code, and then shuts down. There is no memory between jobs. You simply write your code then submit your job. Within a Serverless job, a program can seamlessly access IBM Quantum® backends and classically post-process results therein. With Qiskit Serverless, users can access always-on remote CPU cores and memory, which provides for the distribution of certain classical workloads across remote resources. Users also gain some advantage in parallel processing of programs while avoiding common headaches from device shutdown mid-execution. For more information on Qiskit Serverless, see its documentation, as well as additional material on GitHub.
This tutorial shows a relevant application of the following:
- Sample-based quantum diagonalization
- Client-server computational models for quantum computing
This tutorial is inspired by and based upon research conducted at Cleveland Clinic, as described in Kaliakin, Danil, et al. "Implicit solvent sample-based quantum diagonalization." The Journal of Physical Chemistry B 129.23 (2025): 5788-5796, exposing the full workflow for implicit solvent calculations and extending it with iterative solvent self-consistency ("The Heartwood Algorithm", M. Motta, T. Pellegrini, 2025), geometry optimization, and automatic qubit layout selection. Please refer to the SQD IEF-PCM Qiskit Function template, jointly developed by Cleveland Clinic and IBM® based on Cleveland Clinic research, for a streamlined, black-box interface for running implicit solvent calculations.
Requirements
Before starting this tutorial, be sure you have the following installed:
- Qiskit SDK v2.0 or later with visualization support
- Qiskit Runtime v0.40 or later (
pip install qiskit-ibm-runtime) - Qiskit SDK v2.0 or later with visualization support
pip install qiskit[visualization] - Qiskit Runtime v0.40 or later
pip install qiskit_ibm_runtime - Qiskit IBM Catalog
pip install qiskit_ibm_catalog - Qiskit IBM Serverless
pip install qiskit_serverless - Qiskit addon: Sample-based quantum diagonalization (SQD) v0.12.0
pip install qiskit_addon_sqd - PySCF
pip install pyscf - FFSIM
pip install ffsim - Matplotlib
pip install matplotlib - Geometric
pip install geometric
Setup
# Added by doQumentation — required packages for this notebook
!pip install -q ffsim matplotlib numpy psutil pyscf qiskit qiskit-addon-sqd qiskit-ibm-catalog qiskit-ibm-runtime qiskit-serverless rustworkx
# Establish Quantum Resource connection
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy()
print(f"Using backend {backend.name}")
# Establish Classical HPC Resource connection
from qiskit_ibm_catalog import QiskitFunction, QiskitServerless
client = QiskitServerless()
Create a directory exactly next to the main notebook program called source_files. You will put Python files into this directory that you intend to share with the remote compute environment. You must create two files:
source_files\diagonalization_engine.pysource_files\classical_simulation.py
Click to expand the text for each script below, then copy and paste the content into a local file with these path names.
Click to view source_files\diagonalization_engine.py
Click to view source_files\classical_simulation.py
For more information, please see the SQD IEF-PCM Qiskit Function template guide (jointly developed by Cleveland Clinic and IBM) referenced above. See also the qiskit_addon_sqd library.
#!/usr/bin/env python3
import numpy as np
from json.encoder import JSONEncoder
from json.decoder import JSONDecoder
from functools import partial
import os
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_serverless import distribute_task, get_arguments, get, save_result
from qiskit_addon_sqd.fermion import (
SCIResult,
diagonalize_fermionic_hamiltonian,
solve_sci,
)
### Argument retrieval
args = get_arguments()
data = args["data"] # Chemistry Data
energy_tol = args["energy_tol"] # SQD option
occupancies_tol = args["occupancies_tol"] # SQD option
max_iterations = args["max_iterations"] # SQD option
symmetrize_spin = args["symmetrize_spin"] # Eigenstate solver option
carryover_threshold = args["carryover_threshold"] # Eigenstate solver option
num_batches = args["num_batches"] # Eigenstate solver option
samples_per_batch = args["samples_per_batch"] # Eigenstate solver option
max_cycle = args["max_cycle"] # Eigenstate solver option
mem = args["mem"] # Memory per Worker
# --- fan‑out target: 1 CPU + mem GB RAM per call -------------
@distribute_task(target={"cpu": 1, "mem": mem * 1024**3})
def _solve_sci_worker(
ix, ci_strs, one_body_tensor, two_body_tensor, norb, nelec, spin_sq
):
print(f">>>>> WORKER {ix} INITIATED")
res = solve_sci(
ci_strs,
one_body_tensor,
two_body_tensor,
norb=norb,
nelec=nelec,
spin_sq=spin_sq,
)
print(f">>>>> WORKER {ix} COMPLETE")
return res
def distribute_solve_sci_batch(
ci_strings: list[tuple[np.ndarray, np.ndarray]],
one_body_tensor: np.ndarray,
two_body_tensor: np.ndarray,
norb: int,
nelec: tuple[int, int],
*,
spin_sq: float | None = None,
**kwargs,
) -> list[SCIResult]:
"""Diagonalize Hamiltonian in subspaces, parallelizing across
vCPUs in the Serverless environment.
Args:
ci_strings: List of pairs (strings_a, strings_b) of arrays of
spin-alpha CI strings and spin-beta CI strings whose Cartesian
product gives the basis of the subspace in which to perform a
diagonalization.
one_body_tensor: The one-body tensor of the Hamiltonian.
two_body_tensor: The two-body tensor of the Hamiltonian.
norb: The number of spatial orbitals.
nelec: The numbers of alpha and beta electrons.
spin_sq: Target value for the total spin squared for the ground state.
If ``None``, no spin will be imposed.
**kwargs: Keyword arguments to pass to
`pyscf.fci.selected_ci.kernel_fixed_space`
(https://pyscf.org/pyscf_api_docs/pyscf.fci.html#pyscf.fci.selected_ci.kernel_fixed_space
Returns:
The results of the diagonalizations in the subspaces given by ci_strings.
"""
inputs = [
(ix, ci_strs, one_body_tensor, two_body_tensor, norb, nelec, spin_sq)
for ix, ci_strs in enumerate(ci_strings)
]
# fan‑out: spawn one worker per input tuple
print(">>>>> ENTERING WORKER FAN-OUT")
refs = [_solve_sci_worker(*input_) for input_ in inputs]
print(">>>>> WAITING ON WORKERS TO FINISH TASKS")
# fan‑in: block until every worker finishes
results = get(refs)
print(">>>>> DISTRIBUTED JOBS COMPLETED")
return results
# A caveat of executing a Python program remotely is
# that the inputs to the remote program must be passed
# over an internet network. Similarly, the outputs
# must be passed back to the local program via the same
# structure. Python objects are not always able to be
# passed over a network, and must be encoded in a
# JSON serializable format.
i_data = JSONDecoder().decode(data)
# i_data has all of the information needed from the
# local program to pick up where the computation left off
# after its submission to the remote environment.
[
job_id,
hcore,
eri,
num_orbitals,
nuclear_repulsion_energy,
num_elec_a,
num_elec_b,
] = i_data
# Re-convert data back into numpy format, after serialization
hcore = np.array(hcore)
eri = np.array(eri)
nuclear_repulsion_energy = np.float64(nuclear_repulsion_energy)
# Instantiate Runtime Service to retrieve the
# bitstrings from the QPU job. We provided these
# credentials upon Serverless setup.
service = QiskitRuntimeService(
channel=os.environ.get("QISKIT_IBM_CHANNEL"),
token=os.environ.get("QISKIT_IBM_TOKEN"),
instance=os.environ.get("QISKIT_IBM_INSTANCE"),
)
# retrieving the QPU job data from the Serverless side
job = service.job(job_id)
primitive_result = job.result()
pub_result = primitive_result[0]
bit_array = pub_result.data.meas # Getting the bitstrings
# Pass options to the built-in eigensolver
sci_solver = partial(
distribute_solve_sci_batch, spin_sq=0.0, max_cycle=max_cycle
)
# List to capture intermediate results
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f">>>>> SQD ITERATION {iteration}")
for i, result in enumerate(results):
print(f">>>>> SUBSAMPLE {i}")
print(f">>>>> \tENERGY: {result.energy + nuclear_repulsion_energy}")
print(
f">>>>> \tSUBSPACE DIMENSION: {np.prod(result.sci_state.amplitudes.shape)}"
)
result = diagonalize_fermionic_hamiltonian(
hcore,
eri,
bit_array,
samples_per_batch=samples_per_batch,
norb=num_orbitals,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=energy_tol,
occupancies_tol=occupancies_tol,
max_iterations=max_iterations,
sci_solver=sci_solver,
symmetrize_spin=symmetrize_spin,
carryover_threshold=carryover_threshold,
callback=callback,
seed=12345,
)
print(">>>>> EXACT DIAGONALIZATION COMPLETE. CLEANING UP, SERIALIZING DATA.")
# Numpy arrays are not JSON serializable.
# Convert them to List objects before using the JSONEncoder
o_data = JSONEncoder().encode(
[
result.energy + nuclear_repulsion_energy,
result.energy,
result.rdm1.tolist(),
result.rdm2.tolist(),
[x.tolist() for x in result.orbital_occupancies],
[
result.sci_state.nelec,
result.sci_state.norb,
[x.tolist() for x in result.sci_state.orbital_occupancies()],
[x.tolist() for x in result.sci_state.rdm()],
],
]
)
# JSON-safe package
save_result({"outputs": o_data}) # single JSON blob returned to client
#!/usr/bin/env python3
from json.encoder import JSONEncoder
from json.decoder import JSONDecoder
from qiskit_serverless import get_arguments, save_result
import pyscf
from pyscf import gto, scf
from pyscf.solvent import pcm
from pyscf.mcscf import avas
import psutil
mem_info = (
psutil.virtual_memory()
) # Get information about virtual memory (RAM)
total_ram_gb = mem_info.total / (1024**3) # Convert bytes to GB
print(f">>>>> SERVERLESS TOTAL RAM: {total_ram_gb:.2f} GB")
### Argument retrieval
args = get_arguments()
data = args["data"] # Chemistry Data
i_data = JSONDecoder().decode(data)
[mol_geo, eps, ao_labels] = i_data
print(">>>>> DEFINING MOLECULE")
mol = gto.M()
mol.atom = mol_geo
mol.basis = "cc-pVDZ"
mol.unit = "Ang"
mol.charge = 0
mol.spin = 0
mol.verbose = 0
print(">>>>> BUILDING MOLECULE")
mol.build()
print(">>>>> DEFINING PCM")
cm = pcm.PCM(mol)
cm.eps = eps # for water
cm.method = "IEF-PCM"
print(">>>>> BUILDING RESTRICTED HARTREE FOCK")
mf = scf.RHF(mol).PCM(cm) # This is the Final SCF object
mf.kernel(verbose=0)
print(">>>>> RUNNING AVAS")
avas_ = avas.AVAS(mf, ao_labels, with_iao=True, canonicalize=True, verbose=0)
avas_.kernel()
norb, ne_act, mo_avas = avas_.ncas, avas_.nelecas, avas_.mo_coeff
print(">>>>> STARTING CASCI")
mc_pcm = pyscf.mcscf.CASCI(mf, norb, ne_act).PCM(
cm
) # Make sure to decorate the CASCI object with PCM
mc_pcm.mo_coeff = mo_avas
# mc_pcm.max_memory = 140000
(CASCI_E, _, _, _, _) = mc_pcm.kernel(verbose=0)
print(f">>>>> CASCI_E: {CASCI_E}")
o_data = JSONEncoder().encode([float(CASCI_E)])
# JSON-safe package
save_result({"outputs": o_data}) # single JSON blob returned to client
We need to share the program intended to run in the cloud environment, and re-upload it any time we change its source code:
client.upload(
QiskitFunction(
title="diagonalization_engine",
entrypoint="diagonalization_engine.py", # lives in ./source_files
working_dir="source_files",
)
)
client.upload(
QiskitFunction(
title="classical_simulation",
entrypoint="classical_simulation.py", # lives in ./source_files
working_dir="source_files",
)
)
Small-scale simulator example
This tutorial does not make use of a small-scale simulator because the intent is to demonstrate a scalable quantum application that extends beyond the regime of simulator exploration. Instead, we show later on how this method can be implemented using a classical state-of-the-art comparison method called CASCI.
Large-scale hardware example
# This is a useful helper function that displays
# remote job execution details to the user's local machine
def feedback_serverless(serverless_job):
import time
# Wait for the job to execute
print(f">>>>> Serverless status: {serverless_job.job_id}")
timer = 0
while timer < 10000:
if (
serverless_job.status() == "QUEUED"
or serverless_job.status() == "INITIALIZING"
or serverless_job.status() == "RUNNING"
):
print(f">>>>> [{timer}s] Serverless job {serverless_job.job_id}: \
{serverless_job.status()}")
time.sleep(10)
timer += 10
elif serverless_job.status() == "ERROR":
print(
f">>>>> Serverless job {serverless_job.job_id}: {serverless_job.status()}"
)
print(">>>>> Logs:")
print(serverless_job.logs())
break
elif serverless_job.status() == "DONE":
print(
f">>>>> Serverless job {serverless_job.job_id}: {serverless_job.status()}"
)
break
else:
break
return