Method Configuration Architecture

This document explains the design philosophy behind metalquicha’s method configuration system and provides guidance for developers who want to add new quantum chemistry methods.

Design Philosophy

The method configuration system follows several key principles:

  1. Initialize once, use everywhere: All configuration is read from the input file at startup and stored in a single method_config_t structure. This eliminates scattered configuration and makes the data flow explicit.

  2. Composition over inheritance: Rather than complex class hierarchies, we use composition with nested configuration types. Shared settings live at the top level or in dedicated shared types.

  3. Shared configurations for related methods: Methods that share common parameters (e.g., HF and DFT both need SCF settings) use shared config types to avoid duplication and ensure consistency.

  4. Factory pattern for instantiation: A single factory creates all method instances, making it easy to add new methods without touching calling code.

Configuration Type Hierarchy

The main configuration type uses composition to organize settings:

type :: method_config_t
   !----- Common settings (all methods) -----
   integer(int32) :: method_type    ! METHOD_TYPE_MP2, etc.
   character(len=32) :: basis_set   ! "cc-pvdz", "aug-cc-pvtz", etc.
   logical :: use_spherical         ! Spherical vs Cartesian gaussians
   logical :: verbose               ! Enable verbose output

   !----- Shared configurations -----
   type(scf_config_t) :: scf        ! Used by HF and DFT
   type(correlation_config_t) :: corr  ! Used by MP2, CC, etc.

   !----- Method-specific configurations -----
   type(xtb_config_t) :: xtb        ! Semi-empirical xTB
   type(dft_config_t) :: dft        ! DFT-specific (functional, grid)
   type(mcscf_config_t) :: mcscf    ! Multi-reference
   type(cc_config_t) :: cc          ! Coupled-cluster specific
   type(f12_config_t) :: f12        ! F12 explicitly correlated
end type

Shared Configuration Types

scf_config_t - Shared by HF and DFT:

type :: scf_config_t
   integer :: max_iter = 100
   real(dp) :: energy_convergence = 1.0e-8_dp
   real(dp) :: density_convergence = 1.0e-6_dp
   logical :: use_diis = .true.
   integer :: diis_size = 8
end type

correlation_config_t - Shared by all post-HF methods (MP2, CC, etc.):

type :: correlation_config_t
   real(dp) :: energy_convergence = 1.0e-8_dp
   integer :: n_frozen_core = -1     ! -1 = auto from elements
   logical :: freeze_core = .true.
   logical :: use_df = .true.        ! Density fitting (RI)
   character(len=32) :: aux_basis    ! RI auxiliary basis
   logical :: use_local = .false.    ! Local correlation (DLPNO, etc.)
   character(len=16) :: local_type   ! "dlpno", "pno", "lmp2"
   real(dp) :: pno_threshold         ! PNO truncation threshold
   logical :: use_scs = .false.      ! Spin-component scaling
   real(dp) :: scs_ss, scs_os        ! SCS scaling factors
end type

Method-Specific Configuration Types

cc_config_t - Coupled-cluster specific settings:

type :: cc_config_t
   integer :: max_iter = 100
   real(dp) :: amplitude_convergence = 1.0e-7_dp
   logical :: include_triples = .false.   ! (T) correction
   logical :: perturbative_triples = .true.
   logical :: use_diis = .true.
   integer :: diis_size = 8
   integer :: n_roots = 0            ! EOM-CC roots (0 = ground only)
   character(len=8) :: eom_type      ! "ee", "ip", "ea"
end type

f12_config_t - Explicitly correlated F12 settings:

type :: f12_config_t
   real(dp) :: geminal_exponent = 1.0_dp
   character(len=8) :: ansatz = '3c'      ! "3c", "3c(fix)", "2b"
   character(len=32) :: cabs_basis        ! CABS auxiliary basis
   character(len=32) :: optri_basis       ! Optional RI basis
   logical :: use_exponent_fit = .false.
   logical :: scale_triples = .true.
end type

Method Type Constants

Method types are defined as integer constants in mqc_method_types.f90 with a logical numbering scheme that groups related methods:

! Semi-empirical (1-9)
METHOD_TYPE_GFN1 = 1
METHOD_TYPE_GFN2 = 2

! SCF methods (10-19)
METHOD_TYPE_HF = 10
METHOD_TYPE_DFT = 11

! Multi-reference (20-29)
METHOD_TYPE_MCSCF = 20

! Perturbation theory (30-39)
METHOD_TYPE_MP2 = 30
METHOD_TYPE_MP2_F12 = 31

! Coupled cluster (40-59)
METHOD_TYPE_CCSD = 40
METHOD_TYPE_CCSD_T = 41
METHOD_TYPE_CCSD_F12 = 42
METHOD_TYPE_CCSD_T_F12 = 43

This numbering scheme leaves room for future methods (e.g., MP3 at 32, CC2 at 44, CCSDT at 45).

The Factory Pattern

The method_factory_t in mqc_method_factory.F90 creates method instances from configuration:

function factory_create(this, config) result(method)
   class(method_factory_t), intent(in) :: this
   type(method_config_t), intent(in) :: config
   class(qc_method_t), allocatable :: method

   select case (config%method_type)
   case (METHOD_TYPE_HF)
      allocate(hf_method_t :: method)
      call configure_hf(method, config)

   case (METHOD_TYPE_MP2)
      allocate(mp2_method_t :: method)
      call configure_mp2(method, config)

   case (METHOD_TYPE_CCSD, METHOD_TYPE_CCSD_T)
      allocate(cc_method_t :: method)
      call configure_cc(method, config)
   ! ... etc
   end select
end function

Each configure_* subroutine reads from the appropriate config sections:

subroutine configure_cc(method, config)
   ! Common settings
   m%basis_set = config%basis_set

   ! SCF settings (for reference calculation)
   m%scf_max_iter = config%scf%max_iter

   ! Correlation settings (shared)
   m%freeze_core = config%corr%freeze_core
   m%use_df = config%corr%use_df
   m%aux_basis = config%corr%aux_basis

   ! CC-specific settings
   m%max_iter = config%cc%max_iter
   m%include_triples = config%cc%include_triples
end subroutine

Adding a New Method

To add a new quantum chemistry method, follow these steps:

Step 1: Add Method Type Constant

In src/mqc_method_types.f90:

  1. Add the constant with an appropriate number:

    integer(int32), parameter :: METHOD_TYPE_MP3 = 32
    
  2. Export it in the public statement:

    public :: METHOD_TYPE_MP3
    
  3. Add string conversions in method_type_from_string and method_type_to_string:

    case ('mp3', 'ri-mp3')
       method_type = METHOD_TYPE_MP3
    
    case (METHOD_TYPE_MP3)
       method_str = "mp3"
    

Step 2: Add Configuration (if needed)

If your method needs settings not covered by existing config types:

  1. Prefer using existing shared configs when possible. For example, MP3 would just use correlation_config_t with no additions.

  2. Add to an existing config type if the setting is broadly applicable. For example, adding a use_t1_diagnostic flag to cc_config_t.

  3. Create a new config type only if the method has truly unique settings. Add it to mqc_method_config.f90:

    type :: mp3_config_t
       logical :: use_laplace = .false.  ! Laplace transform MP3
       integer :: laplace_points = 5
    end type
    

    Then add it to method_config_t and config_reset.

Step 3: Create the Method Type

Create src/methods/mqc_method_mp3.f90:

module mqc_method_mp3
   use mqc_method_base, only: qc_method_t
   implicit none

   type :: mp3_options_t
      ! Copy relevant settings from config
      character(len=32) :: basis_set
      logical :: freeze_core
      ! ... etc
   end type

   type, extends(qc_method_t) :: mp3_method_t
      type(mp3_options_t) :: options
   contains
      procedure :: calc_energy => mp3_calc_energy
      procedure :: calc_gradient => mp3_calc_gradient
      procedure :: calc_hessian => mp3_calc_hessian
   end type

contains
   ! Implement the methods...
end module

Step 4: Add to the Factory

In src/methods/mqc_method_factory.F90:

  1. Add the use statement:

    use mqc_method_mp3, only: mp3_method_t
    
  2. Add the case in factory_create:

    case (METHOD_TYPE_MP3)
       allocate(mp3_method_t :: method)
       call configure_mp3(method, config)
    
  3. Implement configure_mp3:

    subroutine configure_mp3(method, config)
       class(qc_method_t), intent(inout) :: method
       type(method_config_t), intent(in) :: config
    
       select type (m => method)
       type is (mp3_method_t)
          m%options%basis_set = config%basis_set
          m%options%freeze_core = config%corr%freeze_core
          m%options%use_df = config%corr%use_df
          ! ... etc
       end select
    end subroutine
    

Step 5: Add to CMakeLists.txt

In src/methods/CMakeLists.txt:

target_sources(${main_lib} PRIVATE mqc_method_mp3.f90)

Step 6: Update Documentation

Update this document and any user-facing documentation to include the new method.

Example: Method Configuration in Practice

Here’s how different calculation types use the configuration:

Simple MP2:

config%method_type = METHOD_TYPE_MP2
config%basis_set = 'cc-pvtz'
config%corr%freeze_core = .true.
config%corr%use_df = .true.
config%corr%aux_basis = 'cc-pvtz-ri'

DLPNO-CCSD(T):

config%method_type = METHOD_TYPE_CCSD_T
config%basis_set = 'cc-pvtz'
config%corr%freeze_core = .true.
config%corr%use_local = .true.
config%corr%local_type = 'dlpno'
config%corr%pno_threshold = 1.0e-7_dp
config%cc%include_triples = .true.

CCSD(T)-F12:

config%method_type = METHOD_TYPE_CCSD_T_F12
config%basis_set = 'cc-pvtz-f12'
config%corr%freeze_core = .true.
config%cc%include_triples = .true.
config%f12%ansatz = '3c'
config%f12%geminal_exponent = 1.0_dp
config%f12%cabs_basis = 'cc-pvtz-f12-cabs'

EOM-CCSD for excited states:

config%method_type = METHOD_TYPE_CCSD
config%basis_set = 'aug-cc-pvdz'
config%cc%n_roots = 5
config%cc%eom_type = 'ee'

Design Rationale

Why Composition Over Inheritance?

Fortran’s OOP support for inheritance is functional but can be awkward, especially for configuration types. Composition provides:

  • Explicit data flow: It’s clear which settings come from where

  • Flexibility: A method can use any combination of shared configs

  • Simplicity: No virtual dispatch overhead for config access

  • Extensibility: Adding new config types doesn’t affect existing ones

Why Shared Config Types?

Many quantum chemistry methods share common concepts:

  • HF and DFT both solve SCF equations -> scf_config_t

  • MP2 and CCSD both have frozen core, RI, local approximations -> correlation_config_t

  • All F12 methods share geminal/CABS settings -> f12_config_t

Sharing these prevents duplication and ensures that common settings are handled consistently across methods.

Why a Factory?

The factory pattern provides:

  • Single point of method creation: Easy to add new methods

  • Encapsulated instantiation: Calling code doesn’t need to know concrete method types

  • Consistent configuration: All methods are configured the same way

  • Testability: Easy to mock or substitute methods

Why Integer Constants for Method Types?

Integer constants (vs. strings) provide:

  • Fast comparison: No string operations in hot paths

  • Compile-time checking: Typos are caught by the compiler

  • Clear grouping: The numbering scheme documents method families

  • Memory efficiency: 4 bytes vs. variable-length strings

File Locations

  • src/mqc_method_types.f90 - Method type constants and conversions

  • src/methods/mqc_method_config.f90 - All configuration types

  • src/methods/mqc_method_factory.F90 - Factory implementation

  • src/methods/mqc_method_base.f90 - Abstract base class

  • src/methods/mqc_method_*.f90 - Concrete method implementations