Factory pattern
QDK/Chemistry extensively uses the Factory pattern, a creational design pattern that provides an interface for creating objects without specifying their concrete classes. This document explains how and why QDK/Chemistry uses this pattern for algorithm instantiation.
Overview of the factory pattern
The factory pattern is a design pattern that encapsulates object creation logic. Instead of directly instantiating objects using constructors, clients request objects from a factory, typically by specifying a type identifier. This approach has several advantages:
- Abstraction
Clients work with abstract interfaces rather than concrete implementations
- Flexibility
The concrete implementation can be changed without affecting client code
- Configuration
Objects can be configured based on runtime parameters
- Extension
New implementations can be added without modifying existing code
Factory pattern in QDK/Chemistry
In QDK/Chemistry, algorithm classes are instantiated through factory classes rather than direct constructors. This design allows QDK/Chemistry to:
Support multiple implementations of the same algorithm interface
Configure algorithm instances based on settings
Load algorithm implementations dynamically at runtime
Isolate algorithm implementation details from client code
Factory classes in QDK/Chemistry
QDK/Chemistry provides factory infrastructure for each algorithm type. In Python, algorithm instantiation is managed through a centralized registry module rather than individual factory classes.
Algorithm |
Algorithm Type (Python) |
Factory Class (C++) |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Python only |
|
|
Python only |
|
|
Python only |
Using factories
To create an algorithm instance, call the appropriate factory method with an optional implementation name. If no name is provided, the default implementation is used. See Discovering implementations below for how to list available implementations programmatically.
#include <qdk/chemistry.hpp>
using namespace qdk::chemistry::algorithms;
using namespace qdk::chemistry::data;
// Create a simple molecule
auto structure = Structure::from_xyz("2\n\nH 0.0 0.0 0.0\nH 0.0 0.0 1.4");
// Create a SCF solver using the default implementation
auto scf_solver = ScfSolverFactory::create();
// Create an orbital localizer using a specific implementation
auto localizer = LocalizerFactory::create("qdk_pipek_mezey");
// Configure the SCF solver and run
scf_solver->settings().set("basis_set", "cc-pvdz");
auto [E_scf, wfn] = scf_solver->run(structure, 0, 1);
from pathlib import Path
from qdk_chemistry.algorithms import 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 default implementation
scf_solver = create("scf_solver")
# Create an orbital localizer using a specific implementation
localizer = create("orbital_localizer", "qdk_pipek_mezey")
# Configure the SCF solver and run
E_scf, wfn = scf_solver.run(
structure, charge=0, spin_multiplicity=1, basis_or_guess="cc-pvdz"
)
Discovering implementations
QDK/Chemistry provides programmatic discovery of available algorithm types and their implementations. This is useful for exploring what’s available at runtime, building dynamic UIs, or debugging plugin loading.
Listing algorithm types and implementations
#include <iostream>
#include <qdk/chemistry.hpp>
using namespace qdk::chemistry::algorithms;
// List available SCF solver implementations
auto scf_methods = ScfSolverFactory::available();
std::cout << "Available SCF solvers:" << std::endl;
for (const auto& name : scf_methods) {
std::cout << " - " << name << std::endl;
}
// List available localizer implementations
auto localizer_methods = LocalizerFactory::available();
std::cout << "Available localizers:" << std::endl;
for (const auto& name : localizer_methods) {
std::cout << " - " << name << std::endl;
}
// List available Hamiltonian constructor implementations
auto ham_methods = HamiltonianConstructorFactory::available();
std::cout << "Available Hamiltonian constructors:" << std::endl;
for (const auto& name : ham_methods) {
std::cout << " - " << name << std::endl;
}
// List available multi-configuration calculator implementations
auto mc_methods = MultiConfigurationCalculatorFactory::available();
std::cout << "Available MC calculators:" << std::endl;
for (const auto& name : mc_methods) {
std::cout << " - " << name << std::endl;
}
// Show default implementation for each factory type
std::cout << "Default SCF solver: " << ScfSolverFactory::default_name()
<< std::endl;
std::cout << "Default localizer: " << LocalizerFactory::default_name()
<< std::endl;
std::cout << "Default Hamiltonian constructor: "
<< HamiltonianConstructorFactory::default_name() << std::endl;
std::cout << "Default MC calculator: "
<< MultiConfigurationCalculatorFactory::default_name() << std::endl;
from qdk_chemistry.algorithms import registry # noqa: E402
# List all algorithm types and their implementations
all_algorithms = registry.available()
print(all_algorithms)
# Output: {'scf_solver': ['qdk', 'pyscf'], 'orbital_localizer': ['qdk_pipek_mezey', 'pyscf'], ...}
# List implementations for a specific algorithm type
scf_methods = registry.available("scf_solver")
print("Available SCF solvers:", scf_methods)
# Output: ['qdk', 'pyscf']
localizer_methods = registry.available("orbital_localizer")
print("Available localizers:", localizer_methods)
# Output: ['qdk_pipek_mezey', 'pyscf', ...]
# Show default implementations for each algorithm type
defaults = registry.show_default()
print("Defaults:", defaults)
# Output: {'scf_solver': 'qdk', 'orbital_localizer': 'qdk_pipek_mezey', ...}
Inspecting settings
Each algorithm implementation has configurable settings. You can discover available settings programmatically as shown below. For comprehensive documentation on working with settings, see Settings.
#include <iostream>
#include <qdk/chemistry.hpp>
using namespace qdk::chemistry::algorithms;
// Create a SCF solver and inspect its settings
auto scf = ScfSolverFactory::create("qdk");
// Print settings as a formatted table
std::cout << scf->settings().as_table() << std::endl;
// Or iterate over individual settings
for (const auto& key : scf->settings().keys()) {
std::cout << key << ": " << scf->settings().get_as_string(key) << std::endl;
}
from qdk_chemistry.algorithms import registry # noqa: E402
# Create a SCF solver and inspect its settings
scf = registry.create("scf_solver", "qdk")
# Print settings as a formatted table
registry.print_settings("scf_solver", "qdk")
# Or iterate over individual settings
for key in scf.settings().keys():
print(f"{key}: {scf.settings().get(key)}")
Connection to the plugin system
The factory pattern serves as the foundation for QDK/Chemistry’s plugin system. Factories enable the registration and instantiation of plugin implementations that connect to external quantum chemistry programs.
Internally, QDK/Chemistry’s factories maintain a registry of creator functions mapped to implementation names. When a client requests an implementation by name, the factory looks up the appropriate creator function and instantiates the object with the necessary setup.
This design enables several key capabilities:
Seamless integration with external quantum chemistry packages
Runtime selection of specific implementations
Decoupling of plugin usage from implementation details
For detailed information about implementing custom plugins see the plugin documentation.