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:
Initialize once, use everywhere: All configuration is read from the input file at startup and stored in a single
method_config_tstructure. This eliminates scattered configuration and makes the data flow explicit.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.
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.
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
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:
Add the constant with an appropriate number:
integer(int32), parameter :: METHOD_TYPE_MP3 = 32
Export it in the public statement:
public :: METHOD_TYPE_MP3
Add string conversions in
method_type_from_stringandmethod_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:
Prefer using existing shared configs when possible. For example, MP3 would just use
correlation_config_twith no additions.Add to an existing config type if the setting is broadly applicable. For example, adding a
use_t1_diagnosticflag tocc_config_t.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_tandconfig_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:
Add the use statement:
use mqc_method_mp3, only: mp3_method_t
Add the case in
factory_create:case (METHOD_TYPE_MP3) allocate(mp3_method_t :: method) call configure_mp3(method, config)
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 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 conversionssrc/methods/mqc_method_config.f90- All configuration typessrc/methods/mqc_method_factory.F90- Factory implementationsrc/methods/mqc_method_base.f90- Abstract base classsrc/methods/mqc_method_*.f90- Concrete method implementations