!=============================================================================== ! MODULE: scf_converger !=============================================================================== ! ! DESCRIPTION: ! The scf_converger module implements a framework for managing convergence ! in Self-Consistent Field (SCF) calculations. It provides a main driver ! type `scf_conv` to coordinate multiple convergence methods, including ! variants of Direct Inversion in the Iterative Subspace (DIIS) and ! Second-Order SCF (SOSCF), storing iteration data in a ring buffer structure. ! ! MEMBERS: ! - conv_none [INTEGER]: Constant (1) for no convergence method. ! - conv_cdiis [INTEGER]: Constant (2) for Commutator DIIS. ! - conv_ediis [INTEGER]: Constant (3) for Energy DIIS. ! - conv_adiis [INTEGER]: Constant (4) for Approximate DIIS. ! - conv_soscf [INTEGER]: Constant (5) for Second-Order SCF. ! - conv_state_not_initialized [INTEGER]: Constant (0) for uninitialized state. ! - conv_state_initialized [INTEGER]: Constant (1) for initialized state. ! - conv_name_maxlen [INTEGER]: Constant (32) for maximum length of converger names. ! ! DEPENDENCIES: ! - precision: Provides `dp` for double precision real numbers. ! - io_constants: Provides `iw` for output unit. ! - messages: Provides `show_message` and `with_abort` for error handling. ! - mathlib: Provides functions for matrix operations: ! `traceprod_sym_packed`: Calculates trace of product of two packed symmetric matrices. ! `orb_to_dens`: Converts orbitals to density matrix. ! `antisymmetrize_matrix`: Makes matrix antisymmetric (A = A - A^T). ! `unpack_matrix`: Converts packed triangular to full square matrix. ! `pack_matrix`: Converts full square to packed triangular matrix. ! - `scf_addons`: Provides Pseudo-Fractional Occupation Number (pFON) functionality. ! ! DATA MANAGEMENT: ! The module uses a ring buffer system implemented in the `converger_data` type ! for efficiently storing SCF iteration history. This designimproves memory efficiency ! and data locality compared to traditional vector growth approaches. ! The buffer system supports both forward and backward indices for intuitive access ! (1 = oldest, -1 = latest). ! ! NOTES: ! - The module defines public types: `scf_conv`, `scf_conv_result`, and `soscf_converger`. ! - All other entities are private unless explicitly made public. ! - The code supports handling of Fock matrices for RHF/ROHF (1 matrix) and UHF (2 matrices). ! - ROHF Fock matrix has already gone through the Guest-Saunders transformation. ! - The module supports integration with the pFON (pseudo-Fractional Occupation Number) ! methodology. ! ! HISTORY: ! - [pre-2022] Initial Development - Vladimir Mironov ! Established the core framework for SCF convergence management, ! including the main driver `scf_conv` and initial DIIS methods. ! - [2025] Ring Buffer - Konstantin Komarov ! Implemented the ring buffer structure via the `converger_data` type, ! improving memory efficiency for storing SCF iteration history. ! - [2025] SOSCF Converger - Konstantin Komarov ! Added the Second-Order SCF (SOSCF) method through the `soscf_converger` type, ! enabling convergence using orbital optimization techniques. ! !=============================================================================== !=============================================================================== ! TYPE: scf_conv - MAIN DRIVER TYPE !=============================================================================== ! ! DESCRIPTION: ! The `scf_conv` type is the main driver for SCF convergence. It manages a ! ring buffer of iteration data via `converger_data`, maintains an array of ! subconvergers, and selects a convergence method based on the current error ! and predefined thresholds. ! ! MEMBERS: ! step [INTEGER]: Current SCF iteration step. ! overlap [REAL(dp), POINTER]: Pointer to the full-format overlap matrix (S). ! overlap_sqrt [REAL(dp), POINTER]: Pointer to the full-format S^(1/2) matrix. ! dat [TYPE(converger_data)]: Stores SCF iteration history in a ring buffer. ! sconv [TYPE(subconverger_), ALLOCATABLE]: Array of subconverger instances. ! thresholds [REAL(dp), ALLOCATABLE]: Error thresholds for selecting subconvergers. ! iter_space_size [INTEGER]: Size of the subconverger problem space, default is 10. ! verbose [INTEGER]: Verbosity level, default is 0. ! state [INTEGER]: Current state (0 = not initialized, 1 = initialized), default is 0. ! current_error [REAL(dp)]: Maximum absolute value of the current DIIS error, default is 1.0e99_dp. ! ! METHODS: ! init - Initializes the driver with iteration parameters and subconvergers. ! clean - Deallocates resources and resets the driver. ! add_data - Adds data from a new SCF iteration to the ring buffer. ! select_method - Selects the active subconverger based on the current error (private). ! run - Executes the selected subconverger and returns a result. ! compute_error - Computes the current DIIS error (private). ! ! USAGE EXAMPLE IS BELOW. ! !=============================================================================== !=============================================================================== ! TYPE: converger_data - DATA MANAGEMENT TYPE !=============================================================================== ! ! DESCRIPTION: ! The `converger_data` type implements a ring buffer for efficient storage ! of SCF iteration history. It manages data locality and memory usage by ! maintaining a fixed-size circular buffer rather than continuously growing ! arrays, improving performance for calculations with many iterations. ! ! MEMBERS: ! slot [INTEGER]: Current slot in the ring buffer (1-based index). ! num_saved [INTEGER]: Number of occupied slots in the buffer. ! num_slots [INTEGER]: Total capacity of the buffer. ! num_focks [INTEGER]: Number of Fock matrices per iteration (1 for RHF/ROHF, 2 for UHF). ! ldim [INTEGER]: Size of square matrices (nbf). ! nelec_a [INTEGER]: Number of alpha electrons. ! nelec_b [INTEGER]: Number of beta electrons. ! buffer [TYPE(scf_data_t), ALLOCATABLE]: Array of SCF data containers. ! ! METHODS: ! init - Initializes the ring buffer with specified dimensions. ! clean - Deallocates buffer resources. ! next_slot - Advances to the next slot in the buffer (private). ! discard_last - Removes the most recent iteration data (private). ! put - Stores new SCF data in the current slot (private). ! get_fock - Retrieves Fock matrix for specific iteration (private). ! get_mo_a - Retrieves alpha MO coefficients (private). ! get_mo_b - Retrieves beta MO coefficients (private). ! get_mo_e_a - Retrieves alpha MO energies (private). ! get_mo_e_b - Retrieves beta MO energies (private). ! get_pfon - Retrieves pFON object pointer (private). ! get_density - Retrieves density matrix (private). ! get_err - Retrieves error matrix (private). ! get_energy - Retrieves SCF energy (private). ! get_slot - Computes slot index from iteration number (private). ! ! USAGE NOTES: ! - Data access supports both positive indices (1 = oldest) and ! negative indices (-1 = latest) for intuitive programming. ! - Each slot contains complete SCF iteration data including Fock matrices, ! density matrices, MO coefficients, energies, and error matrices. ! - Integration with pFON allows storage of fractional occupation data ! when dealing with challenging electronic structures. ! !=============================================================================== !=============================================================================== ! TYPE: subconverger - ABSTRACT BASE TYPE !=============================================================================== ! ! DESCRIPTION: ! The `subconverger` type is an abstract base type defining the interface ! for all convergence methods used by `scf_conv`. Each subtype must implement ! the deferred procedures `run` and `setup`. ! ! MEMBERS: ! last_setup [INTEGER]: Number of SCF iterations since last setup, default is 1024. ! iter [INTEGER]: Number of iterations passed, default is 0. ! conv_name [CHARACTER(len=32)]: Name of the converger, default is an empty string. ! dat [TYPE(converger_data), POINTER]: Pointer to SCF data. ! ! METHODS: ! init - Initializes the subconverger (implemented as `subconverger_init`). ! clean - Deallocates resources (implemented as `subconverger_clean`). ! run - Executes the convergence method, returns a `scf_conv_result` (deferred). ! setup - Prepares the subconverger with new data (deferred). ! !=============================================================================== !=============================================================================== ! TYPE: noconv_converger - DUMMY CONVERGER !=============================================================================== ! ! DESCRIPTION: ! The `noconv_converger` type extends `subconverger` and implements a dummy ! convergence method that returns the latest iteration data without modification. ! ! METHODS: ! init - Initializes with the parent `scf_conv` instance. ! run - Returns the latest Fock matrix data in a `scf_conv_result`. ! setup - Does nothing (empty implementation). ! !=============================================================================== !=============================================================================== ! TYPE: cdiis_converger - COMMUTATOR DIIS !=============================================================================== ! ! DESCRIPTION: ! The `cdiis_converger` type extends `subconverger` and implements the ! Commutator DIIS (C-DIIS) method, solving a constrained minimization problem ! min { Ax, \sum_i x_i = 1 } where A_{ij} = Tr([F_i D_i S_i], [F_j D_j S_j]). ! ! MEMBERS: ! maxdiis [INTEGER]: Maximum number of DIIS vectors. ! a [REAL(dp), ALLOCATABLE]: C-DIIS A-matrix for error overlaps. ! verbose [INTEGER]: Verbosity level, default is 0. ! old_dim [INTEGER]: Dimension of A-matrix from the previous step, default is 0. ! ! METHODS: ! init - Initializes the C-DIIS instance. ! clean - Deallocates the A-matrix. ! run - Solves the DIIS equations and returns a `scf_conv_interp_result`. ! setup - Updates the A-matrix with new error data. ! !=============================================================================== !=============================================================================== ! TYPE: ediis_converger - ENERGY DIIS !=============================================================================== ! ! DESCRIPTION: ! The `ediis_converger` type extends `cdiis_converger` and implements the ! Energy DIIS (E-DIIS) method, optimizing the function ! E_E-DIIS = \sum_i c_i E_i - 0.5 \sum_{i,j} c_i c_j Tr((D_i-D_j)(F_i-F_j)) ! with constraints \sum_i c_i = 1, c_i \geq 0. ! ! MEMBERS: ! b [REAL(dp), ALLOCATABLE]: Energy history from previous iterations. ! xlog [REAL(dp), ALLOCATABLE]: History of interpolation coefficients. ! t [TYPE(ediis_opt_data)]: Optimization data structure for NLOpt. ! fun [PROCEDURE(eadiis_f), POINTER]: Pointer to the objective function. ! ! METHODS: ! init - Initializes the E-DIIS instance. ! clean - Deallocates additional arrays (b, xlog). ! setup - Prepares the optimization problem with energy and matrix data. ! run - Solves the optimization problem and returns a `scf_conv_interp_result`. ! !=============================================================================== !=============================================================================== ! TYPE: adiis_converger - APPROXIMATE DIIS !=============================================================================== ! ! DESCRIPTION: ! The `adiis_converger` type extends `ediis_converger` and implements the ! Approximate DIIS (A-DIIS) method, optimizing the function ! E_A-DIIS = \sum_i c_i Tr((D_i-D_n)F_n) + 2 \sum_{i,j} c_i c_j Tr((D_i-D_n)(F_j-F_n)) ! with constraints \sum_i c_i = 1, c_i \geq 0. ! ! METHODS: ! init - Initializes the A-DIIS instance (inherits from `ediis_converger`). ! setup - Prepares the modified optimization problem specific to A-DIIS. ! ! NOTES: ! - Inherits all members and most methods from `ediis_converger`. ! - Only `setup` is overridden to adjust the objective function. ! !=============================================================================== !=============================================================================== ! TYPE: soscf_converger - SECOND-ORDER SCF !=============================================================================== ! ! DESCRIPTION: ! The `soscf_converger` type extends `subconverger` and implements the ! Second-Order SCF (SOSCF) method, using orbital gradients and an L-BFGS ! approximation of the Hessian for convergence. ! ! MEMBERS: ! verbose [INTEGER]: Verbosity level, default is 0. ! nfocks [INTEGER]: Number of Fock matrices (1 for RHF, 2 for UHF), default is 0. ! nbf [INTEGER]: Number of basis functions, default is 0. ! nbf_tri [INTEGER]: Triangular size of basis functions (nbf*(nbf+1)/2), default is 0. ! scf_type [INTEGER]: SCF type (1=RHF, 2=UHF, 3=ROHF), default is 0. ! nocc_a [INTEGER]: Number of occupied alpha orbitals, default is 0. ! nocc_b [INTEGER]: Number of occupied beta orbitals, default is 0. ! nvec [INTEGER]: Size of the gradient vector, default is 0. ! overlap [REAL(dp), POINTER]: Pointer to overlap matrix, null by default. ! overlap_invsqrt [REAL(dp), POINTER]: Pointer to inverse square root of overlap, null by default. ! max_iter [INTEGER]: Maximum micro-iterations, default is 10. ! min_iter [INTEGER]: Minimum micro-iterations, default is 1. ! hess_thresh [REAL(dp)]: Orbital Hessian threshold, default is 1.0e-10_dp. ! grad_thresh [REAL(dp)]: Gradient threshold, default is 1.0e-5_dp. ! level_shift [REAL(dp)]: Level shifting parameter, default is 0.2_dp. ! s_history [REAL(dp), ALLOCATABLE]: L-BFGS step history (nvec, m_max). ! rho_history [REAL(dp), ALLOCATABLE]: L-BFGS curvature reciprocals (m_max). ! y_history [REAL(dp), ALLOCATABLE]: L-BFGS gradient difference history (nvec, m_max). ! grad [REAL(dp), ALLOCATABLE]: Current gradient (nvec). ! x [REAL(dp), ALLOCATABLE]: Current x (nvec). ! grad_prev [REAL(dp), ALLOCATABLE]: Previous gradient (nvec). ! x_prev [REAL(dp), ALLOCATABLE]: Previous rotation parameters (nvec). ! h_inv [REAL(dp), ALLOCATABLE]: Initial inverse Hessian diagonal (nvec). ! work_1 [REAL(dp), ALLOCATABLE]: Working matrix (nbf, nbf). ! work_2 [REAL(dp), ALLOCATABLE]: Working matrix (nbf, nbf). ! work_3 [REAL(dp), ALLOCATABLE]: Working matrix (nbf, nbf). ! mo_a [REAL(dp), ALLOCATABLE]: Alpha MO coefficients (nbf, nbf). ! mo_b [REAL(dp), ALLOCATABLE]: Beta MO coefficients (nbf, nbf). ! dens_a [REAL(dp), ALLOCATABLE]: Alpha density matrix (nbf_tri). ! dens_b [REAL(dp), ALLOCATABLE]: Beta density matrix (nbf_tri). ! m_max [INTEGER]: Maximum number of stored history steps, default is 0. ! m_history [INTEGER]: Current number of stored history steps, default is 0. ! first_macro [LOGICAL]: Flag for first macro-iteration, default is .true. ! ! METHODS: ! init - Initializes the SOSCF instance. ! clean - Deallocates arrays and resets pointers. ! setup - Prepares data for SOSCF iterations. ! run - Executes SOSCF micro-iterations and returns a `scf_conv_soscf_result`. ! init_hess_inv - Computes the initial inverse Hessian diagonal. ! calc_orb_grad - Calculates the orbital gradient. ! bfgs - Computes the orbital rotation x using L-BFGS. ! rotate_orbs - Applies the orbital rotation. ! !=============================================================================== !=============================================================================== ! TYPE: scf_conv_result - BASE RESULT TYPE !=============================================================================== ! ! DESCRIPTION: ! The `scf_conv_result` type is a base type for returning results from ! convergence methods executed by `scf_conv`. It provides default ! implementations that do nothing, overridden by subtypes. ! ! MEMBERS: ! ierr [INTEGER]: Error status (0 = success, nonzero = failure), default is 5. ! error [REAL(dp)]: Current DIIS error magnitude, default is 1.0e99_dp. ! dat [TYPE(converger_data), POINTER]: Pointer to SCF data, null by default. ! active_converger_name [CHARACTER(len=32)]: Name of the active converger, default is empty. ! ! METHODS: ! get_error - Returns the current error value. ! get_fock - Placeholder returning no data (overridden by subtypes). ! get_density - Placeholder returning no data (overridden by subtypes). ! get_mo_a - Placeholder returning no data (overridden by subtypes). ! get_mo_b - Placeholder returning no data (overridden by subtypes). ! get_mo_e_a - Placeholder returning no data (overridden by subtypes). ! get_mo_e_b - Placeholder returning no data (overridden by subtypes). ! !=============================================================================== !=============================================================================== ! TYPE: scf_conv_interp_result - INTERPOLATION RESULT TYPE !=============================================================================== ! ! DESCRIPTION: ! The `scf_conv_interp_result` type extends `scf_conv_result` and provides ! results for interpolation-based methods (e.g., DIIS), computing updated ! Fock or density matrices as F_n = \sum_i F_i * c_i or D_n = \sum_i D_i * c_i. ! ! MEMBERS: ! coeffs [REAL(dp), ALLOCATABLE]: Interpolation coefficients c_i. ! pstart [INTEGER]: Start index of the interpolation range. ! pend [INTEGER]: End index of the interpolation range. ! ! METHODS: ! get_fock - Returns the interpolated Fock matrix. ! get_density - Returns the interpolated density matrix. ! interpolate - Computes the interpolated matrix (Fock or density, private). ! !=============================================================================== !=============================================================================== ! TYPE: scf_conv_soscf_result - SOSCF RESULT TYPE !=============================================================================== ! ! DESCRIPTION: ! The `scf_conv_soscf_result` type extends `scf_conv_result` and provides ! results specific to the SOSCF method, returning the latest MO coefficients, ! and MO energies. ! ! METHODS: ! get_mo_a - Returns the updated alpha MO coefficients. ! get_mo_b - Returns the updated beta MO coefficients. ! get_mo_e_a - Returns the updated alpha MO energies. ! get_mo_e_b - Returns the updated beta MO energies. ! !=============================================================================== !=============================================================================== ! EXAMPLE USAGE !=============================================================================== ! ! use scf_converger ! type(scf_conv) :: conv ! class(scf_conv_result), allocatable :: res ! ! Initialize converger ! call conv%init(ldim=nbf, ! maxvec=15, & ! subconvergers=[conv_cdiis, conv_ediis], & ! thresholds=[2.0_dp, 1.0_dp], & ! overlap=s_mat, ! overlap_sqrt=s_sqrt, & ! num_focks=1, ! verbose=1) ! ! In SCF loop: ! do iter = 1, max_iter ! ! Build Fock matrix... ! ! ! Add data to converger ! call conv%add_data(f=fock, dens=density, e=energy) ! ! ! Run convergence step and get new Fock matrix ! call conv%run(conv_res) ! call conv_res%get_fock(matrix=fock, istat=stat) ! ! ! Check convergence ! diis_error = conv_res%get_error() ! if (diis_error < thresh) exit ! end do ! ! Clean converger ! call conv%clean() !=============================================================================== ! EXAMPLE USAGE !=============================================================================== module scf_converger use precision, only: dp use io_constants, only: iw use mathlib, only: traceprod_sym_packed, orb_to_dens use messages, only: show_message, with_abort use mathlib, only: antisymmetrize_matrix, unpack_matrix, pack_matrix use scf_addons, only: pfon_t use, intrinsic :: iso_fortran_env, only: int32 use, intrinsic :: iso_c_binding, only: c_bool use types, only: information use mod_dft_molgrid, only: dft_grid_t implicit none private ! -- SOSCF variant selector (always SOSCF_VARIANT_ORIGINAL) -- integer, parameter, public :: SOSCF_VARIANT_ORIGINAL = 0 integer, parameter, public :: SOSCF_VARIANT_STABLE_ONLY = 1 integer, parameter, public :: SOSCF_VARIANT_QUAD_LS = 2 ! public :: scf_conv public :: scf_conv_result public :: scf_conv_trah_result public :: soscf_converger ! Used to provide parametres for soscf public :: soscf_set_variant ! SOSCF mode setter public :: trah_converger ! Used to provide parametres dor trah !> Constants for converger types integer, parameter, public :: conv_none = 1 integer, parameter, public :: conv_cdiis = 2 integer, parameter, public :: conv_ediis = 3 integer, parameter, public :: conv_adiis = 4 integer, parameter, public :: conv_soscf = 5 integer, parameter, public :: conv_trah = 6 !> Converger state constants integer, parameter, public :: conv_state_not_initialized = 0 integer, parameter, public :: conv_state_initialized = 1 !> Maximum length for converger names integer, parameter, public :: conv_name_maxlen = 32 integer, parameter, public :: SCF_RHF=1, SCF_UHF=2, SCF_ROHF=3 !> @brief Type to encapsulate SCF iteration data !> @detail Stores Fock, density, DIIS error matrices, and SCF energy for a single SCF iteration. !> Designed for efficient memory use and data locality in a ring buffer. type :: scf_data_t real(kind=dp), allocatable :: focks(:,:) !< Fock matrices (nbf_tri, num_focks) real(kind=dp), allocatable :: densities(:,:) !< Density matrices (nbf_tri, num_focks) real(kind=dp), allocatable :: errs(:,:) !< DIIS error matrices (nbf_tri, num_focks) real(kind=dp), allocatable :: mo_a(:,:) !< MO coefficients alpha (nbf, nbf) real(kind=dp), allocatable :: mo_b(:,:) !< MO coefficients beta (nbf, nbf) real(kind=dp), allocatable :: mo_e_a(:) !< MO energies alpha (nbf) real(kind=dp), allocatable :: mo_e_b(:) !< MO energies beta (nbf) real(kind=dp) :: energy = 0.0_dp !< SCF energy type(pfon_t), pointer :: pfon_obj => null() !< Pseudo-Fractional Occupation Number (pFON) type real(kind=dp), pointer :: occ_a(:) => null() !< Alpha orbital occupations (nbf) real(kind=dp), pointer :: occ_b(:) => null() !< Beta orbital occupations (nbf) contains procedure, private, pass :: init => scf_data_init procedure, private, pass :: clean => scf_data_clean end type scf_data_t !> @brief Storage of SCF iteration history using a ring buffer of scf_data_t !> @detail Manages a cyclic buffer of SCF data up to num_slots. Data is stored in a single !> array of scf_data_t for better encapsulation and cache efficiency. type :: converger_data integer :: slot = 0 !< Current slot (1-based index) integer :: num_saved = 0 !< Number of occupied slots integer :: num_slots = 0 !< Total number of slots in buffer integer :: num_focks = 0 !< Number of Fock matrices per iteration (1 for RHF/ROHF, 2 for UHF) integer :: ldim = 0 !< Size of square matrices (nbf) integer :: nelec_a = 0 !< Number of occupied alpha orbitals integer :: nelec_b = 0 !< Number of occupied beta orbitals type(scf_data_t), allocatable :: buffer(:) !< Ring buffer of SCF data contains procedure, private, pass :: init => conv_data_init procedure, private, pass :: clean => conv_data_clean procedure, private, pass :: next_slot => conv_data_next_slot procedure, private, pass :: discard_last => conv_data_discard procedure, private, pass :: put => conv_data_put procedure, private, pass :: get_fock => conv_data_get_fock procedure, private, pass :: get_mo_a => conv_data_get_mo_a procedure, private, pass :: get_mo_b => conv_data_get_mo_b procedure, private, pass :: get_mo_e_a => conv_data_get_mo_e_a procedure, private, pass :: get_mo_e_b => conv_data_get_mo_e_b procedure, private, pass :: get_pfon => conv_data_get_pfon procedure, private, pass :: get_density => conv_data_get_density procedure, private, pass :: get_err => conv_data_get_err procedure, private, pass :: get_energy => conv_data_get_energy procedure, private, pass :: get_slot => conv_data_get_slot end type converger_data !> @brief Base type for SCF converger results !> @detail Used by the main SCF convergence driver `scf_conv` to return results. !> The extending type should provide the following interfaces: !> init : preliminary initialization of internal subconverger data !> clean : destructor !> setup : setting-up of the sub-converger equations, taking into the account the new data !> added in main SCF driver !> run : solving the equations, returns `scf_conv_result` datatype. Multiple subsequent calls to !> this procedure without re-running `setup` should not change internal state and have to give same results type :: scf_conv_result integer :: ierr = 5 !< Error status (0 = success, nonzero = failure) real(kind=dp) :: error = 1.0e99_dp !< Current DIIS error magnitude type(converger_data), pointer :: dat => null() !< Pointer to SCF data storage character(len=conv_name_maxlen) :: active_converger_name = '' !< Name of active converger contains procedure, pass :: get_error => conv_result_get_error procedure, pass :: get_fock => conv_result_dummy_get_fock procedure, pass :: get_density => conv_result_dummy_get_density procedure, pass :: get_mo_a => conv_result_dummy_get_mo_a procedure, pass :: get_mo_b => conv_result_dummy_get_mo_b procedure, pass :: get_mo_e_a => conv_result_dummy_get_mo_e_a procedure, pass :: get_mo_e_b => conv_result_dummy_get_mo_e_b procedure, pass :: get_rms_grad => conv_result_dummy_get_rms_g procedure, pass :: get_iter => conv_result_dummy_get_iter procedure, pass :: get_rms_dp => conv_result_dummy_get_rms_dp procedure, pass :: get_etot => conv_result_dummy_get_etot end type scf_conv_result !> @brief SCF converger results for interpolation methods like DIIS !> @details The updated Fock/density is computed as F_n = \sum_{i} F_i * c_i type, extends(scf_conv_result) :: scf_conv_interp_result real(kind=dp), allocatable :: coeffs(:) !< Interpolation coefficients c_i integer :: pstart !< Start index of interpolation range integer :: pend !< End index of interpolation range contains procedure, pass :: get_fock => conv_result_interp_get_fock procedure, pass :: get_density => conv_result_interp_get_density procedure, private, pass :: interpolate => conv_result_interpolate end type scf_conv_interp_result !> @brief SCF converger results for SOSCF method type, extends(scf_conv_result) :: scf_conv_soscf_result real(kind=dp) :: rms_grad = 1 real(kind=dp) :: rms_dp = 1 contains procedure, pass :: get_mo_a => conv_result_soscf_get_mo_a procedure, pass :: get_mo_b => conv_result_soscf_get_mo_b procedure, pass :: get_mo_e_a => conv_result_soscf_get_mo_e_a procedure, pass :: get_mo_e_b => conv_result_soscf_get_mo_e_b procedure, pass :: get_rms_grad => conv_result_soscf_get_rms_g procedure, pass :: get_rms_dp => conv_result_soscf_get_rms_dp end type scf_conv_soscf_result type, extends(scf_conv_result) :: scf_conv_trah_result integer :: iter = 0 real(kind=dp) :: rms_grad = 1 contains procedure, pass :: get_mo_a => conv_result_trah_get_mo_a procedure, pass :: get_mo_b => conv_result_trah_get_mo_b procedure, pass :: get_mo_e_a => conv_result_trah_get_mo_e_a procedure, pass :: get_mo_e_b => conv_result_trah_get_mo_e_b procedure, pass :: get_fock => conv_result_trah_get_fock procedure, pass :: get_rms_grad => conv_result_trah_get_rms_g procedure, pass :: get_iter => conv_result_trah_get_iter end type scf_conv_trah_result !> @brief Base type for real SCF convergers (subconvergers) !> @detail Used by main SCF convergence driver `scf_conv`. !> The extending type should provide the following interfaces: !> init : preliminary initialization of internal subconverger data !> clean : destructor !> setup : setting-up of the sub-converger equations, taking into the account the new data !> added in main SCF driver !> run : solving the equations, returns `scf_conv_result` datatype. Multiple subsequent calls to !> this procedure without re-running `setup` should not change internal state and have to give same results type, abstract :: subconverger integer :: last_setup = 1024 !< Number of SCF iterations since last setup integer :: iter = 0 !< Number of iterations passed character(len=conv_name_maxlen) :: conv_name = '' !< Converger name type(converger_data), pointer :: dat => null() !< Pointer to SCF data contains private procedure, pass :: subconverger_init procedure, pass :: subconverger_clean procedure, public, pass :: init => subconverger_init procedure, public, pass :: clean => subconverger_clean procedure(subconverger_run), pass, deferred :: run procedure(subconverger_setup), pass, deferred :: setup end type subconverger !> @brief Container type for subconvergers !> @detail Used to create allocatable array of allocatable types type :: subconverger_ class(subconverger), allocatable :: s end type subconverger_ !> @brief Main driver for converging SCF problems !> @detail Manages and runs different real convergers depending on the current !> state of SCF optimization. type :: scf_conv integer :: step = 0 !< Current SCF iteration step real(kind=dp), pointer :: overlap(:,:) => null() !< Pointer to full-format overlap matrix (S) real(kind=dp), pointer :: overlap_sqrt(:,:) => null() !< Pointer to full-format S^(1/2) matrix type(converger_data) :: dat !< Storage of SCF iteration history type(subconverger_), allocatable :: sconv(:) !< Array of subconverger methods real(kind=dp), allocatable :: thresholds(:) !< Thresholds to initiate subconvergers integer :: iter_space_size = 10 !< Default size of subconverger problem space integer :: verbose = 0 !< Verbosity level integer :: state = 0 !< Current state (0 = not initialized, 1 = initialized) real(kind=dp) :: current_error = 1.0e99_dp !< Maximum absolute value of current DIIS error integer :: scf_type = 0 contains procedure, pass :: init => scf_conv_init procedure, pass :: clean => scf_conv_clean procedure, pass :: add_data => scf_conv_add_data procedure, private, pass :: select_method => scf_conv_select procedure, pass :: run => scf_conv_run procedure, private, pass :: compute_error => scf_conv_compute_error end type scf_conv !> @brief Dummy (steepest descent) converger !> @detail Used for convenience, does nothing but return the latest data. type, extends(subconverger) :: noconv_converger contains procedure, pass :: init => noconv_init procedure, pass :: run => noconv_run procedure, pass :: setup => noconv_setup end type noconv_converger !> @brief Commutator DIIS (C-DIIS) converger !> @detail Solves constraint minimization problem: min { Ax, \sum_i x_i = 1 } !> where A_{ij} = Tr([F_i D_i S_i], [F_j D_j S_j]) type, extends(subconverger) :: cdiis_converger integer :: maxdiis !< Maximum number of DIIS vectors real(kind=dp), allocatable :: a(:,:) !< C-DIIS A-matrix integer :: verbose = 0 !< Verbosity parameter integer :: old_dim = 0 !< Dimension of A matrix from previous step contains procedure, pass :: init => cdiis_init procedure, pass :: clean => cdiis_clean procedure, pass :: run => cdiis_run procedure, pass :: setup => cdiis_setup end type cdiis_converger !> @brief Datatype to pass optimization parameters to NLOpt for E/A-DIIS !> @detail Used for solving constraint minimization problems in E/A-DIIS: !> \f$ min \{ x^T A x + bx,\ x_i \geq 0,\ \sum_i{x_i} = 1 \} \f$ type :: ediis_opt_data real(kind=8), pointer :: b(:) => null() !< Energy or linear term coefficients real(kind=8), pointer :: A(:,:) => null() !< Quadratic term matrix procedure(eadiis_f), pointer, nopass :: fun => null() !< Objective function pointer end type ediis_opt_data !> @brief Energy DIIS (E-DIIS) converger !> @detail Optimizes the following function: !> \f$ E_\mathrm{E-DIIS} = \sum_{i} c_i E_i - 0.5 \sum_{i,j} c_i c_j Tr( (D_i-D_j) (F_i-F_j) ) \f$ !> under the constraints: !> \f$ \sum_i {c_i} = 1, c_i \geq 0 \f$ type, extends(cdiis_converger) :: ediis_converger real(kind=dp), allocatable :: b(:) !< Energy history real(kind=dp), allocatable :: xlog(:,:) !< Interpolation coefficients history type(ediis_opt_data) :: t !< Optimization data for NLOpt procedure(eadiis_f), pointer, nopass :: fun => null() !< Pointer to objective function contains procedure, pass :: init => ediis_init procedure, pass :: clean => ediis_clean procedure, pass :: setup => ediis_setup procedure, pass :: run => ediis_run end type ediis_converger !> @brief A-DIIS subconverger type !> @detail Optimizes the following function: !> \f$ E_\mathrm{A-DIIS} = \sum_{i} c_i Tr((D_i-D_n)F_n) + 2 \sum_{i,j} c_i c_j Tr( (D_i-D_n) (F_j-F_n) ) \f$ !> under the constraints: !> \f$ \sum_i {c_i} = 1, c_i \geq 0 \f$ type, extends(ediis_converger) :: adiis_converger contains procedure, pass :: init => adiis_init procedure, pass :: setup => adiis_setup end type adiis_converger abstract interface subroutine subconverger_run(self, res) import class(subconverger), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: res end subroutine subroutine subconverger_setup(self) import class(subconverger), intent(inout) :: self end subroutine subroutine eadiis_f(val, n, t, grad, need_gradient, d) import implicit none real(kind=8) :: val, t(*), grad(*) integer(kind=4), intent(in) :: n, need_gradient type(ediis_opt_data), intent(in) :: d end subroutine end interface !> @brief SOSCF subconverger type !> @detail Implementation is based on ! T. H. Fischer, J. Almlof, Journal of Physical Chemistry, 96(24), 9768-9774 (1992) ! F. Neese, Chemical Physics Letters, 325(1-3), 93-98 (2000) type, extends(subconverger) :: soscf_converger integer :: verbose = 0 !< Verbosity parameter integer :: nfocks = 0 !< Number of Focks (RHF = 1, UHF = 2) integer :: nbf = 0 !< Number of basis functions integer :: nbf_tri = 0 !< Number of basis functions in triangular format integer :: scf_type = 0 !< SCF type: 1:RHF, 2:UHF, 3:ROHF integer :: nocc_a = 0 !< Number of occupied alpha orbitals integer :: nocc_b = 0 !< Number of occupied beta orbitals integer :: nvec = 0 !< Size of the gradient vector ! === cheap step-control state (SOSCF only) === real(dp) :: alpha_cap = 1.0_dp real(dp) :: kappa_lim = 0.8_dp real(dp) :: alpha_last = 0.0_dp real(dp) :: gg_last = 0.0_dp real(dp) :: hh_last = 1.0_dp real(dp) :: g_rms_last = 0.0_dp integer :: polish_cnt = 0 real(kind=dp), pointer :: overlap(:, :) => null() real(kind=dp), pointer :: overlap_invsqrt(:, :) => null() class(information), pointer :: infos ! ! SOSCF parameters from scf_driver: real(kind=dp) :: hess_thresh = 1.0e-10_dp !< Orbital Hessian threshold real(kind=dp) :: grad_thresh = 1.0e-3_dp !< Gradient threshold real(kind=dp) :: level_shift = 0.0_dp !< Level shifting parameter real(kind=dp) :: rms_grad_prev = 0.0_dp !< previous Gradient norm integer :: soscf_reset_mod = 1 !< Set the SOSCF Hessian reset mode. logical :: use_lineq = .false. !< Use linear equations (vs BFGS) ! L-BFGS history real(kind=dp), allocatable :: s_history(:,:) !< Step history (nvec, m_max) real(kind=dp), allocatable :: rho_history(:) !< Curvature reciprocals (m_max) real(kind=dp), allocatable :: y_history(:,:) !< Gradient difference history (nvec, m_max) real(kind=dp), allocatable :: upd_history(:,:)! h_inv*dgrad history (nvec, m_max) real(kind=dp), allocatable :: grad(:) !< Gradient (nvec) real(kind=dp), allocatable :: x(:) !< Step (nvec) real(kind=dp), allocatable :: x_prev(:) !< Step (nvec) real(kind=dp), allocatable :: grad_prev(:) !< Previous gradient (nvec) real(kind=dp), allocatable :: h_inv(:) !< Initial inverse Hessian diagonal (nvec) real(kind=dp), allocatable :: work_1(:,:) !< Work matrix (nbf, nbf) real(kind=dp), allocatable :: work_2(:,:) !< Work matrix (nbf, nbf) real(kind=dp), allocatable :: work_3(:,:) !< Work matrix (nbf, nbf) real(kind=dp), allocatable :: mo_a(:,:) !< MOs matrix (nbf, nbf) real(kind=dp), allocatable :: mo_b(:,:) !< MOs matrix (nbf, nbf) real(kind=dp), allocatable :: dens_a(:) !< MOs matrix (nbf_tri) real(kind=dp), allocatable :: dens_b(:) !< MOs matrix (nbf_tri) real(kind=dp), allocatable :: dens_a_old(:) !< MOs matrix (nbf_tri) real(kind=dp), allocatable :: dens_b_old(:) !< MOs matrix (nbf_tri) integer :: m_max = 0 !< Maximum number of stored history steps integer :: m_history = 0 !< Number of stored history steps logical :: first_macro = .true. !< Flag for first macro-iteration ! 0: original; 1: stability-only; 2: stability + quadratic LS + 1-D TR integer :: variant = SOSCF_VARIANT_ORIGINAL contains procedure, pass :: init => soscf_init procedure, pass :: clean => soscf_clean procedure, pass :: setup => soscf_setup procedure, pass :: run => soscf_run procedure, private, pass :: init_hess_inv => init_hess_inv procedure, private, pass :: calc_orb_grad => calc_orb_grad procedure, private, pass :: bfgs => bfgs procedure, private, pass :: rotate_orbs => rotate_orbs procedure, private, pass :: rms_density => rms_density end type soscf_converger !================================================================= !impementation of trust region agumented hessian !================================================================= type, extends(subconverger) :: trah_converger integer :: scf_type = SCF_RHF integer :: nbf = 0 integer :: nbf_tri = 0 integer :: nocc_a = 0 integer :: nocc_b = 0 integer :: nvir_a = 0 integer :: nvir_b = 0 integer :: nfocks = 1 integer :: verbose = 0 real(dp) :: etot = 0 integer(int32) :: n_param = 0 logical :: is_dft = .false. real(dp) :: hf_scale = 1.0_dp real(dp), allocatable :: fock_ao(:,:) ! fock matrix fock_ao (nbf_tri, nfock) real(dp), allocatable :: dens(:,:) ! density matrix (nbf_tri, nfocks) real(dp), allocatable :: f_old(:,:) real(dp), allocatable :: d_old(:,:) real(kind=dp), pointer :: overlap(:, :) => null() real(kind=dp), pointer :: overlap_invsqrt(:, :) => null() class(information), pointer :: infos type(dft_grid_t), pointer :: molgrid real(dp), allocatable :: mo_a(:,:), mo_b(:,:) real(dp), allocatable :: foo_a(:,:), fvv_a(:,:), xmat_a(:,:), x2mat_a(:,:) real(dp), allocatable :: foo_b(:,:), fvv_b(:,:), xmat_b(:,:), x2mat_b(:,:) real(dp), allocatable :: v(:,:) ! response v(nbf, nbf) real(dp), allocatable :: dm(:,:) ! response density dm(nbf,nbf) real(dp), allocatable :: pfock(:,:) ! Fock matrix (nbf_tri, nfocks) from respose density real(dp), allocatable :: dm_tri(:,:) ! packed DM (nbf_tri, nfocks) real(dp), allocatable :: work1(:,:) ! (nbf, nbf) real(dp), allocatable :: work2(:,:) ! (nbf, nbf) real(dp), allocatable :: work3(:,:) ! (nbf, nbf) contains procedure, pass :: init => trah_init procedure, pass :: clean => trah_clean procedure, pass :: setup => trah_setup procedure, pass :: run => trah_run procedure, pass :: rotate_orbs => rotate_orbs_trah procedure, pass :: calc_h_op => calc_h_op procedure, pass :: calc_g_h => calc_g_h end type trah_converger contains subroutine soscf_set_variant(obj, variant) class(subconverger), intent(inout) :: obj integer, intent(in) :: variant select type(me => obj) type is (soscf_converger) me%variant = variant class default ! ignore for non-SOSCF convergers end select end subroutine soscf_set_variant !============================================================================== ! scf_data_t Methods !============================================================================== !> @brief Initialize an scf_data_t instance !> @param[inout] self The scf_data_t object to initialize. !> @param[in] ldim Size of triangular matrices (nbf*(nbf+1)/2) !> @param[in] nfocks Number of Fock matrices (1 for RHF/ROHF, 2 for UHF) !> @param[out] istat Status code (0 for success, nonzero for allocation failure) subroutine scf_data_init(self, ldim, nfocks, istat) class(scf_data_t), intent(inout) :: self integer, intent(in) :: ldim, nfocks integer, intent(out) :: istat integer :: nbf_tri istat = 0 nbf_tri = ldim * (ldim + 1) / 2 if (allocated(self%focks)) call self%clean() allocate(self%focks(nbf_tri, nfocks), & self%densities(nbf_tri, nfocks), & self%errs(nbf_tri, nfocks), & stat=istat) ! Note: MO coefficients mo_a, mo_b and energies mo_e_a, mo_e_b ! Note: are allocated on demand in conv_data_put self%densities = 0.0_dp self%focks = 0.0_dp self%errs = 0.0_dp self%energy = 0.0_dp end subroutine scf_data_init !> @brief Clean up an scf_data_t instance subroutine scf_data_clean(self) class(scf_data_t), intent(inout) :: self if (allocated(self%focks)) deallocate(self%focks) if (allocated(self%densities)) deallocate(self%densities) if (allocated(self%errs)) deallocate(self%errs) self%occ_a => null() self%occ_b => null() self%energy = 0.0_dp end subroutine scf_data_clean !============================================================================== ! converger_data Methods !============================================================================== !> @brief Set up a ring buffer to store SCF iteration history. !> @param[inout] self The converger_data object to initialize. !> @param[in] ldim Number of basis functions (nbf) !> @param[in] nfocks Number of Fock matrices per iteration (1 for RHF/ROHF, 2 for UHF) !> @param[in] nslots Maximum number of SCF iterations to store !> @param[out] istat Success status (0 for success, nonzero for error) subroutine conv_data_init(self, ldim, nfocks, nslots, istat, nelec_a, nelec_b) class(converger_data), intent(inout) :: self integer, intent(in) :: ldim, nfocks, nslots integer, intent(out) :: istat integer, intent(in), optional :: nelec_a, nelec_b integer :: i istat = 0 if (allocated(self%buffer)) call self%clean() self%ldim = ldim self%num_focks = nfocks self%num_slots = nslots self%num_saved = 0 self%slot = 0 if (present(nelec_a)) self%nelec_a = nelec_a if (present(nelec_b)) self%nelec_b = nelec_b allocate(self%buffer(nslots), stat=istat) do i = 1, nslots call self%buffer(i)%init(ldim, nfocks, istat) end do end subroutine conv_data_init !> @brief Finalize converger_data and deallocate memory subroutine conv_data_clean(self) class(converger_data), intent(inout) :: self integer :: i if (allocated(self%buffer)) then do i = 1, self%num_slots call self%buffer(i)%clean() end do deallocate(self%buffer) end if self%ldim = 0 self%num_focks = 0 self%num_slots = 0 self%num_saved = 0 self%slot = 0 end subroutine conv_data_clean !> @brief Advance to the next slot in the ring buffer subroutine conv_data_next_slot(self) class(converger_data), intent(inout) :: self self%slot = mod(self%slot, self%num_slots) + 1 self%num_saved = min(self%num_saved + 1, self%num_slots) end subroutine conv_data_next_slot !> @brief Discard the most recent data entry subroutine conv_data_discard(self) class(converger_data), intent(inout) :: self self%slot = mod(self%slot - 2, self%num_slots) + 1 self%num_saved = min(self%num_saved - 1, 1) end subroutine conv_data_discard !> @brief Store SCF data for the current iteration !> @param[in] fock Fock matrices (optional) !> @param[in] dens Density matrices (optional) !> @param[in] energy SCF energy (optional) !> @param[in] mo_a Alpha MO coefficients (optional) !> @param[in] mo_b Beta MO coefficients (optional) !> @param[in] mo_e_a Alpha MO energies (optional) !> @param[in] mo_e_b Beta MO energies (optional) subroutine conv_data_put(self, fock, dens, energy, mo_a, mo_b, & mo_e_a, mo_e_b, pfon_obj) class(converger_data), intent(inout) :: self real(kind=dp), intent(in), optional :: fock(:,:) real(kind=dp), intent(in), optional :: dens(:,:) real(kind=dp), intent(in), optional :: energy real(kind=dp), intent(in), optional :: mo_a(:,:), mo_b(:,:) real(kind=dp), intent(in), optional :: mo_e_a(:), mo_e_b(:) type(pfon_t), optional, target, intent(in) :: pfon_obj integer :: slot, nbf nbf = self%ldim slot = self%slot if (present(fock)) self%buffer(slot)%focks = fock if (present(dens)) self%buffer(slot)%densities = dens if (present(energy)) self%buffer(slot)%energy = energy if (present(mo_a)) then if (.not. allocated(self%buffer(slot)%mo_a)) then allocate(self%buffer(slot)%mo_a(nbf, nbf)) end if self%buffer(slot)%mo_a = mo_a end if if (present(mo_b)) then if (.not. allocated(self%buffer(slot)%mo_b)) then allocate(self%buffer(slot)%mo_b(nbf, nbf)) end if self%buffer(slot)%mo_b = mo_b end if if (present(mo_e_a)) then if (.not. allocated(self%buffer(slot)%mo_e_a)) then allocate(self%buffer(slot)%mo_e_a(nbf)) end if self%buffer(slot)%mo_e_a = mo_e_a end if if (present(mo_e_b)) then if (.not. allocated(self%buffer(slot)%mo_e_b)) then allocate(self%buffer(slot)%mo_e_b(nbf)) end if self%buffer(slot)%mo_e_b = mo_e_b end if if (present(pfon_obj)) then self%buffer(slot)%pfon_obj => pfon_obj end if end subroutine conv_data_put !> @brief Get Fock matrix for a specific iteration and matrix ID !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @param[in] matrix_id Fock matrix index (1 = alpha, 2 = beta) !> @return Pointer to the Fock matrix function conv_data_get_fock(self, n, matrix_id) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n, matrix_id real(kind=dp), pointer :: res(:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%focks(:, matrix_id) end function conv_data_get_fock !> @brief Get MO coefficients for alpha orbitals for a specific iteration !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @return Pointer to the alpha MO coefficients function conv_data_get_mo_a(self, n) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n real(kind=dp), pointer :: res(:,:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%mo_a end function conv_data_get_mo_a !> @brief Get MO coefficients for beta orbitals for a specific iteration !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @return Pointer to the beta MO coefficients function conv_data_get_mo_b(self, n) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n real(kind=dp), pointer :: res(:,:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%mo_b end function conv_data_get_mo_b !> @brief Get alpha MO energies for a specific iteration !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @return Pointer to the alpha MO energies function conv_data_get_mo_e_a(self, n) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n real(kind=dp), pointer :: res(:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%mo_e_a end function conv_data_get_mo_e_a !> @brief Get beta MO energies for a specific iteration !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @return Pointer to the beta MO energies function conv_data_get_mo_e_b(self, n) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n real(kind=dp), pointer :: res(:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%mo_e_b end function conv_data_get_mo_e_b !> @brief Get pfon object for a specific iteration !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @return Pointer to the pfon object function conv_data_get_pfon(self, n) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n type(pfon_t), pointer :: res integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%pfon_obj end function conv_data_get_pfon !> @brief Get density matrix for a specific iteration and matrix ID !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @param[in] matrix_id Density matrix index (1 = alpha, 2 = beta) !> @return Pointer to the density matrix function conv_data_get_density(self, n, matrix_id) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n, matrix_id real(kind=dp), pointer :: res(:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%densities(:, matrix_id) end function conv_data_get_density !> @brief Get DIIS error matrix for a specific iteration and matrix ID !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @param[in] matrix_id Error matrix index (1 = alpha, 2 = beta) !> @return Pointer to the error matrix function conv_data_get_err(self, n, matrix_id) result(res) class(converger_data), intent(in), target :: self integer, intent(in) :: n, matrix_id real(kind=dp), pointer :: res(:) integer :: slot slot = self%get_slot(n) res => self%buffer(slot)%errs(:, matrix_id) end function conv_data_get_err !> @brief Get SCF energy for a specific iteration !> @param[in] n Slot ID (1 = oldest, -1 = latest) !> @return SCF energy value function conv_data_get_energy(self, n) result(res) class(converger_data), intent(in) :: self integer, intent(in) :: n real(kind=dp) :: res integer :: slot slot = self%get_slot(n) res = self%buffer(slot)%energy end function conv_data_get_energy !> @brief Compute slot index from iteration number !> @param[in] n Iteration number (1 = oldest, -1 = latest) !> @return Slot index in the ring buffer function conv_data_get_slot(self, n) result(slot) class(converger_data), intent(in) :: self integer, intent(in) :: n integer :: slot, num_saved num_saved = self%num_saved if (n == -1) then slot = self%slot else slot = modulo(self%slot - num_saved + n - 1, self%num_slots) + 1 end if end function !============================================================================== ! scf_conv_result Methods !============================================================================== !> @brief Form the new Fock matrix !> @detail Placeholder for derived types to override. Does nothing by default. subroutine conv_result_dummy_get_fock(self, matrix, istat) class(scf_conv_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) istat = 0 end subroutine conv_result_dummy_get_fock !> @brief Form the new density matrix !> @detail Placeholder for derived types to override. Does nothing by default. subroutine conv_result_dummy_get_density(self, matrix, istat) class(scf_conv_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) istat = 0 end subroutine conv_result_dummy_get_density subroutine conv_result_dummy_get_mo_a(self, matrix, istat) class(scf_conv_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) istat = 0 end subroutine conv_result_dummy_get_mo_a subroutine conv_result_dummy_get_mo_b(self, matrix, istat) class(scf_conv_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) istat = 0 end subroutine conv_result_dummy_get_mo_b subroutine conv_result_dummy_get_mo_e_a(self, vector, istat) class(scf_conv_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: vector(:) istat = 0 end subroutine conv_result_dummy_get_mo_e_a subroutine conv_result_dummy_get_mo_e_b(self, vector, istat) class(scf_conv_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: vector(:) istat = 0 end subroutine conv_result_dummy_get_mo_e_b function conv_result_dummy_get_rms_g(self) result(istat) class(scf_conv_result), intent(in) :: self real(kind=dp) :: istat istat = 0 end function conv_result_dummy_get_rms_g function conv_result_dummy_get_iter(self) result(istat) class(scf_conv_result), intent(in) :: self real(kind=dp) :: istat istat = 0 end function conv_result_dummy_get_iter function conv_result_dummy_get_rms_dp(self) result(istat) class(scf_conv_result), intent(in) :: self real(kind=dp) :: istat istat = 0 end function conv_result_dummy_get_rms_dp function conv_result_dummy_get_etot(self) result(istat) class(scf_conv_result), intent(in) :: self real(kind=dp) :: istat istat = 0 end function conv_result_dummy_get_etot !> @brief Form the interpolated Fock matrix !> @detail F_n = \sum_{i=start}^{end} F_i * c_i subroutine conv_result_interp_get_fock(self, matrix, istat) class(scf_conv_interp_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr == 0) then call self%interpolate(matrix, 'fock', istat) else istat = self%ierr end if end subroutine conv_result_interp_get_fock !> @brief Get error value from result !> @return Current error value !> @brief Get the current convergence error !> @return Current error value for the active converger function conv_result_get_error(self) result(err) class(scf_conv_result), intent(in) :: self real(kind=dp) :: err err = self%error end function conv_result_get_error !============================================================================== ! DIIS Result Methods !============================================================================== !> @brief Form the interpolated density matrix !> @detail D_n = \sum_{i=start}^{end} D_i * c_i subroutine conv_result_interp_get_density(self, matrix, istat) class(scf_conv_interp_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr == 0) then call self%interpolate(matrix, 'density', istat) else istat = self%ierr end if end subroutine conv_result_interp_get_density !> @brief Form the interpolated matrix (Fock or density) !> @param[inout] matrix Output matrix (Fock or density) !> @param[in] datatype 'fock' or 'density' to specify which matrix to interpolate !> @param[out] istat Status code (0 = success) subroutine conv_result_interpolate(self, matrix, datatype, istat) class(scf_conv_interp_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) character(len=*), intent(in) :: datatype integer :: i, ifock if (self%ierr /= 0) then istat = self%ierr return end if matrix = 0.0_dp do ifock = 1, self%dat%num_focks do i = self%pstart, self%pend select case (datatype) case ('fock') matrix(:, ifock) = matrix(:, ifock) + self%coeffs(i) * self%dat%get_fock(i, ifock) case ('density') matrix(:, ifock) = matrix(:, ifock) + self%coeffs(i) * self%dat%get_density(i, ifock) end select end do end do istat = 0 end subroutine conv_result_interpolate !============================================================================== ! SOSCF Result Methods !============================================================================== !> @brief Get alpha MO coefficients from SOSCF result subroutine conv_result_soscf_get_mo_a(self, matrix, istat) class(scf_conv_soscf_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr /= 0) then istat = self%ierr return end if matrix = self%dat%buffer(self%dat%slot)%mo_a istat = 0 end subroutine conv_result_soscf_get_mo_a !> @brief Get beta MO coefficients from SOSCF result subroutine conv_result_soscf_get_mo_b(self, matrix, istat) class(scf_conv_soscf_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr /= 0) then istat = self%ierr return end if matrix = self%dat%buffer(self%dat%slot)%mo_b istat = 0 end subroutine conv_result_soscf_get_mo_b !> @brief Get alpha orbital energies from SOSCF result subroutine conv_result_soscf_get_mo_e_a(self, vector, istat) class(scf_conv_soscf_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: vector(:) if (self%ierr /= 0) then istat = self%ierr return end if vector = self%dat%buffer(self%dat%slot)%mo_e_a istat = 0 end subroutine conv_result_soscf_get_mo_e_a !> @brief Get beta orbital energies from SOSCF result subroutine conv_result_soscf_get_mo_e_b(self, vector, istat) class(scf_conv_soscf_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: vector(:) if (self%ierr /= 0) then istat = self%ierr return end if vector = self%dat%buffer(self%dat%slot)%mo_e_b istat = 0 end subroutine conv_result_soscf_get_mo_e_b function conv_result_soscf_get_rms_g(self) result(rms) class(scf_conv_soscf_result), intent(in) :: self real(kind=dp) :: rms rms = self%rms_grad end function conv_result_soscf_get_rms_g function conv_result_soscf_get_rms_dp(self) result(rms) class(scf_conv_soscf_result), intent(in) :: self real(kind=dp) :: rms rms = self%rms_dp end function conv_result_soscf_get_rms_dp !============================================================================== ! TRAH Result Methods !============================================================================== !> @brief Get alpha MO coefficients from TRAH result subroutine conv_result_trah_get_mo_a(self, matrix, istat) class(scf_conv_trah_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr /= 0) then istat = self%ierr return end if matrix = self%dat%buffer(self%dat%slot)%mo_a istat = 0 end subroutine conv_result_trah_get_mo_a !> @brief Get beta MO coefficients from TRAH result subroutine conv_result_trah_get_mo_b(self, matrix, istat) class(scf_conv_trah_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr /= 0) then istat = self%ierr return end if matrix = self%dat%buffer(self%dat%slot)%mo_b istat = 0 end subroutine conv_result_trah_get_mo_b subroutine conv_result_trah_get_mo_e_a(self, vector, istat) class(scf_conv_trah_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: vector(:) if (self%ierr /= 0) then istat = self%ierr return end if vector = self%dat%buffer(self%dat%slot)%mo_e_a istat = 0 end subroutine conv_result_trah_get_mo_e_a subroutine conv_result_trah_get_mo_e_b(self, vector, istat) class(scf_conv_trah_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: vector(:) if (self%ierr /= 0) then istat = self%ierr return end if vector = self%dat%buffer(self%dat%slot)%mo_e_b istat = 0 end subroutine conv_result_trah_get_mo_e_b subroutine conv_result_trah_get_fock(self, matrix, istat) class(scf_conv_trah_result), intent(in) :: self integer, intent(out) :: istat real(kind=dp), intent(inout) :: matrix(:,:) if (self%ierr /= 0) then istat = self%ierr return end if matrix = self%dat%buffer(self%dat%slot)%focks istat = 0 end subroutine conv_result_trah_get_fock function conv_result_trah_get_rms_g(self) result(rms) class(scf_conv_trah_result), intent(in) :: self real(kind=dp) :: rms rms = self%rms_grad end function conv_result_trah_get_rms_g function conv_result_trah_get_iter(self) result(rms) class(scf_conv_trah_result), intent(in) :: self real(kind=dp) :: rms rms = self%iter end function conv_result_trah_get_iter !============================================================================== ! scf_conv Methods !============================================================================== !> @brief Finalize scf_conv datatype subroutine scf_conv_clean(self) class(scf_conv), intent(inout) :: self self%verbose = 0 self%step = 0 self%state = conv_state_not_initialized call self%dat%clean() if (allocated(self%thresholds)) deallocate(self%thresholds) if (allocated(self%sconv)) deallocate(self%sconv) nullify(self%overlap) nullify(self%overlap_sqrt) end subroutine scf_conv_clean !> @brief Initializes the SCF converger driver !> @param[in] ldim Number of orbitals !> @param[in] maxvec Size of SCF converger linear space (e.g., number of DIIS vectors) !> @param[in] subconvergers Array of SCF subconverger codes !> @param[in] thresholds Thresholds to initiate subconvergers, where !> subconvergers[i] runs when current error is less than thresholds[i] !> @param[in] overlap Overlap matrix (S) in full format !> @param[in] overlap_sqrt S^(1/2) matrix in full format !> @param[in] num_focks 1 if R/ROHF, 2 if UHF !> @param[in] verbose Verbosity level subroutine scf_conv_init(self, ldim, nelec_a, nelec_b, maxvec, subconvergers, thresholds, & overlap, overlap_sqrt, num_focks, scf_type ,verbose, sd_scf) class(scf_conv), intent(inout) :: self integer, intent(in) :: ldim integer, optional, intent(in) :: nelec_a integer, optional, intent(in) :: nelec_b integer, optional, intent(in) :: maxvec integer, optional, intent(in) :: subconvergers(:) real(kind=dp), optional, intent(in) :: thresholds(:) real(kind=dp), optional, target, intent(in) :: overlap(:,:), overlap_sqrt(:,:) integer, optional, intent(in) :: num_focks integer, optional, intent(in) :: verbose integer :: nfocks, istat, i integer, optional, intent(in) :: scf_type logical(c_bool), optional, intent(in) :: sd_scf if (self%state /= conv_state_not_initialized) call self%clean() if (present(thresholds)) then allocate(self%thresholds(0:ubound(thresholds,1))) self%thresholds(0:) = [thresholds, 0.0_dp] else allocate(self%thresholds(0:1)) self%thresholds(0:) = [1.0_dp, 0.0_dp] end if self%overlap => null() if (present(overlap)) self%overlap => overlap self%overlap_sqrt => null() if (present(overlap_sqrt)) self%overlap_sqrt => overlap_sqrt nfocks = 1 if (present(num_focks)) nfocks = num_focks if (present(scf_type)) self%scf_type = scf_type self%verbose = 0 if (present(verbose)) self%verbose = verbose self%iter_space_size = 15 if (present(maxvec)) self%iter_space_size = maxvec self%step = 0 if (present(nelec_a) .and. present(nelec_b)) then call self%dat%init(ldim, nfocks, self%iter_space_size, istat, nelec_a, nelec_b) else call self%dat%init(ldim, nfocks, self%iter_space_size, istat) end if if (istat /= 0) then self%state = conv_state_not_initialized return end if if (present(sd_scf)) then if(.not. sd_scf) self%step = self%step + 1 end if if (present(subconvergers)) then allocate(self%sconv(0:ubound(subconvergers,1))) allocate(noconv_converger :: self%sconv(0)%s) call self%sconv(0)%s%init(self) do i = 1, ubound(subconvergers,1) select case (subconvergers(i)) case (conv_none) allocate(noconv_converger :: self%sconv(i)%s) case (conv_cdiis) allocate(cdiis_converger :: self%sconv(i)%s) case (conv_ediis) allocate(ediis_converger :: self%sconv(i)%s) case (conv_adiis) allocate(adiis_converger :: self%sconv(i)%s) case (conv_soscf) allocate(soscf_converger :: self%sconv(i)%s) case (conv_trah) allocate(trah_converger :: self%sconv(i)%s) end select call self%sconv(i)%s%init(self) end do end if self%state = conv_state_initialized end subroutine scf_conv_init !> @brief Store data from the new SCF iteration !> @param[in] f Fock matrix/matrices !> @param[in] dens Density matrix/matrices !> @param[in] e SCF energy (optional) subroutine scf_conv_add_data(self, f, dens, e, mo_a, mo_b, mo_e_a, mo_e_b, & pfon) class(scf_conv), intent(inout) :: self real(kind=dp), intent(in), optional :: f(:,:) ! Fock matrices real(kind=dp), intent(in), optional :: dens(:,:) ! Density matrices real(kind=dp), intent(in), optional :: e ! SCF energy real(kind=dp), intent(in), optional :: mo_a(:,:) ! Alpha MO coefficients real(kind=dp), intent(in), optional :: mo_b(:,:) ! Beta MO coefficients real(kind=dp), intent(in), optional :: mo_e_a(:) ! Alpha MO energies real(kind=dp), intent(in), optional :: mo_e_b(:) ! Beta MO energies type(pfon_t), pointer, intent(in), optional :: pfon ! Pseudo-Fractional Occupation Number (pFON) object integer :: i call self%dat%next_slot() ! Save the current Fock and density matrices if (present(f)) call self%dat%put(fock=f) if (present(dens)) call self%dat%put(dens=dens) if (present(e)) call self%dat%put(energy=e) if (present(mo_a)) call self%dat%put(mo_a=mo_a) if (present(mo_b)) call self%dat%put(mo_b=mo_b) if (present(mo_e_a)) call self%dat%put(mo_e_a=mo_e_a) if (present(mo_e_b)) call self%dat%put(mo_e_b=mo_e_b) if (present(pfon)) call self%dat%put(pfon_obj=pfon) ! Compute the current error self%current_error = self%compute_error() ! Update subconverger states do i = lbound(self%sconv, 1), ubound(self%sconv, 1) self%sconv(i)%s%last_setup = self%sconv(i)%s%last_setup + 1 end do end subroutine scf_conv_add_data !> @brief Computes the new guess to the SCF wavefunction !> @param[out] conv_result Results of the calculation subroutine scf_conv_run(self, conv_result) class(scf_conv), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: conv_result class(subconverger), pointer :: conv class(scf_conv_result), allocatable :: tmp_result conv => self%select_method(self%current_error) if (self%state == conv_state_not_initialized) then conv_result = scf_conv_result(error=self%current_error) return end if conv%iter = conv%iter + 1 self%step = self%step + 1 call conv%setup() ! Nothing left to do on the first iteration, exit if (self%step == 1) then conv_result = scf_conv_result( & ierr=0, & active_converger_name='SD', & error=self%current_error) return end if ! Solve the set of DIIS linear equations call conv%run(tmp_result) call move_alloc(from=tmp_result, to=conv_result) end subroutine scf_conv_run !> @brief Select subconverger basing on the current DIIS error value !> @param[in] error DIIS error value !> @return pointer to selected converger function scf_conv_select(self, error) result(conv) implicit none class(scf_conv), target :: self real(kind=dp), intent(in) :: error class(subconverger), pointer :: conv integer :: i, nconv nconv = ubound(self%thresholds, 1) do i = 0, nconv if (error > self%thresholds(i)) exit end do conv => self%sconv(min(i,nconv))%s ! Continue using the 'SD' converger if ! already initiated if (i == 0 .and. self%step > 0) then conv => self%sconv(1)%s end if ! Use SD by default if no other converger selected if (.not.associated(conv)) then conv => self%sconv(0)%s end if end function !> @brief Calculate the DIIS error matrix: \f$ \mathrm{Err} = FDS - SDF \f$ !> @details This routine is general for RHF, ROHF, and UHF. !> Since each of `F`, `D`, `S` are symmetric, this means calculate \f$ FDS \f$, !> and then subtract the transpose from that result. !> Before entry, `F`, `D` and `S` must be expanded to square storage. !> @return DIIS error value (infinity norm across all matrices) function scf_conv_compute_error(self) result(diis_error) use mathlib, only: antisymmetrize_matrix, unpack_matrix, pack_matrix use oqp_linalg class(scf_conv), target, intent(inout) :: self real(kind=dp) :: diis_error real(kind=dp), pointer :: f(:), d(:), err(:) ! all are (nbf, nbf) square matrices real(kind=dp), allocatable :: fock_full(:,:), dens_full(:,:), err_full(:,:), wrk(:,:) integer :: nbf, ifock, nfocks, slot nfocks = self%dat%num_focks nbf = self%dat%ldim slot = self%dat%slot allocate(fock_full(nbf, nbf), & dens_full(nbf, nbf), & err_full(nbf, nbf), & wrk(nbf, nbf)) diis_error = 0.0_dp do ifock = 1, nfocks f => self%dat%get_fock(-1, ifock) d => self%dat%get_density(-1, ifock) call unpack_matrix(f, fock_full, nbf, 'u') call unpack_matrix(d, dens_full, nbf, 'u') ! F*D call dsymm('l', 'u', nbf, nbf, 1.0_dp, fock_full, nbf, dens_full, nbf, 0.0_dp, wrk, nbf) ! (F*D)*S call dgemm('n', 'n', nbf, nbf, nbf, 1.0_dp, wrk, nbf, self%overlap, nbf, 0.0_dp, err_full, nbf) ! F*D*S - S*D*F call antisymmetrize_matrix(err_full, nbf) ! MV: This step is not really necessary ! Put error matrix into consistent orthonormal basis ! Pulay uses S**-1/2, but here we use Q, Q obeys Q-dagger*S*Q=I ! E-orth = Q-dagger * E * Q, FCKA is used as a scratch `nbf` vector. call dgemm('t', 'n', nbf, nbf, nbf, 1.0_dp, self%overlap_sqrt, nbf, err_full, nbf, 0.0_dp, wrk, nbf) call dgemm('n', 'n', nbf, nbf, nbf, 1.0_dp, wrk, nbf, self%overlap_sqrt, nbf, 0.0_dp, err_full, nbf) err => self%dat%buffer(slot)%errs(:, ifock) call pack_matrix(err_full, nbf, err, 'u') ! Compute DIIS error (infinity norm of error matrix) diis_error = diis_error + maxval(abs(err)) end do deallocate(fock_full, dens_full, err_full, wrk) end function scf_conv_compute_error !============================================================================== ! Subconverger Methods !============================================================================== !> @brief Initialize subconverger !> @detail This subroutine takes SCF converger driver as argument. !> It should be initialized and include all the required parameters. !> @param[in] params Current SCF converger driver subroutine subconverger_init(self, params) class(subconverger), intent(inout) :: self type(scf_conv), target, intent(in) :: params self%iter = 0 self%last_setup = 1024 self%dat => params%dat end subroutine subconverger_init !> @brief Finalize subconverger subroutine subconverger_clean(self) class(subconverger), intent(inout) :: self self%iter = 0 self%last_setup = 1024 self%dat => null() end subroutine subconverger_clean !============================================================================== ! noconv_converger Methods !============================================================================== !> @brief Initialize SD subconverger !> @param[in] params current SCF converger driver subroutine noconv_init(self, params) class(noconv_converger), intent(inout) :: self type(scf_conv), target, intent(in) :: params if (self%iter > 0) call self%clean() call self%subconverger_init(params) self%conv_name = 'SD' end subroutine noconv_init !> @brief Computes the new guess to the SCF wavefunction !> @param[out] res results of the calculation subroutine noconv_run(self, res) class(noconv_converger), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: res real(kind=dp) :: diis_error integer :: ifock allocate(scf_conv_result :: res) res = scf_conv_result(ierr=0, active_converger_name='SD', dat=self%dat) ! Compute error diis_error = 0.0_dp do ifock = 1, self%dat%num_focks diis_error = max(diis_error, maxval(abs(self%dat%get_err(-1, ifock)))) end do res%error = diis_error end subroutine noconv_run !> @brief Prepare subconverger to run subroutine noconv_setup(self) class(noconv_converger), intent(inout) :: self self%last_setup = 0 end subroutine noconv_setup !============================================================================== ! cdiis_converger Methods !============================================================================== !> @brief Initialize C-DIIS subconverger !> @detail This subroutine takes SCF converger driver as argument. !> It should be initialized and include all the required parameters. !> @param[in] params Current SCF converger driver subroutine cdiis_init(self, params) class(cdiis_converger), intent(inout) :: self type(scf_conv), target, intent(in) :: params if (self%iter > 0) call self%clean() call self%subconverger_init(params) self%conv_name = 'C-DIIS' self%verbose = params%verbose self%maxdiis = params%iter_space_size allocate(self%a(self%maxdiis, self%maxdiis), source=0.0_dp) end subroutine cdiis_init !> @brief Finalize C-DIIS subconverger subroutine cdiis_clean(self) class(cdiis_converger), intent(inout) :: self call self%subconverger_clean() self%verbose = 0 if (allocated(self%a)) deallocate(self%a) end subroutine cdiis_clean !> @brief Computes the new guess to the SCF wavefunction using C-DIIS !> @param[out] res Results of the calculation subroutine cdiis_run(self, res) use mathlib, only: solve_linear_equations use io_constants, only: iw class(cdiis_converger), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: res ! integer :: i, na, cur, info, ifock ! real(kind=dp) :: a_loc(self%maxdiis+1, self%maxdiis+1) integer :: i, na, cur, info, ifock, trial, k real(kind=dp) :: a_loc(self%maxdiis+1, self%maxdiis+1), a_sys(self%maxdiis+1, self%maxdiis+1) real(kind=dp) :: delta real(kind=dp) :: x_loc(self%maxdiis+1) real(kind=dp), allocatable :: x(:) real(kind=dp), pointer :: err(:) real(kind=dp) :: diis_error allocate(scf_conv_interp_result :: res) res%dat => self%dat res%active_converger_name = self%conv_name select type (res) class is (scf_conv_interp_result) res%pstart = 1 res%pend = self%dat%num_saved end select res%ierr = 3 ! Need to set up DIIS equations first if (self%last_setup /= 0) return na = self%dat%num_saved ! Solve the set of DIIS linear equations ! Add mild Tikhonov regularization before shrinking the subspace a_loc(:self%maxdiis, :self%maxdiis) = self%a a_loc(1:na, na+1) = -1.0_dp a_loc(na+1, 1:na ) = -1.0_dp a_loc(na+1, na+1) = 0.0_dp do i = na, 1, -1 ! Helper index, needed for dimension reduction in case of instability cur = na - i + 1 x_loc = 0.0_dp x_loc(na+1) = -1.0_dp info = 0 a_sys = a_loc call solve_linear_equations(a_sys(cur:, cur:), x_loc(cur:), i+1, 1, self%maxdiis+1, info) if (info > 0) then do trial = 1, 3 delta = 1.0e-12_dp * (10.0_dp ** (trial-1)) a_sys = a_loc do k = 0, i-1 a_sys(cur+k, cur+k) = a_sys(cur+k, cur+k) + delta end do call solve_linear_equations(a_sys(cur:, cur:), x_loc(cur:), i+1, 1, self%maxdiis+1, info) if (info == 0) exit end do end if if (info <= 0) exit write(iw, *) 'Reducing DIIS Equation size by 1 for numerical stability' end do ! if (info < 0) then res%ierr = 2 ! Illegal value in DSYSV else if (info > 0) then res%ierr = 1 ! Singular DIIS matrix else res%ierr = 0 ! normal exit x = x_loc(1:self%maxdiis) select type (res) class is (scf_conv_interp_result) call move_alloc(from=x, to=res%coeffs) end select end if ! Compute DIIS error from the latest iteration diis_error = 0.0_dp do ifock = 1, self%dat%num_focks err => self%dat%get_err(-1, ifock) diis_error = max(diis_error, maxval(abs(err))) end do res%error = diis_error end subroutine cdiis_run !> @brief Prepare C-DIIS subconverger to run subroutine cdiis_setup(self) class(cdiis_converger), intent(inout) :: self integer :: i, j, na, maxdiis, ifock, nfocks real(kind=dp) :: factor maxdiis = self%maxdiis na = self%dat%num_saved nfocks = self%dat%num_focks ! Factor to account RHF/UHF cases factor = 1.0_dp / nfocks if (self%last_setup > 1) then ! DIIS matrix is rather old, generate it from scratch self%old_dim = na self%a = 0.0_dp do ifock = 1, nfocks do i = 1, na do j = 1, i self%a(j,i) = self%a(j,i) + factor * dot_product( & self%dat%get_err(j, ifock), & self%dat%get_err(i, ifock)) end do end do end do else if (self%last_setup == 1) then ! DIIS matrix is old by 1 iteration, just update it ! If the current number of iterations exceeds the dimension of A matrix: ! discard the data of oldest iteration by shifting the bottom-rigth square ! to the top-left corner if (self%old_dim >= maxdiis) then self%a(1:maxdiis-1, 1:maxdiis-1) = self%a(2:maxdiis, 2:maxdiis) end if self%old_dim = na ! Compute new elements (`na`-th column) self%a(:, na) = 0.0_dp do ifock = 1, nfocks do i = 1, na self%a(i, na) = self%a(i, na) + factor * dot_product( & self%dat%get_err(na, ifock), & self%dat%get_err(i, ifock)) end do end do end if ! DIIS matrix is already prepared nothing to do here self%last_setup = 0 end subroutine cdiis_setup !============================================================================== ! ediis_converger Methods !============================================================================== !> @brief Initialize E-DIIS subconverger !> @detail This subroutine takes SCF converger driver as argument. !> It should be initialized and include all the required parameters. !> @param[in] params current SCF converger driver subroutine ediis_init(self, params) class(ediis_converger), intent(inout) :: self type(scf_conv), target, intent(in) :: params call self%cdiis_converger%init(params) self%conv_name = 'E-DIIS' self%fun => ediis_fun allocate(self%b(self%maxdiis)) allocate(self%xlog(self%maxdiis, self%maxdiis), source=0.0_dp) end subroutine ediis_init !> @brief Finalize E-DIIS subconverger subroutine ediis_clean(self) class(ediis_converger), intent(inout) :: self call self%cdiis_converger%clean() if (allocated(self%b)) deallocate(self%b) if (allocated(self%xlog)) deallocate(self%xlog) end subroutine ediis_clean !> @brief Prepare E-DIIS subconverger to run subroutine ediis_setup(self) class(ediis_converger), intent(inout) :: self integer :: i, j, na, maxdiis, ifock, nfocks real(kind=dp) :: factor maxdiis = self%maxdiis na = self%dat%num_saved nfocks = self%dat%num_focks ! Factor to account RHF/UHF cases factor = 1.0_dp / nfocks if (self%last_setup > 1) then ! DIIS matrix is rather old, generate it from scratch self%old_dim = na self%a = 0.0_dp do ifock = 1, nfocks do i = 1, na do j = 1, i-1 self%a(j,i) = self%a(j,i) + factor * dot_product( & self%dat%get_density(j, ifock) - self%dat%get_density(i, ifock), & self%dat%get_fock(j, ifock) - self%dat%get_fock(i, ifock)) self%a(i,j) = self%a(j,i) end do end do end do else if (self%last_setup == 1) then ! DIIS matrix is old by 1 iteration, just update it ! If the current number of iterations exceeds the dimension of A matrix: ! discard the data of oldest iteration by shifting the bottom-rigth square ! to the top-left corner if (self%old_dim >= maxdiis) then self%a(1:maxdiis-1, 1:maxdiis-1) = self%a(2:maxdiis, 2:maxdiis) end if self%old_dim = na ! Compute new elements (`na`-th column) self%a(:, na) = 0.0_dp do ifock = 1, nfocks do j = 1, na self%a(j, na) = self%a(j, na) + factor * dot_product( & self%dat%get_density(j, ifock) - self%dat%get_density(na, ifock), & self%dat%get_fock(j, ifock) - self%dat%get_fock(na, ifock)) end do end do self%a(na, 1:na-1) = self%a(1:na-1, na) end if do i = 1, na self%b(i) = self%dat%get_energy(i) end do ! DIIS matrix is already prepared nothing to do here self%last_setup = 0 end subroutine ediis_setup !> @brief Computes the new guess to the SCF wavefunction using E-DIIS !> @param[out] res results of the calculation subroutine ediis_run(self, res) use io_constants, only: iw use nlopt class(ediis_converger), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: res real(kind=dp), parameter :: tol = 1.0e-5_dp real(kind=dp), parameter :: constrtol = 1.0e-8_dp real(kind=dp) :: minf, minf_min real(kind=dp), allocatable :: x(:), xmin(:) real(kind=dp), pointer :: err(:) real(kind=dp) :: diis_error integer(kind=4) :: ires integer :: na, i, j, ifock logical :: is_a_repeat ! NLOpt's f77-style API stores a C POINTER in the opt handle argument ! (NLOpt documents it as integer*8), so the handles must be 8 bytes ! REGARDLESS of the build's default integer width. With a default-width ! declaration, LP64 builds (4-byte default integer, e.g. native macOS ! Accelerate) let nlo_create() write an 8-byte pointer into 4-byte storage ! => stack corruption => SIGSEGV (exit -11) in every EDIIS/ADIIS SCF. integer(kind=8) :: opt_global, opt_lbfgs type(ediis_opt_data) :: t allocate(scf_conv_interp_result :: res) res%dat => self%dat res%active_converger_name = self%conv_name select type (res) class is (scf_conv_interp_result) res%pstart = 1 res%pend = self%dat%num_saved end select res%ierr = 3 ! Need to set up DIIS equations first if (self%last_setup /= 0) return na = self%dat%num_saved if (self%iter > self%maxdiis) then do i = 1, self%maxdiis-1 self%xlog(:, i) = cshift(self%xlog(:, i+1), 1) self%xlog(i+1:, i) = 0.0_dp end do end if allocate(x(na), xmin(na)) opt_global = 0 minf_min = huge(1.0_dp) ! Initialize E-DIIS equation parameters for NLOpt t = ediis_opt_data(A=self%a(1:na, 1:na), b=self%b(1:na), fun=self%fun) ! Set up the Improved Stochastic Ranking Evolution Strategy ! It will run coarse global optimization, which will be further refined via L-BFGS call nlo_create(opt_global, NLOPT_GN_ISRES, na) ! Max. number of calls to the objective function call nlo_set_maxeval(ires, opt_global, 100*(na+1)) ! Relative convergence tolerance for arguments call nlo_set_xtol_rel(ires, opt_global, 1.0e-2_dp) ! Absolute convergence tolerance for function value call nlo_set_ftol_abs(ires, opt_global, 1.0e-4_dp) ! Relative convergence tolerance for function value call nlo_set_ftol_rel(ires, opt_global, 1.0e-4_dp) ! Sum of coeffs equal to 1 call nlo_add_equality_constraint(ires, opt_global, eadiis_constraints, 0, constrtol) ! 0 <= c_i <= 1 call nlo_set_lower_bounds1(ires, opt_global, 0.0_dp) call nlo_set_upper_bounds1(ires, opt_global, 1.0_dp) ! Objective function call nlo_set_min_objective(ires, opt_global, eadiis_fun, t) x = 1.0_dp / na call nlo_optimize(ires, opt_global, x(:na), minf) call nlo_destroy(opt_global) ! Refine the results of global optimization via L-BFGS opt_lbfgs = 0 call nlo_create(opt_lbfgs, NLOPT_LD_LBFGS, na) call nlo_set_xtol_rel(ires, opt_lbfgs, tol) call nlo_set_ftol_abs(ires, opt_lbfgs, tol*tol) call nlo_set_ftol_rel(ires, opt_lbfgs, tol*tol) ! Here, the modified E-DIIS equations are used, because L-BFGS does not support ! equality constraints ! They utilize the following variable substitution: ! c_i = t_i^2/\sum_i{t_i^2} call nlo_set_min_objective(ires, opt_lbfgs, eadiis_objective, t) call nlo_optimize(ires, opt_lbfgs, x(:na), minf) ! Because we used modified E-DIIS equations, we need to compute ! coefficients `c` from `t`: x = x**2 / sum(x**2) ! Get prediction of the new SCF energy call eadiis_fun(minf, int(na, 4), x, x, int(0,4), t) if (ires < 0) then if (self%verbose > 2) write(iw, '(10X,"*** nlopt0 failed:",I4," ***")') ires elseif (minf < minf_min) then is_a_repeat = any([(norm2(self%xlog(:na, j) - x(:na)) < 1.0e-4_dp, & j = 1, min(self%iter, self%maxdiis))]) .or. & any(1.0_dp - x(1:na-1) < 1.0e-4_dp) if (.not. is_a_repeat) then minf_min = minf xmin = x if (self%verbose > 2) then write(iw, '(A,*(F15.6))') 'nlopt0: improving x at ', xmin(:) write(iw, '(A,*(F15.6))') 'nlopt0: improved val = ', minf end if elseif (self%verbose > 2) then write(iw, '(A,*(F15.6))') 'nlopt0: found rep at ', x(:) write(iw, '(A,*(F15.6))') 'nlopt0: rep val = ', minf end if else if (self%verbose > 2) then write(iw, '(A,*(F15.6))') 'nlopt0: found min at ', x(:) write(iw, '(A,*(F15.6))') 'nlopt0: min val = ', minf end if end if ! If no solution found, try the alternative: ! Start from the trivial guess [x(1:n-1)=0, x(n) = 1] ! then run two L-BFGS iterations and average with ! [x(1:i-i), x(i+1:n) = 0, x(i) = 1] vector and run few L-BFGS steps again ! for all [ i = n-1, 1 ] and then [i = 1, n] if (minf_min > 1.0e99_dp) then call nlo_set_maxeval(ires, opt_lbfgs, 2) call nlo_set_xtol_rel(ires, opt_lbfgs, 0.1_dp) xmin = 0.0_dp xmin(na) = 1.0_dp do i = na-1, 1, -1 x = 0.0_dp x(i) = 1.0_dp xmin = (xmin + x) / (1 + sum(x)) call nlo_optimize(ires, opt_lbfgs, xmin, minf) if (ires < 0) then if (self%verbose > 2) then write(iw, '(10X,"*** nlopt2 failed:",I4," ***")') ires end if exit end if xmin = xmin**2 / sum(xmin**2) end do if (ires >= 0) then do i = 1, na x = 0.0_dp x(i) = 1.0_dp xmin = (xmin + x) / (1 + sum(x)) call nlo_optimize(ires, opt_lbfgs, xmin, minf) if (ires < 0) then if (self%verbose > 2) then write(iw, '(10X,"*** nlopt2 failed:",I4," ***")') ires end if exit end if xmin = xmin**2 / sum(xmin**2) end do end if ! If still no success, the default is minimum energy + small contribution from others: if (ires < 0) then xmin = 1.0_dp xmin(minloc(self%b(1:na-1))) = 10.0_dp xmin = xmin / sum(xmin) if (self%verbose > 2) write(iw, *) 'nlopt2: unoptimal default' end if call eadiis_fun(minf_min, int(na, 4), xmin, xmin, int(0,4), t) end if minf = minf_min self%xlog(:na, min(self%iter, self%maxdiis)) = xmin(1:na) call nlo_destroy(opt_lbfgs) res%ierr = 0 select type (res) class is (scf_conv_interp_result) call move_alloc(from=xmin, to=res%coeffs) end select ! compute diis error from the latest iteration diis_error = 0.0_dp do ifock = 1, self%dat%num_focks err => self%dat%get_err(-1, ifock) diis_error = max(diis_error, maxval(abs(err))) end do res%error = diis_error end subroutine ediis_run !============================================================================== ! adiis_converger Methods !============================================================================== !> @brief Initialize A-DIIS subconverger !> @detail This subroutine takes SCF converger driver as argument. !> It should be initialized and include all the required parameters. !> @param[in] params current SCF converger driver subroutine adiis_init(self, params) class(adiis_converger), intent(inout) :: self type(scf_conv), target, intent(in) :: params call self%cdiis_converger%init(params) self%conv_name = 'A-DIIS' self%fun => adiis_fun allocate(self%b(self%maxdiis)) allocate(self%xlog(self%maxdiis, self%maxdiis), source=0.0_dp) end subroutine adiis_init !> @brief Prepare A-DIIS subconverger to run subroutine adiis_setup(self) class(adiis_converger), intent(inout) :: self integer :: i, j, na, maxdiis, ifock, nfocks real(kind=dp) :: factor maxdiis = self%maxdiis na = self%dat%num_saved nfocks = self%dat%num_focks ! Factor to account RHF/UHF cases factor = 1.0_dp / nfocks self%old_dim = na self%a = 0.0_dp self%b = 0.0_dp do ifock = 1, nfocks do i = 1, na do j = 1, na self%a(j,i) = self%a(j,i) + 0.5_dp * factor * dot_product( & self%dat%get_density(j, ifock) - self%dat%get_density(-1, ifock), & self%dat%get_fock(j, ifock) - self%dat%get_fock(-1, ifock)) end do self%b(i) = self%b(i) + factor * dot_product( & self%dat%get_density(i, ifock) - self%dat%get_density(-1, ifock), & self%dat%get_fock(-1, ifock)) end do end do ! DIIS matrix is already prepared nothing to do here self%last_setup = 0 end subroutine adiis_setup !============================================================================== ! Optimization Helper Routines !============================================================================== !> @brief Modified E/A-DIIS objective function wrapper, which allows to use unconstrained optimization !> @details The following variable substitution is used: !> \f$ c_i = t_i^2 / \sum_i{t_i^2} \f$ !> @note This is standard interface to work with NLOpt library !> @param[out] val Function value !> @param[in] n Dimension of the problem !> @param[in] t Vector of arguments !> @param[out] grad Vector of function gradient !> @param[in] need_gradient Flag to turn on computing gradient, 0 - gradient not computed !> @param[in] d Datatype storing function parameters subroutine eadiis_objective(val, n, t, grad, need_gradient, d) integer(kind=4), intent(in) :: n, need_gradient real(kind=8) :: val, t(n), grad(n) type(ediis_opt_data), intent(in) :: d real(kind=8) :: x(n), tnorm, jac(n,n) integer :: i, j tnorm = 1.0_dp / sum(t(1:n)**2) x = (t(1:n)**2) * tnorm call d%fun(val, n, x, grad, need_gradient, d) if (need_gradient /= 0) then jac = 0.0_dp do i = 1, n jac(i,i) = 1.0_dp do j = 1, n jac(j,i) = 2.0_dp * tnorm * t(j) * (jac(j,i) - x(i)) end do end do grad(1:n) = matmul(jac, grad(1:n)) end if end subroutine eadiis_objective !> @brief Non-modified E/A-DIIS objective function wrapper !> @note This is standard interface to work with NLOpt library !> @param[out] val Function value !> @param[in] n Dimension of the problem !> @param[in] t Vector of arguments !> @param[out] grad Vector of function gradient !> @param[in] need_gradient Flag to turn on computing gradient, 0 - gradient not computed !> @param[in] d Datatype storing function parameters subroutine eadiis_fun(val, n, x, grad, need_gradient, d) integer(kind=4), intent(in) :: n, need_gradient real(kind=8) :: val, x(n), grad(n) type(ediis_opt_data), intent(in) :: d call d%fun(val, n, x, grad, need_gradient, d) end subroutine eadiis_fun !> @brief E-DIIS objective function calculation !> @note This is standard interface to work with NLOpt library !> @param[out] val Function value !> @param[in] n Dimension of the problem !> @param[in] t Vector of arguments !> @param[out] grad Vector of function gradient !> @param[in] need_gradient Flag to turn on computing gradient, 0 - gradient not computed !> @param[in] d Datatype storing function parameters subroutine ediis_fun(val, n, x, grad, need_gradient, d) integer(kind=4), intent(in) :: n, need_gradient real(kind=8) :: val, x(*), grad(*) type(ediis_opt_data), intent(in) :: d if (need_gradient /= 0) then grad(1:n) = d%b(1:n) - matmul(d%A(1:n, 1:n), x(1:n)) end if val = dot_product(x(1:n), d%b(1:n)) - & 0.5_dp * dot_product(x(1:n), matmul(d%A(1:n, 1:n), x(1:n))) end subroutine ediis_fun !> @brief E/A-DIIS constraints !> @note This is standard interface to work with NLOpt library !> @param[out] val Function value !> @param[in] n Dimension of the problem !> @param[in] t Vector of arguments !> @param[out] grad Vector of function gradient !> @param[in] need_gradient Flag to turn on computing gradient, 0 - gradient not computed !> @param[in] d Datatype storing function parameters subroutine eadiis_constraints(val, n, x, grad, need_gradient, d) integer(kind=4), intent(in) :: n, need_gradient real(kind=8) :: val, x(n), grad(n) class(ediis_converger), intent(in) :: d if (need_gradient /= 0) grad = 1.0_dp val = sum(x) - 1.0_dp end subroutine eadiis_constraints !> @brief A-DIIS objective function calculation !> @note This is standard interface to work with NLOpt library !> @param[out] val Function value !> @param[in] n Dimension of the problem !> @param[in] t Vector of arguments !> @param[out] grad Vector of function gradient !> @param[in] need_gradient Flag to turn on computing gradient, 0 - gradient not computed !> @param[in] d Datatype storing function parameters subroutine adiis_fun(val, n, x, grad, need_gradient, d) integer(kind=4), intent(in) :: n, need_gradient real(kind=8) :: val, x(*), grad(*) type(ediis_opt_data), intent(in) :: d if (need_gradient /= 0) then grad(1:n) = 2.0_dp * d%b(1:n) + matmul(d%A(1:n, 1:n), x(1:n)) + & matmul(x(1:n), d%A(1:n, 1:n)) end if val = d%b(n) + 2.0_dp * dot_product(x(1:n), d%b(1:n)) + & dot_product(x(1:n), matmul(d%A(1:n, 1:n), x(1:n))) end subroutine adiis_fun !============================================================================== ! Debug Printing (optional) !============================================================================== !> @brief Debug printing of the DIIS equation data subroutine diis_print_equation(self) use printing, only: print_square use io_constants, only: iw class(cdiis_converger), intent(in) :: self integer :: num_saved num_saved = self%dat%num_saved write(iw, '("(dbg) --------------------------------------------------")') write(iw, '("(dbg) DIIS iteration / max.dim. : ",G0," / ",G0)') self%iter, self%maxdiis write(iw, '("(dbg)",I4," Fock sets stored")') num_saved write(iw, '("(dbg) --------------------------------------------------")') write(iw, '("(dbg) Current DIIS equation matrix:")') call print_square(self%a, num_saved, num_saved, ubound(self%a,1), tag='(dbg)') write(iw, '("(dbg) --------------------------------------------------")') select type(self) class is (ediis_converger) write(iw, '("(dbg) Current DIIS equation vector:")') write(iw, '(*(ES15.7,","))') self%b(1:num_saved) end select end subroutine diis_print_equation !============================================================================== ! soscf_converger Methods !============================================================================== !> @brief Initialize the SOSCF converger !> @param[inout] self The SOSCF converger instance !> @param[in] params SCF convergence parameters from the driver subroutine soscf_init(self, params) class(soscf_converger), intent(inout) :: self type(scf_conv), target, intent(in) :: params integer :: nvec, m_max, istat, nvir_a, nvir_b ! Initialize base class call self%subconverger_init(params) self%conv_name = 'SOSCF' self%nfocks = params%dat%num_focks self%verbose = params%verbose self%nbf = params%dat%ldim self%nocc_a = params%dat%nelec_a self%nocc_b = params%dat%nelec_b self%nbf_tri = self%nbf * (self%nbf + 1) / 2 self%m_max = params%dat%num_slots self%m_history = 0 self%dat => params%dat self%overlap => params%overlap self%overlap_invsqrt => params%overlap_sqrt self%first_macro = .true. self%scf_type = params%scf_type nvir_a = self%nbf - self%nocc_a nvir_b = self%nbf - self%nocc_b ! Calculate gradient vector size (nvec) select case (self%scf_type) case (1) ! RHF self%nvec = self%nocc_a * nvir_a case (2) ! UHF self%nvec = self%nocc_a * nvir_a + self%nocc_b * nvir_b case (3) ! ROHF self%nvec = self%nocc_b * nvir_b + (self%nocc_a - self%nocc_b) * nvir_a end select ! Allocate working matrices istat = 0 if (.not. allocated(self%rho_history)) & allocate(self%rho_history(self%m_max), stat=istat, source=0.0_dp) if (.not. allocated(self%work_1)) & allocate(self%work_1(self%nbf, self%nbf), stat=istat, source=0.0_dp) if (.not. allocated(self%work_2)) & allocate(self%work_2(self%nbf, self%nbf), stat=istat, source=0.0_dp) if (.not. allocated(self%work_3)) & allocate(self%work_3(self%nbf, self%nbf), stat=istat, source=0.0_dp) ! Allocate L-BFGS history arrays if (.not. allocated(self%s_history)) & allocate(self%s_history(self%nvec, self%m_max), stat=istat, source=0.0_dp) if (.not. allocated(self%grad)) & allocate(self%grad(self%nvec), stat=istat, source=0.0_dp) if (.not. allocated(self%x)) & allocate(self%x(self%nvec), stat=istat, source=0.0_dp) if (.not. allocated(self%x_prev)) & allocate(self%x_prev(self%nvec), stat=istat, source=0.0_dp) if (.not. allocated(self%grad_prev)) & allocate(self%grad_prev(self%nvec), stat=istat, source=0.0_dp) if (.not. allocated(self%y_history)) & allocate(self%y_history(self%nvec, self%m_max), stat=istat, source=0.0_dp) if (.not. allocated(self%upd_history)) & allocate(self%upd_history(self%nvec, self%m_max), stat=istat, source=0.0_dp) if (.not. allocated(self%h_inv)) & allocate(self%h_inv(self%nvec), stat=istat, source=0.0_dp) ! Allocate SOSCF arrays if (.not. allocated(self%mo_a)) & allocate(self%mo_a(self%nbf, self%nbf), stat=istat, source=0.0_dp) if (.not. allocated(self%dens_a)) & allocate(self%dens_a(self%nbf_tri), stat=istat, source=0.0_dp) if (.not. allocated(self%dens_a_old)) & allocate(self%dens_a_old(self%nbf_tri), stat=istat, source=0.0_dp) if (self%scf_type > 1) then if (.not. allocated(self%mo_b)) & allocate(self%mo_b(self%nbf, self%nbf), stat=istat, source=0.0_dp) if (.not. allocated(self%dens_b)) & allocate(self%dens_b(self%nbf_tri), stat=istat, source=0.0_dp) if (.not. allocated(self%dens_b_old)) & allocate(self%dens_b_old(self%nbf_tri), stat=istat, source=0.0_dp) end if if (istat /= 0) then write(iw, '(A)') 'ERROR: Failed to allocate arrays in soscf_init' stop end if end subroutine soscf_init !> @brief Clean up SOSCF converger !> @param[inout] self The SOSCF converger instance subroutine soscf_clean(self) class(soscf_converger), intent(inout) :: self if (allocated(self%rho_history)) deallocate(self%rho_history) if (allocated(self%work_1)) deallocate(self%work_1) if (allocated(self%work_2)) deallocate(self%work_2) if (allocated(self%work_3)) deallocate(self%work_3) if (allocated(self%s_history)) deallocate(self%s_history) if (allocated(self%y_history)) deallocate(self%y_history) if (allocated(self%upd_history)) deallocate(self%upd_history) if (allocated(self%grad)) deallocate(self%grad) if (allocated(self%grad_prev)) deallocate(self%grad_prev) if (allocated(self%x_prev)) deallocate(self%x_prev) if (allocated(self%x)) deallocate(self%x) if (allocated(self%h_inv)) deallocate(self%h_inv) if (allocated(self%mo_a)) deallocate(self%mo_a) if (allocated(self%dens_a)) deallocate(self%dens_a) if (allocated(self%mo_b)) deallocate(self%mo_b) if (allocated(self%dens_b)) deallocate(self%dens_b) if (allocated(self%dens_a_old)) deallocate(self%dens_a_old) if (allocated(self%dens_b_old)) deallocate(self%dens_b_old) call self%subconverger_clean() end subroutine soscf_clean !> @brief Setup SOSCF for the current iteration !> @param[inout] self The SOSCF converger instance subroutine soscf_setup(self) class(soscf_converger), intent(inout) :: self ! Data verification on each setup call self%last_setup = 0 end subroutine soscf_setup !> @brief Run the SOSCF convergence step !> @param[inout] self The SOSCF converger instance !> @param[out] res Convergence result subroutine soscf_run(self, res) use mathlib, only: unpack_matrix use oqp_linalg class(soscf_converger), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: res ! Local variables type(pfon_t), pointer :: pfon => null() real(kind=dp), pointer :: occ_a(:) => null() real(kind=dp), pointer :: occ_b(:) => null() real(kind=dp), pointer :: fock_ao_a(:), fock_ao_b(:) real(kind=dp), pointer :: mo_e_a(:), mo_e_b(:) real(kind=dp) :: grad_norm, alpha, sy, rms_dp real(kind=dp) :: grad_norm_ratio integer :: iter, istat integer :: i, nocc ! Allocate result object allocate(scf_conv_soscf_result :: res) res%dat => self%dat res%active_converger_name = self%conv_name if (self%last_setup /= 0) then if (self%verbose > 0) write(iw, '(A)') 'SOSCF: Setup not called, returning' return end if res%ierr = 0 istat = 0 ! --- Step 1: Extract current data from converger_data --- mo_e_a => self%dat%get_mo_e_a(-1) fock_ao_a => self%dat%get_fock(-1, 1) self%mo_a = self%dat%get_mo_a(-1) self%dens_a = self%dat%get_density(-1, 1) if (self%scf_type > 1) then mo_e_b => self%dat%get_mo_e_b(-1) fock_ao_b => self%dat%get_fock(-1, 2) self%mo_b = self%dat%get_mo_b(-1) self%dens_b = self%dat%get_density(-1, 2) end if if (self%scf_type == 1) then call self%rms_density( d_new_a = self%dens_a, & d_old_a = self%dens_a_old, & rms_dp = rms_dp ) self%dens_a_old = self%dens_a else call self%rms_density(self%dens_a, self%dens_b, & self%dens_a_old, self%dens_b_old, rms_dp) self%dens_a_old = self%dens_a self%dens_b_old = self%dens_b end if pfon => self%dat%get_pfon(-1) if (associated(pfon)) then ! Get occupation arrays directly from pfon occ_a => pfon%occ_a if (self%scf_type > 1) then occ_b => pfon%occ_b end if else ! Allocate local occupation arrays nocc = min(self%nocc_a,self%nocc_b) allocate(occ_a(nocc), source=0.0_dp) occ_a = 2 if (self%scf_type/=1) occ_a = 1 end if ! --- Step 2: Initialize LBFGS history --- if (self%first_macro) then self%s_history = 0.0_dp self%y_history = 0.0_dp self%upd_history = 0.0_dp self%grad_prev = 0.0_dp self%grad = 0.0_dp self%x = 0.0_dp self%x_prev = 0.0_dp self%m_history = 0 if (self%m_history == 0) & call self%init_hess_inv(mo_e_a, mo_e_b) self%first_macro = .false. if (self%verbose>1) then write(iw, '(A,E20.10)') 'DEBUG: soscf_run: Input: grad_thresh=', self%grad_thresh write(iw, '(A,E20.10)') 'DEBUG: soscf_run: Input: level_shift=', self%level_shift end if end if call self%calc_orb_grad(self%grad, fock_ao_a, fock_ao_b, self%mo_a, self%mo_b) grad_norm = sqrt(dot_product(self%grad, self%grad)/self%nvec) if (self%m_history == 0 ) self%rms_grad_prev = grad_norm if (self%verbose>1) & write(iw, '(A,I3,A,E20.10)') 'DEBUG soscf_run:1:calc_orb_grad: iter=', 0, ' grad_norm=', grad_norm if (self%verbose>1) & write(iw, '(A,E20.10)') 'DEBUG soscf_run: init: grad_thresh=', self%grad_thresh ! Check convergence if ( grad_norm < self%grad_thresh) then if (self%verbose>1) & write(iw, '(A,I3,A,E20.10)') 'DEBUG soscf_run: loop exit: iter=',iter, ' grad_norm=', grad_norm end if ! Compute trail vector x using J. Phys. Chem. 1992, 96, 9768-9774 call self%bfgs(self%x) if (self%scf_type == 1) then call self%rotate_orbs(self%x, self%nocc_a, self%nocc_a, self%mo_a) elseif(self%scf_type == 2) then call self%rotate_orbs(self%x, self%nocc_a, self%nocc_a, self%mo_a) call self%rotate_orbs(self%x(self%nocc_a * (self%nbf - self%nocc_a) +1 : self%nvec)& , self%nocc_b, self%nocc_b, self%mo_b) elseif(self%scf_type == 3) then call self%rotate_orbs(self%x, self%nocc_a, self%nocc_b, self%mo_a) self%mo_b(1:self%nbf, 1:self%nbf) = self%mo_a(1:self%nbf, 1:self%nbf) end if self%m_history = self%m_history + 1 self%grad_prev = self%grad ! --- Step 4: Update result and converger_data --- res%ierr = 0 res%error = grad_norm select type (res) class is (scf_conv_soscf_result) res%rms_grad = res%error res%rms_dp = rms_dp end select ! Update MO coefficients and compute MO energies self%dat%buffer(self%dat%slot)%mo_a = self%mo_a if (self%scf_type == 2) self%dat%buffer(self%dat%slot)%mo_b = self%mo_b call compute_mo_energies(self, fock_ao_a, self%mo_a, & self%dat%buffer(self%dat%slot)%mo_e_a, self%work_1, self%work_2) if (self%scf_type == 2) then call compute_mo_energies(self, fock_ao_b, self%mo_b, & self%dat%buffer(self%dat%slot)%mo_e_b, self%work_1, self%work_2) end if if (self%soscf_reset_mod == 0) return if (mod(self%m_history, self%soscf_reset_mod) == 0) then if (self%rms_grad_prev > 1.0d-12) then grad_norm_ratio = grad_norm / self%rms_grad_prev self%rms_grad_prev = grad_norm if (grad_norm_ratio > 0.95_dp) then write(iw, '(8X, "Resetting Hessian, gradient norm ratio = ", F8.5)') grad_norm_ratio self%m_history = 0 self%first_macro = .true. end if end if end if end subroutine soscf_run subroutine rms_density(self, d_new_a, d_new_b, & d_old_a, d_old_b, rms_dp, max_dp) use, intrinsic :: iso_fortran_env, only : dp => real64 implicit none class(soscf_converger), intent(in) :: self real(dp), intent(in) :: d_new_a(:), d_old_a(:) real(dp), intent(in), optional :: d_new_b(:), d_old_b(:) real(dp), intent(out) :: rms_dp real(dp), intent(out), optional :: max_dp integer :: ntri, k real(dp) :: sum_sq, sum_sq_b real(dp) :: max_loc, max_loc_b real(dp) :: diff ntri = self%nbf_tri sum_sq = 0.0_dp max_loc = 0.0_dp !$omp parallel do default(shared) private(k,diff) & !$omp& reduction(+:sum_sq) reduction(max:max_loc) do k = 1, ntri diff = d_new_a(k) - d_old_a(k) sum_sq = sum_sq + diff*diff max_loc = max(max_loc, abs(diff)) end do !$omp end parallel do rms_dp = sqrt( sum_sq / real(ntri, dp) ) if (present(max_dp)) max_dp = max_loc if (present(d_new_b) .and. present(d_old_b)) then sum_sq_b = 0.0_dp max_loc_b = 0.0_dp !$omp parallel do default(shared) private(k,diff) & !$omp& reduction(+:sum_sq_b) reduction(max:max_loc_b) do k = 1, ntri diff = d_new_b(k) - d_old_b(k) sum_sq_b = sum_sq_b + diff*diff max_loc_b = max(max_loc_b, abs(diff)) end do !$omp end parallel do rms_dp = 0.5_dp * ( rms_dp + sqrt( sum_sq_b / real(ntri, dp) ) ) if (present(max_dp)) max_dp = max(max_dp, max_loc_b) end if end subroutine rms_density !> @brief Computes the initial diagonal inverse Hessian for SOSCF !> @details Approximates the inverse Hessian diagonal using orbital energy differences, !> adjusted for SCF type: !> - RHF: h_inv = 0.25 / (ε_a - ε_i) for closed-shell. !> - UHF: h_inv combines alpha and beta energy differences. !> - ROHF: h_inv varies by orbital region (closed-virtual, open-virtual). !> Includes level-shifting for stability. !> @param[out] self%h_inv Diagonal inverse Hessian (size depends on scf_type) !> @param[in] mo_e_a Alpha orbital energies (size: nbf) !> @param[in] mo_e_b Beta orbital energies (size: nbf, ignored for RHF/ROHF) subroutine init_hess_inv(self, mo_e_a, mo_e_b) implicit none class(soscf_converger) :: self real(kind=dp), pointer, intent(in) :: mo_e_a(:) real(kind=dp), pointer, intent(in) :: mo_e_b(:) real(kind=dp) :: diff, scale integer :: i, a, k, istart associate (nbf => self%nbf, & nocc_a => self%nocc_a, & nocc_b => self%nocc_b, & scf_type => self%scf_type, & lvl_shift => self%level_shift, & thresh => self%hess_thresh) select case (scf_type) case (1) ! RHF: Closed-shell system ! Single set of orbitals, nocc_a = nocc_b, 4 * (ε_a - ε_i) scaling k = 0 do i = 1, nocc_a if (i <= nocc_b ) then istart = nocc_b+1 scale = 0.25_dp ! Closed-virtual: like RHF else istart = nocc_a +1 scale = 0.25_dp ! Open-virtual: like UHF (singly occupied) end if do a= istart, nbf k = k +1 diff = mo_e_a(a) - mo_e_a(i) if (abs(diff) < thresh) then diff = sign(thresh + lvl_shift, diff) end if self%h_inv(k) = scale /diff end do end do case (2) ! UHF: Unrestricted, separate alpha and beta orbitals ! Alpha occupied -> alpha virtual, beta occupied -> beta virtual k = 0 ! Alpha rotations do i = 1, nocc_a do a= nocc_a+1, nbf k = k +1 diff = mo_e_a(a) - mo_e_a(i) if (abs(diff) < thresh) then diff = sign(thresh + lvl_shift, diff) end if self%h_inv(k) = 0.5_dp /diff end do end do ! Beta rotations do i = 1, nocc_b do a= nocc_b+1, nbf k = k +1 diff = mo_e_b(a) - mo_e_b(i) if (abs(diff) < thresh) then diff = sign(thresh + lvl_shift, diff) end if self%h_inv(k) = 0.5_dp /diff end do end do case (3) ! ROHF: Restricted open-shell ! Regions: closed (j <= nocc_b) ! open (nocc_b < j <= nocc_a) ! virtual (j > nocc_a) k = 0 do i = 1, nocc_a if (i <= nocc_b ) then istart = nocc_b+1 scale = 0.25_dp ! Closed-virtual: like RHF else istart = nocc_a +1 scale = 0.50_dp ! Open-virtual: like UHF (singly occupied) end if do a= istart, nbf k = k +1 diff = mo_e_a(a) - mo_e_a(i) if (abs(diff) < thresh) then diff = sign(thresh + lvl_shift, diff) end if self%h_inv(k) = scale /diff end do end do end select end associate end subroutine init_hess_inv !> @brief Computes orbital gradient. !> @details Calculates the gradient for occupied-virtual orbital rotations: !> - RHF: g(i,a) = 4 * F(i,a), single gradient. !> - UHF: g_a(i,a) = 2 * F_a(i,a), g_b(i,a) = 2 * F_b(i,a), separate gradients. !> - ROHF: g(c,v) = 2 * F_b(c,v) for doubly occ -> singly occ, !> g(o,v) = 2 * (F_a(o,v) + F_b(o,v)) doubly occ ->virt, !> g(o,v) = 2 * F_a(o,v) for singly occ -> virt !> @param[out] grad_a Alpha gradient for RHF, ROHF, and UHF !> @param[out] grad_b Beta gradient for UHF (optional, size: nocc_b * (nbf - nocc_b)) !> @param[in] fock_a Packed alpha/single Fock matrix (size: nbf*(nbf+1)/2) !> @param[in] fock_b Packed beta Fock matrix for UHF (optional, size: nbf*(nbf+1)/2) subroutine calc_orb_grad(self, grad, fock_a, fock_b, mo_a, mo_b) implicit none class(soscf_converger) :: self real(kind=dp), intent(out) :: grad(:) real(kind=dp), pointer, intent(in) :: fock_a(:) real(kind=dp), pointer, intent(in) :: fock_b(:) real(kind=dp), intent(in) :: mo_a(:,:) real(kind=dp), intent(in) :: mo_b(:,:) integer :: i, a, k, nvir_a, nvir_b, istart if (.not. ASSOCIATED(fock_a)) & call show_message('Failed to use Fock array in calc_orb_grad', & with_abort) associate (nbf => self%nbf, & nocc_a => self%nocc_a, & nocc_b => self%nocc_b) nvir_a = nbf - nocc_a nvir_b = nbf - nocc_b grad = 0.0_dp select case (self%scf_type) case (1) ! RHF ! Unpack AO-basis Fock matrix self%work_1 = 0.0_dp call unpack_matrix(fock_a, self%work_1) ! Convert AO Fock to MO Fock. ! self%work_2 = F_ao * C_virt call dgemm('N', 'N', nbf, nvir_a, nbf, & 1.0_dp, self%work_1, nbf, & mo_a(:, nocc_a+1:), nbf, & 0.0_dp, self%work_2, nbf) ! self%work_1 = C_occ^T * self%work_2 call dgemm('T', 'N', nocc_a, nvir_a, nbf, & 1.0_dp, mo_a(:, 1:nocc_a), nbf, & self%work_2, nbf, & 0.0_dp, self%work_1(1:nocc_a, 1:nvir_a), nocc_a) k = 0 do i = 1, nocc_a if (i <= nocc_b ) then istart = nbf - nocc_b else istart = nbf - nocc_a end if do a= 1, istart k = k +1 grad(k) = 4.0_dp * self%work_1(i, a) end do end do case (2) ! UHF if (.not. associated(fock_b)) & call show_message('Failed to use Fock arrays in calc_orb_grad', & with_abort) ! Alpha call unpack_matrix(fock_a, self%work_1) call dgemm('N', 'N', nbf, nvir_a, nbf, & 1.0_dp, self%work_1, nbf, & mo_a(:, nocc_a+1:), nbf, & 0.0_dp, self%work_2, nbf) call dgemm('T', 'N', nocc_a, nvir_a, nbf, & 1.0_dp, mo_a(:, 1:nocc_a), nbf, & self%work_2, nbf, & 0.0_dp, self%work_1(1:nocc_a, 1:nvir_a), nocc_a) k = 0 do i = 1, nocc_a do a= 1, nvir_a k = k +1 grad(k) = 2.0_dp * self%work_1(i, a) end do end do ! Beta self%work_1 = 0.0_dp call unpack_matrix(fock_b, self%work_1) call dgemm('N', 'N', nbf, nvir_b, nbf, & 1.0_dp, self%work_1, nbf, & mo_b(:, nocc_b+1:), nbf, & 0.0_dp, self%work_2, nbf) call dgemm('T', 'N', nocc_b, nvir_b, nbf, & 1.0_dp, mo_b(:, 1:nocc_b), nbf, & self%work_2, nbf, & 0.0_dp, self%work_1(1:nocc_b, 1:nvir_b), nocc_b) do i = 1, nocc_b do a= 1, nvir_b k = k +1 grad(k) = 2.0_dp * self%work_1(i, a) end do end do case (3) ! ROHF self%work_1 = 0.0_dp self%work_3 = 0.0_dp call unpack_matrix(fock_a, self%work_1) call unpack_matrix(fock_b, self%work_3) call dgemm('N', 'N', nbf, nvir_a, nbf, & 1.0_dp, self%work_1, nbf, & mo_a(:, nocc_a+1:), nbf, & 0.0_dp, self%work_2, nbf) call dgemm('T', 'N', nocc_a, nvir_a, nbf, & 1.0_dp, mo_a(:, 1:nocc_a), nbf, & self%work_2, nbf, & 0.0_dp, self%work_1(1:nocc_a, 1:nvir_a), nocc_a) self%work_2 = 0.0_dp call dgemm('N', 'N', nbf, nvir_b, nbf, & 1.0_dp, self%work_3, nbf, & mo_a(:, nocc_b+1:), nbf, & 0.0_dp, self%work_2, nbf) call dgemm('T', 'N', nocc_b, nvir_b, nbf, & 1.0_dp, mo_a(:, 1:nocc_b), nbf, & self%work_2, nbf, & 0.0_dp, self%work_3(1:nocc_b, 1:nvir_b), nocc_b) k = 0 do i = 1, nocc_a if (i <= nocc_b ) then istart = nbf - nocc_b else istart = nbf - nocc_a end if do a= 1, istart k = k +1 if (i <= nocc_b) then if (a <= (nocc_a - nocc_b)) then grad(k) = 2.0_dp * self%work_3(i,a) else grad(k) = 2.0_dp * (self%work_3(i,a) + self%work_1(i, a - nocc_a + nocc_b)) end if else grad(k) = 2.0_dp * self%work_1(i, a) end if end do end do end select end associate end subroutine calc_orb_grad subroutine bfgs_original_legacy(self, x) implicit none class(soscf_converger) :: self real(kind=dp), intent(out) :: x(:) real(kind=dp) :: displn(self%nvec) real(kind=dp) :: dgrad(self%nvec) real(kind=dp) :: updti(self%nvec) real(kind=dp) :: alpha, beta, t, t1, t2, t3, t4, scale, norm_disp real(kind=dp) :: s1, s2, s3, s4, s5, s6 integer :: i, j ! Initialize displacement and preconditioned gradient difference if (self%m_history < 1) then scale = 1.0_dp x = -scale * self%h_inv * self%grad where (isnan(x)) x = 0.0_dp else displn = self%h_inv * self%grad dgrad = self%grad-self%grad_prev updti = self%h_inv * dgrad if (self%m_history > 1) then do i = 1, self%m_history-1 s1 = dot_product(self%s_history(:,i), self%y_history(:,i)) s2 = dot_product(self%y_history(:,i), self%upd_history(:,i)) s3 = dot_product(self%s_history(:,i), self%grad) s4 = dot_product(self%upd_history(:,i), self%grad) s5 = dot_product(self%s_history(:,i), dgrad) s6 = dot_product(self%upd_history(:,i), dgrad) s1 = 1.0d0 / s1 s2 = 1.0d0 / s2 t = 1.0d0 + s1 / s2 t2 = s1 * s3 t4 = s1 * s5 t1 = t * t2 - s1 * s4 t3 = t * t4 - s1 * s6 displn = displn + t1 * self%s_history(:,i) - t2 * self%upd_history(:,i) updti = updti + t3 * self%s_history(:,i) - t4 * self%upd_history(:,i) end do end if ! Final correction using current dgrad and updti s1 = dot_product(self%x_prev, dgrad) s2 = dot_product(dgrad, updti) s3 = dot_product(self%x_prev, self%grad) s4 = dot_product(updti, self%grad) s1 = 1.0_dp / s1 s2 = 1.0_dp /s2 t = 1.0_dp + s1 / s2 t2 = s1 * s3 t1 = t * t2 - s1 * s4 displn = displn + t1 * self%x_prev - t2 * updti self%s_history(:, self%m_history) = self%x_prev self%y_history(:, self%m_history) = dgrad self%upd_history(:, self%m_history) = updti x = -displn end if norm_disp = sqrt(dot_product(x, x)/self%nvec) if (norm_disp > 0.1) then x = x*0.1/norm_disp end if self%x_prev = x self%grad_prev = self%grad end subroutine bfgs_original_legacy subroutine bfgs_stable_only_impl(self, x) implicit none class(soscf_converger) :: self real(kind=dp), intent(out) :: x(:) ! --- locals --- real(kind=dp) :: displn(self%nvec) ! preconditioned (approx) H^{-1} g real(kind=dp) :: dgrad(self%nvec) ! y_k = g_k - g_{k-1} real(kind=dp) :: updti(self%nvec) ! H0^{-1} y_k (H0 diagonal from gaps) real(kind=dp) :: s1, s2, s3, s4, s5, s6 ! scalars used in the L-BFGS application real(kind=dp) :: t, t1, t2, t3, t4 integer :: i ! cheap step control scalars real(kind=dp) :: gg, hh, alpha, cg, kappa_max, norm_rms real(kind=dp), parameter :: eps = 1.0e-16_dp real(kind=dp), parameter :: alpha_min = 0.05_dp !real(kind=dp), parameter :: alpha_max = 1.00_dp !real(kind=dp), parameter :: kappa_lim = 0.30_dp ! cap on max |rotation element| real(dp) :: alpha_max, kappa_l alpha_max = self%alpha_cap kappa_l = self%kappa_lim ! -------- form search direction p (in x) via diagonal-precond L-BFGS -------- if (self%m_history < 1) then ! first step: preconditioned steepest descent x = - self%h_inv * self%grad where (isnan(x)) x = 0.0_dp else displn = self%h_inv * self%grad dgrad = self%grad - self%grad_prev updti = self%h_inv * dgrad ! apply stored pairs (i = 1 .. m_history-1) with curvature checks if (self%m_history > 1) then do i = 1, self%m_history-1 s1 = dot_product(self%s_history(:,i), self%y_history(:,i)) ! s·y s2 = dot_product(self%y_history(:,i), self%upd_history(:,i)) ! y·(H0^{-1}y) ! skip ill-conditioned pairs if ( s1 <= 1.0e-12_dp * & sqrt(max(dot_product(self%s_history(:,i), self%s_history(:,i)), eps)) * & sqrt(max(dot_product(self%y_history(:,i), self%y_history(:,i)), eps)) ) cycle if (abs(s2) <= eps) cycle s3 = dot_product(self%s_history(:,i), self%grad) ! s·g s4 = dot_product(self%upd_history(:,i), self%grad) ! (H0^{-1}y)·g s5 = dot_product(self%s_history(:,i), dgrad) ! s·y_k s6 = dot_product(self%upd_history(:,i), dgrad) ! (H0^{-1}y)·y_k s1 = 1.0_dp / s1 s2 = 1.0_dp / s2 t = 1.0_dp + s1 / s2 t2 = s1 * s3 t4 = s1 * s5 t1 = t * t2 - s1 * s4 t3 = t * t4 - s1 * s6 displn = displn + t1 * self%s_history(:,i) - t2 * self%upd_history(:,i) updti = updti + t3 * self%s_history(:,i) - t4 * self%upd_history(:,i) end do end if ! final correction using the current pair (x_prev, dgrad) s1 = dot_product(self%x_prev, dgrad) ! s·y with current pair s2 = dot_product(dgrad, updti) ! y·(H0^{-1}y) if ( s1 > 1.0e-12_dp * sqrt(max(dot_product(self%x_prev, self%x_prev), eps)) * & sqrt(max(dot_product(dgrad, dgrad), eps)) .and. abs(s2) > eps ) then s3 = dot_product(self%x_prev, self%grad) ! s·g s4 = dot_product(updti, self%grad) ! (H0^{-1}y)·g s1 = 1.0_dp / s1 s2 = 1.0_dp / s2 t = 1.0_dp + s1 / s2 t2 = s1 * s3 t1 = t * t2 - s1 * s4 displn = displn + t1 * self%x_prev - t2 * updti ! store the current pair for the next iteration (safe to reuse) self%s_history(:, self%m_history) = self%x_prev self%y_history(:, self%m_history) = dgrad self%upd_history(:, self%m_history) = updti else ! neutral store (keeps indexing consistent; pair will be skipped next time) self%s_history(:, self%m_history) = 0.0_dp self%y_history(:, self%m_history) = 0.0_dp self%upd_history(:, self%m_history) = 0.0_dp end if x = -displn end if ! -------------------- CHEAP step length and safeguards --------------------- ! Quadratic one-shot step along p = x using diagonal Hessian H0 gg = dot_product(self%grad, x) ! g·p hh = sum( x * x / max(self%h_inv, eps) ) ! p·H0·p (H0 = diag(1/h_inv)) if (hh > eps) then alpha = -gg / hh else alpha = 0.1_dp end if alpha = min(max(alpha, alpha_min), alpha_max) ! angle safeguard: if p is poorly aligned with -g, be conservative cg = -gg / ( sqrt(max(dot_product(self%grad, self%grad), eps)) * & sqrt(max(dot_product(x, x), eps)) ) if (cg < 0.2_dp) alpha = 0.5_dp * alpha ! elementwise amplitude cap (keeps exponential/orbital update in safe regime) kappa_max = maxval(abs(x)) if (kappa_max > 0.0_dp) then if (alpha * kappa_max > kappa_l) alpha = (kappa_l / kappa_max) end if ! scale the step x = alpha * x ! existing RMS clamp (RMS amplitude <= 0.1) norm_rms = sqrt(dot_product(x, x) / self%nvec) if (norm_rms > 0.1_dp) x = x * (0.1_dp / norm_rms) where (isnan(x)) x = 0.0_dp ! keep these for the next macro-iteration self%x_prev = x self%grad_prev = self%grad self%alpha_last = alpha self%gg_last = gg self%hh_last = hh self%g_rms_last = sqrt( sum(self%grad*self%grad) / real(self%nvec,dp) ) end subroutine bfgs_stable_only_impl subroutine bfgs_quad_ls_impl(self, x) !! Mode 2: Curvature-safe L-BFGS + robust quadratic line search + 1-D trust region !! - No extra J/K builds. !! - Conservative early caps; bolder near the end. !! - Guarantees descent direction; falls back to precond. SD if needed. implicit none class(soscf_converger) :: self real(kind=dp), intent(out) :: x(:) integer :: n, i, m real(kind=dp) :: eps, sTy, rho_i, qdot, sN, yN, tol_curv real(kind=dp) :: gnorm, pnorm, g_rms, p_rms, p_inf, cg real(kind=dp) :: gg, hh, alpha, alpha_min, alpha_max real(kind=dp) :: delta_rms, kappa_lim, dEmodel ! Two-loop temporaries (automatic arrays) real(kind=dp) :: q(size(x)), r(size(x)) real(kind=dp), allocatable :: alpha_buf(:) n = size(x) eps = 1.0e-16_dp ! ------------------------------- ! 1) Curvature-safe L-BFGS two-loop ! ------------------------------- m = self%m_history q = self%grad if (m > 0) then allocate(alpha_buf(m)) ! backward loop do i = m, 1, -1 sTy = dot_product(self%s_history(:,i), self%y_history(:,i)) ! Nocedal curvature tolerance: skip weak/indef pairs sN = sqrt( max(dot_product(self%s_history(:,i), self%s_history(:,i)), eps) ) yN = sqrt( max(dot_product(self%y_history(:,i), self%y_history(:,i)), eps) ) tol_curv = 1.0e-12_dp * sN * yN if (sTy > tol_curv) then rho_i = 1.0_dp / sTy alpha_buf(i) = rho_i * dot_product(self%s_history(:,i), q) q = q - alpha_buf(i) * self%y_history(:,i) else alpha_buf(i) = 0.0_dp end if end do end if ! Initial inverse-Hessian action via diagonal preconditioner H0^{-1} r = self%h_inv * q if (m > 0) then ! forward loop do i = 1, m sTy = dot_product(self%s_history(:,i), self%y_history(:,i)) sN = sqrt( max(dot_product(self%s_history(:,i), self%s_history(:,i)), eps) ) yN = sqrt( max(dot_product(self%y_history(:,i), self%y_history(:,i)), eps) ) tol_curv = 1.0e-12_dp * sN * yN if (sTy <= tol_curv) cycle rho_i = 1.0_dp / sTy qdot = rho_i * dot_product(self%y_history(:,i), r) r = r + self%s_history(:,i) * (alpha_buf(i) - qdot) end do deallocate(alpha_buf) end if ! Unscaled search direction x = -r ! ------------------------------- ! 2) Robust step control (no extra J/K) ! - quadratic step on diagonal model ! - descent/angle checks ! - 1-D trust region (RMS + ∞-norm) ! ------------------------------- g_rms = sqrt( sum(self%grad*self%grad) / real(self%nvec, dp) ) gnorm = sqrt( max(dot_product(self%grad, self%grad), eps) ) pnorm = sqrt( max(dot_product(x, x), eps) ) p_rms = sqrt( dot_product(x, x) / real(self%nvec, dp) ) p_inf = maxval(abs(x)) ! If L-BFGS gave a non-descent direction, fall back to preconditioned SD gg = dot_product(self%grad, x) ! g·p (p == x at this point) if (gg >= 0.0_dp) then x = - self%h_inv * self%grad ! safe fallback pnorm = sqrt( max(dot_product(x, x), eps) ) p_rms = sqrt( dot_product(x, x) / real(self%nvec, dp) ) p_inf = maxval(abs(x)) gg = dot_product(self%grad, x) end if ! Diagonal quadratic curvature along p: H0 = diag(1/h_inv) hh = sum( x * x / max(self%h_inv, eps) ) ! p·H0·p ! Stationary step on the quadratic model if (hh > eps) then alpha = -gg / hh else alpha = 0.10_dp end if ! Conservative bounds far from minimum; relax as ||g||_rms shrinks if (g_rms > 5.0e-3_dp) then alpha_min = 0.02_dp; alpha_max = 0.50_dp delta_rms = 0.08_dp; kappa_lim = 0.25_dp elseif (g_rms > 1.0e-3_dp) then alpha_min = 0.05_dp; alpha_max = 0.75_dp delta_rms = 0.12_dp; kappa_lim = 0.40_dp elseif (g_rms > 2.0e-4_dp) then alpha_min = 0.10_dp; alpha_max = 0.90_dp delta_rms = 0.16_dp; kappa_lim = 0.50_dp else alpha_min = 0.20_dp; alpha_max = 1.00_dp delta_rms = 0.20_dp; kappa_lim = 0.60_dp end if if (alpha < alpha_min) alpha = alpha_min if (alpha > alpha_max) alpha = alpha_max ! Descent-angle safeguard (only damp far from minimum) if (pnorm > 0.0_dp) then cg = -gg / ( gnorm * pnorm ) ! cos(angle(-g, p)) else cg = 1.0_dp end if if (g_rms > 3.0e-3_dp) then if (cg < 0.10_dp) alpha = 0.50_dp * alpha if (cg < 0.00_dp) alpha = 0.25_dp * alpha end if ! 1-D trust region caps if (p_rms > 0.0_dp) alpha = min(alpha, delta_rms / p_rms) if (p_inf > 0.0_dp) alpha = min(alpha, kappa_lim / p_inf) ! Model-based sufficient decrease (on the quadratic, not extra Fock): ! Ensure m(alpha) = alpha*gg + 0.5*alpha^2*hh is negative and not tiny positive. dEmodel = alpha*gg + 0.5_dp*alpha*alpha*hh if (dEmodel >= 0.0_dp .and. hh > eps) then ! move to half of stationary step on the model alpha = min(alpha, -0.5_dp*gg/hh) dEmodel = alpha*gg + 0.5_dp*alpha*alpha*hh if (dEmodel >= 0.0_dp) alpha = 0.5_dp*alpha end if ! Final scaled step x = alpha * x ! Keep these for the next iteration (unchanged behavior) self%x_prev = x self%grad_prev = self%grad end subroutine bfgs_quad_ls_impl subroutine bfgs(self, x) !! Dispatcher so that soscf_mode = 0 is *bit-identical* to original. implicit none class(soscf_converger) :: self real(kind=dp), intent(out) :: x(:) select case (self%variant) case (SOSCF_VARIANT_ORIGINAL) call bfgs_original_legacy(self, x) ! exact copy from original file case (SOSCF_VARIANT_STABLE_ONLY) call bfgs_stable_only_impl(self, x) ! curvature-safe L-BFGS + legacy RMS clamp case (SOSCF_VARIANT_QUAD_LS) call bfgs_quad_ls_impl(self, x) ! stable + quadratic line search (no extra J/K) case default call bfgs_original_legacy(self, x) end select end subroutine bfgs subroutine rotate_orbs(self, x, nocc_a, nocc_b, mo) implicit none class(soscf_converger) :: self real(kind=dp), intent(in) :: x(:) integer, intent(in) :: nocc_a, nocc_b real(kind=dp), intent(inout) :: mo(:,:) integer :: nbf, i, idx logical :: second_term nbf = self%nbf self%work_1 = 0 self%work_2 = 0 idx = 0 second_term = .true. if (self%scf_type == 3) then! ROHF second_term = .false. end if call exp_scaling(self%work_1, x, idx, nocc_a, nocc_b, nbf, second_term) call orthonormalize(self%work_1, nbf) call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, mo, nbf, self%work_1, nbf, 0.0_dp, self%work_2, nbf) mo = self%work_2 contains subroutine exp_scaling(G, x, idx, nocc_a, nocc_b, nbf, second_term) real(kind=dp), intent(out) :: G(:,:) real(kind=dp), intent(in) :: x(:) integer, intent(inout) :: idx integer, intent(in) :: nocc_a, nocc_b, nbf real(kind=dp), allocatable :: K(:,:), K2(:,:) integer :: occ, virt, istart, i logical, intent(inout) :: second_term allocate(K(nbf,nbf), source=0.0_dp) allocate(K2(nbf,nbf), source=0.0_dp) do occ = 1, nocc_a istart = merge(nocc_b+1, nocc_a+1, occ <= nocc_b) do virt = istart, nbf idx = idx + 1 K(virt, occ) = x(idx) K(occ, virt) = -x(idx) end do end do G = 0.0_dp do i = 1, nbf G(i,i) = 1.0_dp end do G = G + K if (second_term) then call dgemm('N','T', nbf, nbf, nbf, 1.0_dp, K, nbf, K, nbf, 0.0_dp, K2, nbf) G = G + 0.5_dp * K2 end if deallocate(K, K2) end subroutine exp_scaling subroutine orthonormalize(G, nbf) real(kind=dp), intent(inout) :: G(:,:) integer, intent(in) :: nbf integer :: i, j real(kind=dp) :: norm, dot do i = 1, nbf norm = sqrt(dot_product(G(:,i), G(:,i))) call dscal(nbf, 1.0_dp / norm, G(1,i), 1) if (i == nbf) cycle do j = i+1, nbf dot = dot_product(G(:,i), G(:,j)) call daxpy(nbf, -dot, G(1,i), 1, G(1,j), 1) end do end do end subroutine orthonormalize end subroutine rotate_orbs !> @brief Computes MO energies from the Fock matrix and updated MO coefficients. !> @details Projects the Fock matrix onto the MO basis: F_mo = C^T * F * C, !> taking diagonal elements as approximate MO energies for monitoring. !> @param[in] self The SOSCF converger object (for nbf). !> @param[in] fock Packed Fock matrix (nbf_tri). !> @param[in] mo_coeffs MO coefficients (nbf, nbf). !> @param[out] mo_energies MO energies (nbf). subroutine compute_mo_energies(self, fock, mo_coeffs, mo_energies, work_1, work_2) use mathlib, only: unpack_matrix implicit none class(soscf_converger), intent(in) :: self real(kind=dp), intent(in) :: fock(:) real(kind=dp), intent(in) :: mo_coeffs(:, :) real(kind=dp), intent(out) :: mo_energies(:) real(kind=dp), intent(inout) :: work_1(:, :) real(kind=dp), intent(inout) :: work_2(:, :) real(kind=dp), allocatable :: f_full(:, :), temp(:, :), f_mo(:, :) integer :: nbf, i nbf = self%nbf call unpack_matrix(fock, work_1) call dgemm('T', 'N', nbf, nbf, nbf, & 1.0_dp, mo_coeffs, nbf, & work_1, nbf, & 0.0_dp, work_2, nbf) call dgemm('N', 'N', nbf, nbf, nbf, & 1.0_dp, work_2, nbf, & mo_coeffs, nbf, & 0.0_dp, work_1, nbf) do i = 1, nbf mo_energies(i) = work_1(i, i) end do end subroutine compute_mo_energies !==================================================================== ! Trust region agumnted hessian subroutines !==================================================================== !> @brief Initialize the TRAH (Trust-Region Augmented Hessian) converger. !> @detail Copies basic problem dimensions and pointers from `params`, sets !> up spin/occ/vir sizes, counts the optimization variables, and !> allocates working arrays via `alloc_workspace`. !> @param[inout] self TRAH converger object. !> @param[in] params SCF convergence context (dimensions, data handles). !> @author Mohsen Mazaherifar !> @date August 2025 subroutine trah_init(self, params) implicit none class(trah_converger), intent(inout) :: self type(scf_conv), target, intent(in) :: params call self%subconverger_init(params) self%conv_name = 'TRAH' self%nfocks = params%dat%num_focks self%verbose = params%verbose self%nbf = params%dat%ldim self%nocc_a = params%dat%nelec_a self%nocc_b = params%dat%nelec_b self%nbf_tri = self%nbf * (self%nbf + 1) / 2 self%dat => params%dat self%overlap => params%overlap self%overlap_invsqrt => params%overlap_sqrt self%scf_type = params%scf_type self%nvir_a = self%nbf - self%nocc_a self%nvir_b = self%nbf - self%nocc_b ! self%is_dft = (infos%control%hamilton >= 20) ! self%hf_scale = merge(infos%dft%HFscale, 1.0_dp, self%is_dft) select case (self%scf_type) case (SCF_RHF) self%n_param = int((self%nocc_a * self%nvir_a), kind=int32) case (2) self%n_param = int((self%nocc_a * self%nvir_a) + (self%nocc_b * self%nvir_b), kind=int32) case (SCF_ROHF) self%n_param = int(self%nvir_a * (self%nocc_a - self%nocc_b) + (self%nocc_b * self%nvir_b), kind=int32) end select call alloc_workspace(self) end subroutine trah_init !> @brief Initialize the TRAH (Trust-Region Augmented Hessian) converger. !> @detail Copies basic problem dimensions and pointers from `params`, sets !> up spin/occ/vir sizes, counts the optimization variables, and !> allocates working arrays via `alloc_workspace`. !> @param[inout] self TRAH converger object. !> @param[in] params SCF convergence context (dimensions, data handles). !> @author Mohsen Mazaherifar !> @date August 2025 subroutine alloc_workspace(self) implicit none class(trah_converger), intent(inout) :: self integer :: nbf, nbf_tri nbf = self%nbf nbf_tri = nbf*(nbf+1)/2 if (.not.allocated(self%work1)) allocate(self%work1(nbf,nbf)) if (.not.allocated(self%work2)) allocate(self%work2(nbf,nbf)) if (.not.allocated(self%work3)) allocate(self%work3(nbf,nbf)) if (.not.allocated(self%v)) allocate(self%v(nbf,nbf)) if (.not.allocated(self%dm)) allocate(self%dm(nbf,nbf)) if (.not.allocated(self%pfock)) allocate(self%pfock(nbf_tri, self%nfocks)) if (.not.allocated(self%dens)) allocate(self%dens(nbf_tri, self%nfocks)) if (.not.allocated(self%fock_ao)) allocate(self%fock_ao(nbf_tri, self%nfocks)) if (.not.allocated(self%d_old)) allocate(self%d_old(nbf_tri, self%nfocks)) if (.not.allocated(self%f_old)) allocate(self%f_old(nbf_tri, self%nfocks)) if (.not.allocated(self%dm_tri)) allocate(self%dm_tri(nbf_tri, self%nfocks)) if (self%scf_type == SCF_RHF .or. self%scf_type == SCF_ROHF .or. self%scf_type == SCF_UHF) then if (.not.allocated(self%mo_a)) allocate(self%mo_a(nbf,nbf)) if (.not.allocated(self%foo_a)) allocate(self%foo_a(self%nocc_a, self%nocc_a)) if (.not.allocated(self%fvv_a)) allocate(self%fvv_a(self%nvir_a, self%nvir_a)) if (.not.allocated(self%xmat_a)) allocate(self%xmat_a(self%nvir_a, self%nocc_a)) if (.not.allocated(self%x2mat_a)) allocate(self%x2mat_a(self%nvir_a, self%nocc_a)) end if if (self%scf_type == SCF_UHF .or. self%scf_type == SCF_ROHF) then if (.not.allocated(self%mo_b)) allocate(self%mo_b(nbf,nbf)) if (.not.allocated(self%foo_b)) allocate(self%foo_b(self%nocc_b, self%nocc_b)) if (.not.allocated(self%fvv_b)) allocate(self%fvv_b(self%nvir_b, self%nvir_b)) if (.not.allocated(self%xmat_b)) allocate(self%xmat_b(self%nvir_b, self%nocc_b)) if (.not.allocated(self%x2mat_b)) allocate(self%x2mat_b(self%nvir_b, self%nocc_b)) end if end subroutine alloc_workspace !> @brief Free all allocated TRAH work arrays. !> @detail Deallocates AO work buffers, spin-resolved slices, packed !> densities/Focks, and incremental-update buffers (d_old, f_old). !> @param[inout] self TRAH converger object to clean up. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine trah_clean(self) implicit none class(trah_converger), intent(inout) :: self if (allocated(self%work1)) deallocate(self%work1) if (allocated(self%work2)) deallocate(self%work2) if (allocated(self%work3)) deallocate(self%work3) if (allocated(self%v)) deallocate(self%v) if (allocated(self%dm)) deallocate(self%dm) if (allocated(self%pfock)) deallocate(self%pfock) if (allocated(self%dm_tri)) deallocate(self%dm_tri) if (allocated(self%foo_a)) deallocate(self%foo_a) if (allocated(self%fvv_a)) deallocate(self%fvv_a) if (allocated(self%xmat_a)) deallocate(self%xmat_a) if (allocated(self%x2mat_a)) deallocate(self%x2mat_a) if (allocated(self%foo_b)) deallocate(self%foo_b) if (allocated(self%fvv_b)) deallocate(self%fvv_b) if (allocated(self%xmat_b)) deallocate(self%xmat_b) if (allocated(self%x2mat_b)) deallocate(self%x2mat_b) if (allocated(self%d_old)) deallocate(self%d_old) if (allocated(self%f_old)) deallocate(self%f_old) end subroutine trah_clean !> @brief Lightweight (re)setup before a TRAH iteration. !> @detail Resets internal flags that track whether data-dependent setup !> has been performed for the current iteration/step. !> @param[inout] self TRAH converger object. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine trah_setup(self) class(trah_converger), intent(inout) :: self ! Data verification on each setup call self%last_setup = 0 end subroutine trah_setup !> @brief Entry point to run a TRAH step (skeleton). !> @detail Pulls the latest Fock, density, and MO blocks from `self%dat` !> and creates a `scf_conv_trah_result`. Actual step logic is !> intended to be added where noted. !> @param[inout] self TRAH converger object. !> @param[out] res Result object produced by TRAH iteration. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine trah_run(self, res) ! use otr_interface, only: init_trah_solver, run_trah_solver class(trah_converger), target, intent(inout) :: self class(scf_conv_result), allocatable, intent(out) :: res ! --- Step 1: Extract current data from converger_data --- self%fock_ao(:,1) = self%dat%get_fock(-1, 1) self%mo_a = self%dat%get_mo_a(-1) self%dens(:,1) = self%dat%get_density(-1, 1) if (self%scf_type > 1) then self%fock_ao(:,2) = self%dat%get_fock(-1, 2) self%mo_b = self%dat%get_mo_b(-1) self%dens(:,2) = self%dat%get_density(-1, 2) end if allocate(scf_conv_trah_result :: res) res%dat => self%dat res%active_converger_name = self%conv_name res%ierr=0 end subroutine trah_run !> @brief Build orbital-rotation gradient and diagonal of the Hessian proxy. !> @detail Transforms packed AO Fock to MO space (FOO/FVV blocks) and !> constructs the orbital-gradient (off-diagonal Fock) and a !> diagonal approximation to the Hessian using eigenvalue gaps. !> Covers RHF, UHF (α/β), and ROHF cases. !> @param[inout] self TRAH converger. !> @param[out] grad Orbital-rotation gradient (vectorized). !> @param[out] h_diag Diagonal of the Hessian approximation. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine calc_g_h(self, grad, h_diag) use precision, only: dp use mathlib, only: unpack_matrix implicit none class(trah_converger), intent(inout) :: self real(dp), intent(out) :: grad(:) real(dp), intent(out) :: h_diag(:) real(dp), allocatable :: ugd(:), uh(:) integer :: i, a, k, k_rohf associate( nbf => self%nbf, & nocc_a => self%nocc_a, & nocc_b => self%nocc_b, & nvir_a => self%nvir_a, & nvir_b => self%nvir_b, & w1 => self%work1, & w2 => self%work2, & w3 => self%work3, & fock_ao => self%fock_ao,& mo_a => self%mo_a, & mo_b => self%mo_b, & foo => self%foo_a, fvv => self%fvv_a,& foo_b => self%foo_b, fvv_b => self%fvv_b) select case (self%scf_type) case (SCF_RHF) call unpack_matrix(fock_ao(:,1), w1) w2 = 0.0_dp w3 = 0.0_dp call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, w1, nbf, mo_a, nbf, 0.0_dp, w2, nbf) call dgemm('T','N', nbf, nbf, nbf, 1.0_dp, mo_a, nbf, w2, nbf, 0.0_dp, w3, nbf) foo = w3(1:nocc_a,1:nocc_a) fvv = w3(nocc_a+1:nbf,nocc_a+1:nbf) k = 0 do i = nocc_a+1, nbf do a = 1, nocc_a k = k + 1 grad(k) = 2.0_dp * w3(i,a) h_diag(k) = 2.0_dp * ( w3(i,i) - w3(a,a) ) end do end do case (SCF_UHF) ! ---- UHF α block ---- call unpack_matrix(fock_ao(:,1), w1) w2 = 0.0_dp w3 = 0.0_dp call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, w1, nbf, mo_a, nbf, 0.0_dp, w2, nbf) call dgemm('T','N', nbf, nbf, nbf, 1.0_dp, mo_a, nbf, w2, nbf, 0.0_dp, w3, nbf) foo = w3(1:nocc_a,1:nocc_a) fvv = w3(nocc_a+1:nbf,nocc_a+1:nbf) k = 0 do i = nocc_a+1, nbf do a = 1, nocc_a k = k + 1 grad(k) = w3(i,a) h_diag(k) = ( w3(i,i) - w3(a,a) ) end do end do ! ---- UHF beta block ---- call unpack_matrix(fock_ao(:,2), w1) w2 = 0.0_dp w3 = 0.0_dp call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, w1, nbf, mo_b, nbf, 0.0_dp, w2, nbf) call dgemm('T','N', nbf, nbf, nbf, 1.0_dp, mo_b, nbf, w2, nbf, 0.0_dp, w3, nbf) foo_b = w3(1:nocc_b,1:nocc_b) fvv_b = w3(nocc_b+1:nbf,nocc_b+1:nbf) k = self%nvir_a * nocc_a do i = nocc_b+1, nbf do a = 1, nocc_b k = k + 1 grad(k) = w3(i,a) h_diag(k) = ( w3(i,i) - w3(a,a) ) end do end do case (SCF_ROHF) ! ---- ROHF α block ---- call unpack_matrix(fock_ao(:,1), w1) w2 = 0.0_dp call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, w1, nbf, mo_a, nbf, 0.0_dp, w2, nbf) call dgemm('T','N', nbf, nbf, nbf, 1.0_dp, mo_a, nbf, w2, nbf, 0.0_dp, w1, nbf) foo = w1(1:nocc_a,1:nocc_a) fvv = w1(nocc_a+1:nbf,nocc_a+1:nbf) call unpack_matrix(fock_ao(:,2), w3) w2 = 0.0_dp call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, w3, nbf, mo_b, nbf, 0.0_dp, w2, nbf) call dgemm('T','N', nbf, nbf, nbf, 1.0_dp, mo_b, nbf, w2, nbf, 0.0_dp, w3, nbf) foo_b = w3(1:nocc_b,1:nocc_b) fvv_b = w3(nocc_b+1:nbf,nocc_b+1:nbf) k = 0 do i = nocc_b+1, nocc_a do a = 1, nocc_b k = k + 1 grad(k) = w3(i, a) h_diag(k) = w3(i,i) - w3(a,a) end do end do do i = nocc_a+1, nbf do a = 1, nocc_b k = k + 1 grad(k) = w3(i, a) + w1(i, a) h_diag(k) = (w3(i,i) - w3(a,a)) + (w1(i,i) - w1(a,a)) end do end do do i = nocc_a+1, nbf do a = nocc_b+1, nocc_a k = k + 1 grad(k) = w1(i, a) h_diag(k) = w1(i,i) - w1(a,a) end do end do case default error stop 'calc_g_h: unsupported scftype' end select end associate end subroutine calc_g_h !> @brief Apply the Hessian/operator to a trial vector (H·x) for TRAH. !> @detail Maps trial rotations `x` (RHF/UHF/ROHF) to AO-space density !> perturbations, calls `get_response_packed` to obtain the !> packed Coulomb/exchange(+XC) response, transforms back to MO, !> and assembles the resulting vector `x2` in rotation space. !> @param[inout] self TRAH converger (uses work arrays, MO blocks). !> @param[inout] infos System/control information (basis, DFT flags, grid). !> @param[in] x Trial vector (packed rotations; α then β for UHF). !> @param[out] x2 Result of operator application (same layout as x). !> @author Mohsen Mazaherifar !> @date August 2025 subroutine calc_h_op(self, infos, x, x2) use precision, only: dp use types, only: information use mathlib, only: pack_matrix, unpack_matrix use scf_addons, only: get_response_packed implicit none class(trah_converger), intent(inout) :: self class(information), intent(inout), target :: infos real(dp), intent(in) :: x(:) ! length nocc*nvir (α then β for UHF) real(dp), intent(out) :: x2(:) ! same length integer :: nbf, nocc_a, nocc_b, nvir_a, nvir_b integer :: i, a, k associate( nbf => self%nbf, & nocc_a => self%nocc_a, nocc_b => self%nocc_b, & nvir_a => self%nvir_a, nvir_b => self%nvir_b, & work1 => self%work1, & work2 => self%work2, & work3 => self%work3, & fock_ao => self%fock_ao,& mo => self%mo_a, & mo_b => self%mo_b, & dm => self%dm, pfock => self%pfock, dm_tri => self%dm_tri, v => self%v, & foo => self%foo_a, fvv => self%fvv_a, xmat => self%xmat_a, x2mat => self%x2mat_a, & foo_b => self%foo_b, fvv_b => self%fvv_b, xmat_b => self%xmat_b, x2mat_b => self%x2mat_b ) select case (self%scf_type) !============================== RHF ============================== case(SCF_RHF) k = 0 do i = 1, nvir_a do a = 1, nocc_a k= k+1 xmat(i,a) = x(k) end do end do call dgemm('N','N', nvir_a, nocc_a, nvir_a, & 1.0_dp, fvv, nvir_a, & xmat, nvir_a, & 0.0_dp, x2mat, nvir_a) call dgemm('N','N', nvir_a, nocc_a, nocc_a, & -1.0_dp, xmat, nvir_a, & foo, nocc_a, & 1.0_dp, x2mat, nvir_a) dm = 0.0_dp work2 = 0 call dgemm('N','N', nbf, nocc_a, nvir_a, & 2.0_dp, mo(:, nocc_a+1:nbf), nbf, & xmat, nvir_a, & 0.0_dp, work2, nbf) call dgemm('N','T', nbf, nbf, nocc_a, & 1.0_dp, work2, nbf, & mo(:, 1:nocc_a), nbf, & 0.0_dp, work3, nbf) do i = 1, nbf do a = 1, nbf dm(i,a) = work3(i,a) + work3(a,i) end do end do call pack_matrix(dm,dm_tri(:,1)) call get_response_packed(infos%basis, infos, self%molGrid, mo, dm_tri, pfock) call unpack_matrix(pfock(:,1), v) work2 = 0 call dgemm('T','N', nbf, nbf, nbf, & 1.0_dp, mo, nbf, & v , nbf, & 0.0_dp, work2, nbf) work3 = 0 call dgemm('N','N', nbf, nbf, nbf, & 1.0_dp, work2, nbf, & mo, nbf, & 0.0_dp, work3, nbf) x2mat = x2mat + work3(nocc_a+1:,1:nocc_a) k = 0 do i = 1, nvir_a do a = 1, nocc_a k= k+1 x2(k) = 2*x2mat(i,a) end do end do !============================== UHF ============================== case (scf_uhf) ! ---- α channel ---- k = 0 do i = 1, nvir_a do a = 1, nocc_a k= k+1 xmat(i,a) = x(k) end do end do call dgemm('N','N', nvir_a, nocc_a, nvir_a, & 1.0_dp, fvv, nvir_a, & xmat, nvir_a, & 0.0_dp, x2mat, nvir_a) call dgemm('N','N', nvir_a, nocc_a, nocc_a, & -1.0_dp, xmat, nvir_a, & foo, nocc_a, & 1.0_dp, x2mat, nvir_a) dm = 0.0_dp work2 = 0 call dgemm('N','N', nbf, nocc_a, nvir_a, & 1.0_dp, mo(:, nocc_a+1:nbf), nbf, & xmat, nvir_a, & 0.0_dp, work2, nbf) call dgemm('N','T', nbf, nbf, nocc_a, & 1.0_dp, work2, nbf, & mo(:, 1:nocc_a), nbf, & 0.0_dp, work3, nbf) do i = 1, nbf do a = 1, nbf dm(i,a) = work3(i,a) + work3(a,i) end do end do call pack_matrix(dm,dm_tri(:,1)) k = nvir_a*nocc_a do i = 1, nvir_b do a = 1, nocc_b k= k+1 xmat_b(i,a) = x(k) end do end do call dgemm('N','N', nvir_b, nocc_b, nvir_b, & 1.0_dp, fvv_b, nvir_b, & xmat_b, nvir_b, & 0.0_dp, x2mat_b, nvir_b) call dgemm('N','N', nvir_b, nocc_b, nocc_b, & -1.0_dp, xmat_b, nvir_b, & foo_b, nocc_b, & 1.0_dp, x2mat_b, nvir_b) dm = 0.0_dp work2 = 0 call dgemm('N','N', nbf, nocc_b, nvir_b, & 1.0_dp, mo_b(:, nocc_b+1:nbf), nbf, & xmat_b, nvir_b, & 0.0_dp, work2, nbf) call dgemm('N','T', nbf, nbf, nocc_b, & 1.0_dp, work2, nbf, & mo_b(:, 1:nocc_b), nbf, & 0.0_dp, work3, nbf) do i = 1, nbf do a = 1, nbf dm(i,a) = work3(i,a) + work3(a,i) end do end do call pack_matrix(dm,dm_tri(:,2)) ! end of dm calculation call get_response_packed(infos%basis, infos, self%molGrid, mo, dm_tri, pfock, mo_b) ! alpha x2mat call unpack_matrix(pfock(:,1), v) work2 = 0 call dgemm('T','N', nbf, nbf, nbf, & 1.0_dp, mo, nbf, & v , nbf, & 0.0_dp, work2, nbf) work3 = 0 call dgemm('N','N', nbf, nbf, nbf, & 1.0_dp, work2, nbf, & mo, nbf, & 0.0_dp, work3, nbf) x2mat = x2mat + work3(nocc_a+1:,1:nocc_a) ! beta x2mat call unpack_matrix(pfock(:,2), v) work2 = 0 call dgemm('T','N', nbf, nbf, nbf, & 1.0_dp, mo_b, nbf, & v , nbf, & 0.0_dp, work2, nbf) work3 = 0 call dgemm('N','N', nbf, nbf, nbf, & 1.0_dp, work2, nbf, & mo_b, nbf, & 0.0_dp, work3, nbf) x2mat_b = x2mat_b + work3(nocc_b+1:,1:nocc_b) k = 0 do i = 1, nvir_a do a = 1, nocc_a k= k+1 x2(k) = x2mat(i,a) end do end do do i = 1, nvir_b do a = 1, nocc_b k= k+1 x2(k) = x2mat_b(i,a) end do end do !============================== ROHF ============================== case (scf_ROHF) ! alpha call unpack_rohf_trial(x, xmat, xmat_b, nbf, nocc_a, nocc_b) call dgemm('N','N', nvir_a, nocc_a, nvir_a, & 1.0_dp, fvv, nvir_a, & xmat, nvir_a, & 0.0_dp, x2mat, nvir_a) call dgemm('N','N', nvir_a, nocc_a, nocc_a, & -1.0_dp, xmat, nvir_a, & foo, nocc_a, & 1.0_dp, x2mat, nvir_a) dm = 0.0_dp work2 = 0 call dgemm('N','N', nbf, nocc_a, nvir_a, & 1.0_dp, mo(:, nocc_a+1:nbf), nbf, & xmat, nvir_a, & 0.0_dp, work2, nbf) call dgemm('N','T', nbf, nbf, nocc_a, & 1.0_dp, work2, nbf, & mo(:, 1:nocc_a), nbf, & 0.0_dp, work3, nbf) do i = 1, nbf do a = 1, nbf dm(i,a) = work3(i,a) + work3(a,i) end do end do call pack_matrix(dm,dm_tri(:,1)) ! beta call dgemm('N','N', nvir_b, nocc_b, nvir_b, & 1.0_dp, fvv_b, nvir_b, & xmat_b, nvir_b, & 0.0_dp, x2mat_b, nvir_b) call dgemm('N','N', nvir_b, nocc_b, nocc_b, & -1.0_dp, xmat_b, nvir_b, & foo_b, nocc_b, & 1.0_dp, x2mat_b, nvir_b) dm = 0.0_dp work2 = 0 call dgemm('N','N', nbf, nocc_b, nvir_b, & 1.0_dp, mo_b(:, nocc_b+1:nbf), nbf, & xmat_b, nvir_b, & 0.0_dp, work2, nbf) call dgemm('N','T', nbf, nbf, nocc_b, & 1.0_dp, work2, nbf, & mo_b(:, 1:nocc_b), nbf, & 0.0_dp, work3, nbf) do i = 1, nbf do a = 1, nbf dm(i,a) = work3(i,a) + work3(a,i) end do end do call pack_matrix(dm,dm_tri(:,2)) ! end of dm calculation call get_response_packed(infos%basis, infos, self%molGrid, mo, dm_tri, pfock, mo_b) ! alpha x2mat call unpack_matrix(pfock(:,1), v) work2 = 0 call dgemm('T','N', nbf, nbf, nbf, & 1.0_dp, mo, nbf, & v , nbf, & 0.0_dp, work2, nbf) work3 = 0 call dgemm('N','N', nbf, nbf, nbf, & 1.0_dp, work2, nbf, & mo, nbf, & 0.0_dp, work3, nbf) x2mat = x2mat + work3(nocc_a+1:,1:nocc_a) ! beta x2mat call unpack_matrix(pfock(:,2), v) work2 = 0 call dgemm('T','N', nbf, nbf, nbf, & 1.0_dp, mo_b, nbf, & v , nbf, & 0.0_dp, work2, nbf) work3 = 0 call dgemm('N','N', nbf, nbf, nbf, & 1.0_dp, work2, nbf, & mo_b, nbf, & 0.0_dp, work3, nbf) x2mat_b = x2mat_b + work3(nocc_b+1:,1:nocc_b) call pack_rohf_trial(x2,x2mat,x2mat_b, nbf, nocc_a, nocc_b) end select end associate end subroutine calc_h_op !> @brief Pack ROHF α/β trial matrices into a single rotation vector. !> @detail Packs S↔D, V↔D, and V↔S blocks according to the ROHF layout !> (assuming nocc_a ≥ nocc_b), producing the linearized step. !> @param[inout] x Output vector of packed rotations. !> @param[in] xa α-block trial matrix (V×Occ_α). !> @param[in] xb β-block trial matrix (V×Occ_β with offset). !> @param[in] nbf Number of basis functions. !> @param[in] nocc_a Number of α occupied orbitals. !> @param[in] nocc_b Number of β occupied orbitals. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine pack_rohf_trial(x, xa, xb, nbf, nocc_a, nocc_b) use iso_fortran_env, only: dp => real64 implicit none real(dp), intent(inout) :: x(:) real(dp), intent(in) :: xa(:,:), xb(:,:) integer, intent(in) :: nbf, nocc_a, nocc_b integer :: nvir_a, nvir_b, offset, npar, k, iv, a nvir_a = nbf - nocc_a nvir_b = nbf - nocc_b offset = nocc_a - nocc_b npar = nocc_b*nvir_b + offset*nvir_a x = 0.0_dp k = 0 if (offset > 0) then do iv = 1, offset do a = 1, nocc_b k = k + 1 x(k) = xb(iv, a) end do end do end if do iv = 1, nvir_a do a = 1, nocc_b k = k + 1 x(k) = xa(iv, a) + xb(offset + iv, a) end do end do if (offset > 0) then do iv = 1, nvir_a do a = 1, offset k = k + 1 x(k) = xa(iv, nocc_b + a) end do end do end if end subroutine !> @brief Unpack ROHF packed rotation vector into α/β trial matrices. !> @detail Inverse of `pack_rohf_trial`. Reconstructs α and β trial blocks !> from the linearized ROHF step layout (nocc_a ≥ nocc_b). !> @param[in] x Input packed rotation vector. !> @param[out] xa α-block trial matrix (V×Occ_α). !> @param[out] xb β-block trial matrix (V×Occ_β with offset). !> @param[in] nbf Number of basis functions. !> @param[in] nocc_a Number of α occupied orbitals. !> @param[in] nocc_b Number of β occupied orbitals. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine unpack_rohf_trial(x, xa, xb, nbf, nocc_a, nocc_b) use precision, only: dp implicit none real(dp), intent(in) :: x(:) real(dp), intent(out) :: xa(:,:), xb(:,:) integer, intent(in) :: nbf, nocc_a, nocc_b integer :: nvir_a, nvir_b, ndocc, offset integer :: k, iv, a nvir_a = nbf - nocc_a nvir_b = nbf - nocc_b ndocc = nocc_b offset = nocc_a - nocc_b xa = 0.0_dp xb = 0.0_dp k = 0 if (offset > 0) then do iv = 1, offset do a = 1, nocc_b k = k + 1 xb(iv, a) = x(k) end do end do end if do iv = 1, nvir_a do a = 1, nocc_b k = k + 1 xa(iv, a) = x(k) xb(offset + iv, a) = x(k) end do end do if (offset > 0) then do iv = 1, nvir_a do a = 1, offset k = k + 1 xa(iv, nocc_b + a) = x(k) end do end do end if end subroutine !> @brief Build a skew-symmetric orbital-rotation generator K from a step. !> @detail Fills K with V↔Occ (and ROHF S↔D, V↔D, V↔S) elements from the !> linearized step vector. Handles RHF/UHF with explicit `nocc`, !> and ROHF using `self`’s spin occupations. Aborts on size mismatch. !> @param[inout] self TRAH converger (uses scf_type and occ/vir sizes). !> @param[in] step Linearized rotation vector. !> @param[inout] K Output skew-symmetric generator (nbf×nbf). !> @param[in,opt] nocc Occupied count for non-ROHF (RHF/UHF) paths. !> @author Mohsen Mazaherifar !> @date August 2025 subroutine skew_sym_k(self, step, K, nocc) use precision, only : dp implicit none class(trah_converger), intent(inout) :: self real(dp), intent(in) :: step(:) real(dp), intent(inout) :: K(:,:) integer, intent(in), optional :: nocc ! used for non-ROHF paths integer :: nbf, nocc_a, nocc_b, nvir_a, nvir_b, scftype integer :: i, a, idx, expected, offset nbf = self%nbf nocc_a = self%nocc_a nocc_b = self%nocc_b nvir_a = self%nvir_a nvir_b = self%nvir_b scftype = self%scf_type if (size(K,1) /= nbf .or. size(K,2) /= nbf) error stop "skew_sym_k: bad K dims" if (nocc_a + nvir_a /= nbf) error stop "skew_sym_k: nocc_a+nvir_a != nbf" if (nocc_b + nvir_b /= nbf) error stop "skew_sym_k: nocc_b+nvir_b != nbf" K = 0.0_dp idx = 0 if (scftype == 3) then ! ---------------- ROHF (single spatial K), assume nocc_a >= nocc_b ---------------- ! Subspaces: D=1..nocc_b, S=nocc_b+1..nocc_a, V=nocc_a+1..nbf ! ! step packing (length npar): ! 1) S<->D (β-only extras): size = nocc_b*(nocc_a - nocc_b) ! 2) V<->D (common spin-sum): size = nocc_b*nvir_a ! 3) V<->S (α-only extras): size = (nocc_a - nocc_b)*nvir_a ! offset = nocc_a - nocc_b expected = nocc_b*offset + nocc_b*nvir_a + offset*nvir_a if (size(step) /= expected) error stop "skew_sym_k(ROHF): step length mismatch" if (offset > 0) then do i = nocc_b+1, nocc_a do a = 1, nocc_b idx = idx + 1 K(i, a) = step(idx) K(a, i) = -step(idx) end do end do end if do i = nocc_a+1, nbf do a = 1, nocc_b idx = idx + 1 K(i, a) = step(idx) K(a, i) = -step(idx) end do end do if (offset > 0) then do i = nocc_a+1, nbf do a = nocc_b+1, nocc_a idx = idx + 1 K(i, a) = step(idx) K(a, i) = -step(idx) end do end do end if else ! ---------------- RHF / UHF path (single block V<->Occ) ---------------- if (.not. present(nocc)) error stop "skew_sym_k: nocc required for non-ROHF" if (nocc < 0 .or. nocc > nbf) error stop "skew_sym_k: nocc out of range" expected = (nbf - nocc) * nocc if (size(step) /= expected) error stop "skew_sym_k: step length mismatch" do i = nocc+1, nbf do a = 1, nocc idx = idx + 1 K(i, a) = step(idx) K(a, i) = -step(idx) end do end do end if if (idx /= size(step)) error stop "skew_sym_k: not all elements consumed" end subroutine !> @brief Rotate MOs by an exponential map of the generator (TRAH step). !> @detail Builds the skew-symmetric generator K from `step`, evaluates a !> truncated matrix exponential (optionally higher-order), re-orthonormalizes !> the transform, and updates MO := MO · exp(K). !> @param[inout] self TRAH converger. !> @param[in] step Linearized rotation vector (packed). !> @param[in] nbf Basis dimension. !> @param[in] nocc Occupied count (used for non-ROHF paths). !> @param[inout] mo MO coefficient matrix to be rotated (nbf×nbf). !> @author Mohsen Mazaherifar !> @date August 2025 subroutine rotate_orbs_trah(self, step, nbf, nocc, mo) implicit none class(trah_converger), intent(inout) :: self real(kind=dp), intent(in) :: step(:) integer, intent(in) :: nbf,nocc real(kind=dp), intent(inout) :: mo(:,:) real(kind=dp), allocatable :: K(:,:) integer :: i, idx, occ, virt, istart logical :: second_term if (all(step == 0.0d0)) return allocate(K(nbf,nbf), source=0.0_dp) self%work1 = 0 self%work2 = 0 idx = 0 second_term = .true. ! ---- Build skew-symmetric K from "step" (same as before) ---- call skew_sym_k(self, step, K, nocc) if (self%scf_type == 3) then! ROHF second_term = .true. end if call exp_scaling(self%work1, K, second_term) call orthonormalize(self%work1, nbf) call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, mo, nbf, self%work1, nbf, 0.0_dp, self%work2, nbf) mo = self%work2 if(allocated(K)) deallocate(K) contains subroutine exp_scaling(G, K, higher_order) implicit none real(kind=dp), intent(out) :: G(:,:) real(kind=dp), intent(in) :: K(:,:) logical, intent(in) :: higher_order real(kind=dp), allocatable :: Kpow(:,:), Tmp(:,:) integer :: i, m integer, parameter :: max_order = 6 ! increase to 8/10 if desired real(kind=dp) :: coef allocate(Kpow(nbf,nbf), source=0.0_dp) allocate(Tmp(nbf,nbf), source=0.0_dp) ! ---- Initialize G = I ---- G = 0.0_dp do i = 1, nbf G(i,i) = 1.0_dp end do ! ---- First-order: G <- I + K (always) ---- G = G + K if (higher_order) then ! Accumulate higher Taylor terms up to max_order using BLAS ! Start from K^1 already in hand; build K^m iteratively. Kpow = K coef = 1.0_dp do m = 2, max_order ! Kpow <- Kpow * K == K^m call dgemm('N','N', nbf, nbf, nbf, 1.0_dp, Kpow, nbf, K, nbf, 0.0_dp, Tmp, nbf) Kpow = Tmp coef = coef / real(m, dp) ! 1/m! (incrementally) G = G + coef * Kpow end do end if deallocate(Tmp, Kpow) end subroutine exp_scaling subroutine orthonormalize(G, nbf) real(kind=dp), intent(inout) :: G(:,:) integer, intent(in) :: nbf integer :: i, j real(kind=dp) :: norm, dot do i = 1, nbf norm = sqrt(dot_product(G(:,i), G(:,i))) call dscal(nbf, 1.0_dp / norm, G(1,i), 1) if (i == nbf) cycle do j = i+1, nbf dot = dot_product(G(:,i), G(:,j)) call daxpy(nbf, -dot, G(1,i), 1, G(1,j), 1) end do end do end subroutine orthonormalize end subroutine rotate_orbs_trah end module scf_converger