Plugins

QDK/Chemistry uses a plugin system to support multiple implementations of each of the available algorithm type. This allows switching between native QDK implementations and third-party packages (e.g., PySCF, Qiskit) without modifying application code.

Plugin system

Architecture

Each algorithm in QDK/Chemistry can have multiple implementations. All implementations inherit from the same base class and conform to the same interface:

digraph InterfaceArchitecture {
    rankdir=TB;
    bgcolor="#FAFAFA";
    node [shape=box, style="rounded,filled", fontname="Arial", margin=0.3];
    edge [color="#1976D2", penwidth=2];

    UserCode [label="User Code", fillcolor="#E8EAF6", color="#5C6BC0", penwidth=2, fontcolor="#3949AB"];
    API [label="QDK/Chemistry Algorithm API", fillcolor="#E3F2FD", color="#42A5F5", penwidth=2, fontcolor="#1976D2"];
    Native [label="Native Implementation", fillcolor="#E0F2F1", color="#26A69A", penwidth=2, fontcolor="#00796B"];
    ThirdParty [label="Third-Party Interface", fillcolor="#E0F2F1", color="#26A69A", penwidth=2, fontcolor="#00796B"];
    External [label="External Package", fillcolor="#F3E5F5", color="#AB47BC", penwidth=2, fontcolor="#7B1FA2"];

    UserCode -> API;
    API -> Native;
    API -> ThirdParty;
    ThirdParty -> External;
}

This design supports several workflows:

  • Benchmarking native implementations against established packages

  • Mixing backends (e.g., PySCF for SCF, MACIS for multi-configurational methods)

  • Adding custom implementations

The implementations for each algorithm type are managed by a factory class, which provides a consistent interface for creating instances and listing available implementations. We refer the reader to the factory pattern and algorithm documentation pages for more details on this design pattern.

Using plugins

To select an implementation, specify it by name:

#include <qdk/chemistry.hpp>

// Create a SCF solver that uses the QDK/Chemistry library as solver
auto scf = ScfSolverFactory::create();

// Configure it using the standard settings interface
scf->settings().set("basis_set", "cc-pvdz");
scf->settings().set("method", "hf");

// Run calculation with the same API as native implementations
auto [energy, orbitals] = scf->solve(structure);
from pathlib import Path
from qdk_chemistry.algorithms import available, create
from qdk_chemistry.data import Structure

# Load H2 molecule from XYZ file
structure = Structure.from_xyz_file(Path(__file__).parent / "../data/h2.structure.xyz")

# Create a SCF solver using the factory
scf_solver = create("scf_solver", "pyscf")

# Configure it using the standard settings interface
scf_solver.settings().set("method", "hf")

# Run calculation - returns (energy, wavefunction)
energy, wavefunction = scf_solver.run(
    structure, charge=0, spin_multiplicity=1, basis_or_guess="cc-pvdz"
)
orbitals = wavefunction.get_orbitals()

print(f"SCF Energy: {energy:.10f} Hartree")

To list available implementations:

#include <iostream>
#include <qdk/chemistry.hpp>

// Get a list of available SCF solver implementations
auto available_solvers = ScfSolverFactory::available();
for (const auto& solver : available_solvers) {
  std::cout << solver << std::endl;
}

// Get documentation for a specific implementation
std::cout << ScfSolverFactory::get_docstring("default") << std::endl;
# List available implementations for each algorithm type
for algorithm_name in available():
    print(f"{algorithm_name} has methods:")
    for method_name in available(algorithm_name):
        print(f"  {method_name} has settings:")
        method = create(algorithm_name, method_name)
        settings = method.settings()
        for key, value in settings.items():
            print(f"    {key}: {value}")

Documentation pertaining to the availability and configuration of each algorithm implementation provided within QDK/Chemistry can be found on the algorithm documentation pages.

Included third-party plugins

In addition to the native implementations packaged within QDK/Chemistry, plugins are included for the following packages:

  • PySCF — Python-based quantum chemistry

  • Qiskit — Quantum computing

These plugins are enabled automatically when the corresponding package is installed.

Community-developed plugins are also welcome. See Creating plugins for guidance on creating new plugins.

Creating plugins

QDK/Chemistry supports two extension mechanisms:

  1. Implementing a new backend for an existing algorithm type (e.g., integrating an external quantum chemistry package)

  2. Defining an entirely new algorithm type with its own factory and implementations

The following sections provide comprehensive examples of each approach.

Implementing a new algorithm backend

This section demonstrates how to integrate an external SCF solver as a QDK/Chemistry plugin, enabling access through the standard API.

Interface requirements

Each algorithm type in QDK/Chemistry defines an abstract base class specifying the interface that all implementations must satisfy:

  • A name() method that returns a unique identifier for the implementation

  • A _run_impl() method containing the computational logic

  • A settings() object for runtime configuration

Defining custom settings

When an implementation requires configuration options beyond those provided by the base settings class, a derived settings class can be defined:

class CustomScfSettings
    : public qdk::chemistry::algorithms::ElectronicStructureSettings {
 public:
  CustomScfSettings() : ElectronicStructureSettings() {
    // Define additional settings beyond the inherited defaults
    set_default("custom_option", "default_value");
  }
};
from qdk_chemistry.data import ElectronicStructureSettings


class CustomScfSettings(ElectronicStructureSettings):
    """Settings for the custom SCF solver."""

    def __init__(self):
        super().__init__()
        # Define additional settings beyond the inherited defaults
        self._set_default(
            "custom_option",
            "string",
            "default_value",
            "Description of the custom option",
        )


Implementation structure

The implementation class inherits from the algorithm base class and overrides the required methods. The _run_impl() method is responsible for:

  1. Converting QDK/Chemistry data structures to the external package’s format

  2. Invoking the external computation

  3. Converting results back to QDK/Chemistry data structures

#include <qdk/chemistry/algorithms/scf.hpp>

#include "external_chemistry_package.hpp"

class CustomScfSolver : public qdk::chemistry::algorithms::ScfSolver {
 public:
  CustomScfSolver() { _settings = std::make_unique<CustomScfSettings>(); }

  std::string name() const override { return "custom"; }

 protected:
  std::pair<double, std::shared_ptr<qdk::chemistry::data::Wavefunction>>
  _run_impl(std::shared_ptr<qdk::chemistry::data::Structure> structure,
            int charge, int spin_multiplicity,
            std::optional<std::shared_ptr<qdk::chemistry::data::Orbitals>>
                initial_guess) override {
    // Convert to external format
    auto external_mol = convert_to_external_format(structure);

    // Execute external calculation
    auto basis = _settings->get<std::string>("basis_set");
    auto [energy, external_orbitals] =
        external_package::run_scf(external_mol, basis);

    // Convert results to QDK format
    auto wavefunction = convert_to_qdk_wavefunction(external_orbitals);

    return {energy, wavefunction};
  }
};
from qdk_chemistry.algorithms import ScfSolver  # noqa: E402
from qdk_chemistry.data import (  # noqa: E402
    BasisSet,
    Orbitals,
    Structure,
    Wavefunction,
)


class CustomScfSolver(ScfSolver):
    """Custom SCF solver wrapping an external chemistry package."""

    def __init__(self):
        super().__init__()
        self._settings = CustomScfSettings()

    def name(self) -> str:
        return "custom"

    def _run_impl(
        self,
        structure: Structure,
        charge: int,
        spin_multiplicity: int,
        basis_or_guess: Orbitals | BasisSet | str | None = None,
    ) -> tuple[float, Wavefunction]:
        """Perform a self-consistent field (SCF) calculation using a custom backend.

        This method should convert the input structure to the external format, run the SCF calculation
        using the specified method and basis set, and return the electronic energy and wavefunction
        in QDK/Chemistry format.

        Args:
            structure: The molecular structure to be calculated.
            charge: The total charge of the molecular system.
            spin_multiplicity: The spin multiplicity (2S+1) of the system.
            basis_or_guess: Basis set information or initial guess, which can be:
                - An Orbitals object (used as initial guess)
                - A BasisSet object
                - A string specifying the basis set name
                - None (use default from settings)

        Returns:
            Tuple of (energy, wavefunction)
        """
        # Convert to external format

        # Execute external calculation

        # Convert results to QDK format

        # energy = 0.0
        # wavefunction = Wavefunction(...)
        # return energy, wavefunction
        return 0.0, None


Registration

Implementations are registered with the algorithm factory to enable discovery and instantiation by name. Registration is typically performed during module initialization:

#include <qdk/chemistry/algorithms/scf.hpp>

// Static registration during library initialization
static auto registration =
    qdk::chemistry::algorithms::ScfSolver::register_implementation(
        []() { return std::make_unique<CustomScfSolver>(); });
from qdk_chemistry.algorithms.registry import register  # noqa: E402

# Registration during module import
register(lambda: CustomScfSolver())

Following registration, the implementation is accessible through the standard API:

from qdk_chemistry.algorithms import available, create  # noqa: E402
from qdk_chemistry.data import Structure  # noqa: E402

# Define a molecular structure (e.g., H2 molecule)
coords = [[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]]
molecule = Structure(coords, symbols=["H", "H"])

# Instantiate the custom solver
solver = create("scf_solver", "custom")
# energy, wavefunction = solver.run(
#     molecule, charge=0, spin_multiplicity=1, basis_or_guess="sto-3g"
# )

# Verify registration
print(available("scf_solver"))  # [..., 'custom']

Defining a new algorithm type

When the required functionality does not correspond to an existing algorithm category, a new algorithm type can be defined. This section demonstrates the complete process using a geometry optimizer as an example.

Interface design

The first step is to specify the algorithm’s interface:

Input type

The data the algorithm operates on (e.g., Structure)

Output type

The data the algorithm produces (e.g., optimized Structure)

Configuration

Required settings (e.g., convergence thresholds, iteration limits)

Settings class definition

Define a settings class containing all configuration parameters:

class GeometryOptimizerSettings : public qdk::chemistry::data::Settings {
 public:
  GeometryOptimizerSettings() {
    set_default<int64_t>(
        "max_steps", 100, "Maximum optimization steps",
        qdk::chemistry::data::BoundConstraint<int64_t>{1, 10000});
    set_default<double>("convergence_threshold", 1e-5,
                        "Gradient convergence threshold");
    set_default<double>("step_size", 0.1, "Initial optimization step size");
  }
};
from qdk_chemistry.data import Settings  # noqa: E402


class GeometryOptimizerSettings(Settings):
    """Settings for geometry optimization algorithms."""

    def __init__(self):
        super().__init__()
        self._set_default(
            "max_steps", "int", 100, "Maximum optimization steps", (1, 10000)
        )
        self._set_default(
            "convergence_threshold", "double", 1e-5, "Gradient convergence threshold"
        )
        self._set_default("step_size", "double", 0.1, "Initial optimization step size")


Base class definition

Define an abstract base class specifying the interface for all implementations:

class GeometryOptimizer
    : public qdk::chemistry::algorithms::Algorithm<
          GeometryOptimizer,
          std::shared_ptr<qdk::chemistry::data::Structure>,  // Return type
          std::shared_ptr<qdk::chemistry::data::Structure>>  // Input type
{
 public:
  static std::string type_name() { return "geometry_optimizer"; }
};
from qdk_chemistry.algorithms.base import Algorithm  # noqa: E402


class GeometryOptimizer(Algorithm):
    """Abstract base class for geometry optimization algorithms."""

    def type_name(self) -> str:
        return "geometry_optimizer"


Factory definition

The factory manages implementation registration and provides instance creation:

// The Algorithm base class template provides factory functionality
// automatically.
from qdk_chemistry.algorithms.base import AlgorithmFactory  # noqa: E402


class GeometryOptimizerFactory(AlgorithmFactory):
    """Factory for creating geometry optimizer instances."""

    def algorithm_type_name(self) -> str:
        return "geometry_optimizer"

    def default_algorithm_name(self) -> str:
        return "bfgs"


Concrete implementations

Implement the algorithm by inheriting from the base class:

class BfgsOptimizer : public GeometryOptimizer {
 public:
  BfgsOptimizer() { _settings = std::make_unique<GeometryOptimizerSettings>(); }

  std::string name() const override { return "bfgs"; }

 protected:
  std::shared_ptr<qdk::chemistry::data::Structure> _run_impl(
      std::shared_ptr<qdk::chemistry::data::Structure> structure) override {
    auto max_steps = _settings->get<int64_t>("max_steps");
    auto threshold = _settings->get<double>("convergence_threshold");

    // BFGS optimization implementation
    return optimized_structure;
  }
};
from qdk_chemistry.data import Structure  # noqa: E402


class BfgsOptimizer(GeometryOptimizer):
    """BFGS quasi-Newton geometry optimizer."""

    def __init__(self):
        super().__init__()
        self._settings = GeometryOptimizerSettings()

    def name(self) -> str:
        return "bfgs"

    def _run_impl(self, structure: Structure) -> Structure:
        # max_steps = self.settings().get("max_steps")
        # threshold = self.settings().get("convergence_threshold")

        # BFGS optimization implementation
        # Placeholder for optimized structure
        optimized_structure = Structure()
        return optimized_structure


Additional implementations follow the same pattern:

class SteepestDescentOptimizer(GeometryOptimizer):
    """Steepest descent geometry optimizer."""

    def __init__(self):
        super().__init__()
        self._settings = GeometryOptimizerSettings()

    def name(self) -> str:
        return "steepest_descent"

    def _run_impl(self, structure: Structure) -> Structure:
        # Steepest descent implementation
        # Placeholder for optimized structure
        optimized_structure = Structure()
        return optimized_structure


Registration

Register the factory and all implementations:

// During library initialization
static auto factory_reg = register_factory<GeometryOptimizer>();
static auto bfgs_reg = GeometryOptimizer::register_implementation(
    []() { return std::make_unique<BfgsOptimizer>(); });
import qdk_chemistry.algorithms as algorithms  # noqa: E402

# Register the factory
algorithms.registry.register_factory(GeometryOptimizerFactory())

# Register implementations
algorithms.register(lambda: BfgsOptimizer())
algorithms.register(lambda: SteepestDescentOptimizer())

Usage

Following registration, the new algorithm type is accessible through the standard API:

from qdk_chemistry.algorithms import available, create  # noqa: E402

# List available implementations
print(available("geometry_optimizer"))  # ['bfgs', 'steepest_descent']

# Instantiate and configure
optimizer = create("geometry_optimizer", "bfgs")
optimizer.settings().set("max_steps", 200)
optimizer.settings().set("convergence_threshold", 1e-6)

# Execute
# optimized_structure = optimizer.run(initial_structure)

For additional information on the factory pattern and settings system, refer to the factory pattern and settings documentation.

Further reading