High-level design

This document outlines the core architectural design principles of QDK/Chemistry, explaining the conceptual framework that guides the library’s organization and implementation. For a complete overview of QDK/Chemistry’s documentation, see the in-depth documentation index.

QDK/Chemistry is designed with a clear separation between data classes and algorithms. This design choice enables flexibility, extensibility, and maintainability of the codebase, while providing users with a consistent and intuitive API.

Separation of Data and Algorithms

QDK/Chemistry follows a design pattern that strictly separates:

  1. Data Classes: Immutable containers that store and manage quantum chemical data

  2. Algorithm Classes: Processors that operate on data objects to produce new data objects

This separation follows the principle of single responsibility and creates a clear flow of data through computational workflows.

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

    Input [label="Input Data", fillcolor="#E8EAF6", color="#5C6BC0", penwidth=2, fontcolor="#3949AB"];
    Algorithm [label="Algorithm", fillcolor="#E3F2FD", color="#42A5F5", penwidth=2, fontcolor="#1976D2"];
    Output [label="Output Data", fillcolor="#E0F2F1", color="#26A69A", penwidth=2, fontcolor="#00796B"];

    Input -> Algorithm;
    Algorithm -> Output;
}

Data classes

Data classes in QDK/Chemistry concretely represent intermediate quantities commonly encountered in quantum applications workflows. These classes are designed to be:

Immutable

Once created, the core data cannot be modified

Self-contained

Include all information necessary to represent the underlying quantum chemical quantity

Serializable

Can be easily saved to and loaded from files

Language-agnostic

Accessible through identical APIs in both C++ and Python

See the Data Classes documentation for further details on the availability and usage of QDK/Chemistry’s data classes.

Algorithm classes

Algorithm classes represent mutations on data, such as the execution of quantum chemical methods and generation of circuit components commonly found in quantum applications workflows. These classes are designed to be:

Stateless

Their behavior depends only on their input data and configuration

Configurable

Through a standardized Settings interface

Conforming

Exposing a common interface for disparate implementations to enable a unified user experience.

Extensible

Allowing new implementations to be added without modifying existing code

Programmatically, Algorithms are specified as abstract interfaces which can be specialized downstream through concrete implementations. This allows QDK/Chemistry to be expressed as a plugin architecture, for which algorithm implementations may be specified either natively within QDK/Chemistry or through established third-party quantum chemistry packages:

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

    Interface [label="QDK/Chemistry Interface\n(ScfSolver, Localizer,\nMCCalculator, etc.)", fillcolor="#E3F2FD", color="#42A5F5", penwidth=2, fontcolor="#1976D2"];
    QDK [label="QDK Implementation\n(AutoCAS, etc.)", fillcolor="#E0F2F1", color="#26A69A", penwidth=2, fontcolor="#00796B"];
    ThirdParty [label="Third-Party Interfaces\n(PySCF, etc.)", fillcolor="#F3E5F5", color="#AB47BC", penwidth=2, fontcolor="#7B1FA2"];

    Interface -> QDK;
    Interface -> ThirdParty;
}

This design allows users to benefit from specialized capabilities of “best-in-breed” software while maintaining a consistent user experience. See the Plugin System documentation for further details on how to contribute new algorithm implementations.

Further details on the availability and usage of QDK/Chemistry’s algorithm implementations can be found in the Algorithms documentation.

Factory pattern

QDK/Chemistry’s plugin architecture leverages a factory pattern for algorithm creation:

#include <qdk/chemistry.hpp>

using namespace qdk::chemistry::data;
using namespace qdk::chemistry::algorithms;

auto scf = ScfSolverFactory::create();
from pathlib import Path
from qdk_chemistry.algorithms import create
from qdk_chemistry.data import Structure

scf_solver = create("scf_solver")

This pattern allows:

  • Extension of workflows by new Algorithm implementations without changing client code

  • Centralized management of dependencies and resources

Read more on QDK/Chemistry’s usage of this pattern in the Factory Pattern documentation.

Runtime configuration with settings

Algorithm configuration is managed through instances of Settings objects, which contain a type-safe data store of configuration parameters consistent between the python and C++ APIs:

std::cout << "Available settings: " << scf_solver.settings().get_summary()
          << std::endl;
scf_solver->settings().set("max_iterations", 100);
print(f"Available settings: {scf_solver.settings().get_summary()}")
scf_solver.settings().set("max_iterations", 100)

Read more on how one can configure, discover, and extend instances of Settings objects in the Settings documentation.

A complete dataflow example

A typical workflow in QDK/Chemistry demonstrates the data-algorithm separation:

int main() {
  // Create molecular structure from an XYZ file
  auto molecule = Structure::from_xyz_file("molecule.xyz");

  // Configure and run SCF calculation
  auto scf_solver = ScfSolverFactory::create();
  scf_solver->settings().set("basis_set", "cc-pvdz");
  auto [scf_energy, wfn_hf] = scf_solver->run(molecule, 0, 1);

  // Select active space orbitals
  auto active_selector = ActiveSpaceSelectorFactory::create("qdk_valence");
  active_selector->settings().set("num_active_orbitals", 6);
  active_selector->settings().set("num_active_electrons", 6);
  auto active_wfn = active_selector->run(wfn_hf);
  auto active_orbitals = active_wfn->get_orbitals();

  // Create Hamiltonian with active space
  auto ham_constructor = HamiltonianConstructorFactory::create();
  auto hamiltonian = ham_constructor->run(active_orbitals);

  // Run multi-configuration calculation
  auto mc_solver = MultiConfigurationCalculatorFactory::create();
  auto [mc_energy, wave_function] = mc_solver->run(hamiltonian, 3, 3);

  return 0;
}
# Load a Structure from file (data classes in QDK/Chemistry are immutable by design)
structure = Structure.from_xyz_file(Path(__file__).parent / "../data/h2.structure.xyz")

# Configure and run SCF calculation
scf_solver = create("scf_solver")
scf_energy, scf_wavefunction = scf_solver.run(
    structure, charge=0, spin_multiplicity=1, basis_or_guess="cc-pvdz"
)

# Select active space orbitals
active_space_selector = create(
    "active_space_selector",
    algorithm_name="qdk_valence",
)
active_space_selector.settings().set("num_active_orbitals", 2)
active_space_selector.settings().set("num_active_electrons", 2)
active_wfn = active_space_selector.run(scf_wavefunction)
active_orbitals = active_wfn.get_orbitals()

# Create Hamiltonian with active space
ham_constructor = create("hamiltonian_constructor")
hamiltonian = ham_constructor.run(active_orbitals)

mc = create("multi_configuration_calculator")
mc.settings().set("davidson_iterations", 300)
E_cas, wfn_cas = mc.run(
    hamiltonian, n_active_alpha_electrons=1, n_active_beta_electrons=1
)

Further reading

  • The above examples can be downloaded as complete C++ and Python scripts.

  • Factory Pattern: Details on QDK/Chemistry’s implementation of the factory pattern

  • Settings: How to configure the execution behavior of algorithms through the Settings interface

  • Plugin system: QDK/Chemistry’s plugin system for extending functionality