Extending Solvers

This guide explains how to add a new solver to qvartools.

Architecture

All solvers extend the Solver ABC defined in qvartools/solvers/solver.py. The solver ecosystem follows a flat hierarchy:

Solver (ABC)
├── FCISolver          (reference/)
├── CCSDSolver         (reference/)
├── SQDSolver          (subspace/)
├── BatchedSQDSolver   (subspace/)
├── CIPSISolver        (subspace/)
├── SKQDSolver         (krylov/)
├── NFSKQDSolver       (krylov/)
├── DCISKQDSolver      (krylov/)
├── IterativeNFSQDSolver  (iterative/)
└── IterativeNFSKQDSolver (iterative/)

Step 1: Choose a Location

Place your solver in the appropriate subdirectory:

  • solvers/reference/ – exact or near-exact methods (FCI, CCSD, DMRG)

  • solvers/subspace/ – methods that diagonalize in a sampled subspace

  • solvers/krylov/ – methods that use Krylov subspace techniques

  • solvers/iterative/ – methods with iterative refinement loops

Step 2: Implement the Solver

# solvers/subspace/my_solver.py

from __future__ import annotations

import time
from typing import Any, Dict

from qvartools.hamiltonians.hamiltonian import Hamiltonian
from qvartools.solvers.solver import Solver, SolverResult


class MySolver(Solver):
    """My custom subspace solver.

    Parameters
    ----------
    n_samples : int
        Number of configurations to sample.
    tolerance : float
        Convergence tolerance in Hartree.

    Examples
    --------
    >>> from qvartools.molecules import get_molecule
    >>> hamiltonian, mol_info = get_molecule("H2")
    >>> solver = MySolver(n_samples=1000)
    >>> result = solver.solve(hamiltonian, mol_info)
    >>> print(f"Energy: {result.energy:.10f}")
    """

    def __init__(
        self,
        n_samples: int = 1000,
        tolerance: float = 1e-6,
    ) -> None:
        self._n_samples = n_samples
        self._tolerance = tolerance

    def solve(
        self, hamiltonian: Hamiltonian, mol_info: Dict[str, Any]
    ) -> SolverResult:
        t0 = time.perf_counter()

        # Your algorithm here
        energy = self._run_algorithm(hamiltonian)

        return SolverResult(
            energy=energy,
            diag_dim=self._n_samples,
            wall_time=time.perf_counter() - t0,
            method="MySolver",
            converged=True,
        )

    def _run_algorithm(self, hamiltonian: Hamiltonian) -> float:
        ...

Step 3: Register the Solver

Export the solver from its package __init__.py:

# solvers/subspace/__init__.py
from qvartools.solvers.subspace.my_solver import MySolver

And from the top-level solvers/__init__.py:

# solvers/__init__.py
from qvartools.solvers.subspace import MySolver

Step 4: Add Tests

Create a test file in tests/test_solvers/:

# tests/test_solvers/test_my_solver.py

import pytest
from qvartools.solvers.subspace.my_solver import MySolver

@pytest.mark.pyscf
def test_my_solver_h2(h2_hamiltonian, h2_mol_info, h2_exact_energy):
    solver = MySolver(n_samples=500)
    result = solver.solve(h2_hamiltonian, h2_mol_info)
    assert result.energy is not None
    assert abs(result.energy - h2_exact_energy) < 0.01  # 10 mHa

Best Practices

  • Use dataclasses.replace() instead of mutating config objects

  • Store iteration history in SolverResult.metadata

  • Raise ValueError for invalid parameters in __init__

  • Use torch.no_grad() for inference-only computation

  • Support both CPU and CUDA devices via a device parameter