LCOV - code coverage report
Current view: top level - src/emd - rt_bse_linearized.F (source / functions) Coverage Total Hit
Test: CP2K Regtests (git:21ef868) Lines: 94.0 % 1012 951
Test Date: 2026-08-14 07:04:57 Functions: 97.1 % 35 34

            Line data    Source code
       1              : !--------------------------------------------------------------------------------------------------!
       2              : !   CP2K: A general program to perform molecular dynamics simulations                              !
       3              : !   Copyright 2000-2026 CP2K developers group <https://cp2k.org>                                   !
       4              : !                                                                                                  !
       5              : !   SPDX-License-Identifier: GPL-2.0-or-later                                                      !
       6              : !--------------------------------------------------------------------------------------------------!
       7              : 
       8              : ! **************************************************************************************************
       9              : !> \brief Routines for the propagation of the linearized RT-BSE equations of motion.
      10              : !>        Propagates the first-order density matrix response Δρ within the active MO window,
      11              : !>        in the Tamm-Dancoff approximation or with the full (A, B) coupling, instead of the
      12              : !>        lesser Green's function propagated by rt_bse. Also provides the Liouvillian eigenvalue
      13              : !>        diagnostic, which builds the Liouvillian by probing the kernel with canonical basis
      14              : !>        vectors and diagonalizes it.
      15              : !> \note  The control is handed directly from cp2k_runs
      16              : !>        The initialization and delta-kick routines are adapted from the full RT-BSE
      17              : !>        propagator in rt_bse.F.
      18              : !> \author Maximilian Graml (03.26)
      19              : !> \author Stepan Marek (09.24) - original RT-BSE routines adapted here
      20              : ! **************************************************************************************************
      21              : 
      22              : MODULE rt_bse_linearized
      23              :    USE cp_cfm_basic_linalg,             ONLY: cp_cfm_gemm,&
      24              :                                               cp_cfm_norm,&
      25              :                                               cp_cfm_scale,&
      26              :                                               cp_cfm_scale_and_add,&
      27              :                                               cp_cfm_transpose
      28              :    USE cp_cfm_diag,                     ONLY: cp_cfm_heevd
      29              :    USE cp_cfm_types,                    ONLY: &
      30              :         cp_cfm_get_info, cp_cfm_get_submatrix, cp_cfm_set_all, cp_cfm_set_element, &
      31              :         cp_cfm_set_submatrix, cp_cfm_to_cfm, cp_cfm_to_fm, cp_cfm_type, cp_fm_to_cfm
      32              :    USE cp_dbcsr_api,                    ONLY: dbcsr_add,&
      33              :                                               dbcsr_copy,&
      34              :                                               dbcsr_get_info,&
      35              :                                               dbcsr_p_type,&
      36              :                                               dbcsr_release,&
      37              :                                               dbcsr_set
      38              :    USE cp_dbcsr_operations,             ONLY: copy_dbcsr_to_fm,&
      39              :                                               copy_fm_to_dbcsr
      40              :    USE cp_fm_basic_linalg,              ONLY: cp_fm_scale,&
      41              :                                               cp_fm_scale_and_add,&
      42              :                                               cp_fm_transpose
      43              :    USE cp_fm_types,                     ONLY: cp_fm_create,&
      44              :                                               cp_fm_get_diag,&
      45              :                                               cp_fm_get_info,&
      46              :                                               cp_fm_release,&
      47              :                                               cp_fm_set_all,&
      48              :                                               cp_fm_to_fm_submat_general,&
      49              :                                               cp_fm_type
      50              :    USE cp_log_handling,                 ONLY: cp_get_default_logger,&
      51              :                                               cp_logger_type
      52              :    USE cp_output_handling,              ONLY: cp_add_iter_level,&
      53              :                                               cp_iterate,&
      54              :                                               cp_print_key_finished_output,&
      55              :                                               cp_print_key_unit_nr,&
      56              :                                               cp_rm_iter_level
      57              :    USE dbt_api,                         ONLY: dbt_copy_matrix_to_tensor
      58              :    USE force_env_methods,               ONLY: force_env_calc_energy_force
      59              :    USE force_env_types,                 ONLY: force_env_type
      60              :    USE input_constants,                 ONLY: rtp_bse_ham_g0w0,&
      61              :                                               use_mom_ref_zero,&
      62              :                                               use_rt_restart
      63              :    USE kinds,                           ONLY: dp
      64              :    USE machine,                         ONLY: m_walltime
      65              :    USE mathconstants,                   ONLY: twopi
      66              :    USE moments_utils,                   ONLY: get_reference_point
      67              :    USE parallel_gemm_api,               ONLY: parallel_gemm
      68              :    USE physcon,                         ONLY: evolt,&
      69              :                                               seconds
      70              :    USE post_scf_bandstructure_types,    ONLY: post_scf_bandstructure_type
      71              :    USE qs_environment_types,            ONLY: get_qs_env
      72              :    USE qs_moments,                      ONLY: build_local_moment_matrix
      73              :    USE rpa_gw_kpoints_util,             ONLY: cp_cfm_power
      74              :    USE rt_bse,                          ONLY: get_hartree,&
      75              :                                               get_sigma,&
      76              :                                               init_hartree,&
      77              :                                               initialize_rtbse_env,&
      78              :                                               propagate_density,&
      79              :                                               rho_metric
      80              :    USE rt_bse_io,                       ONLY: &
      81              :         check_restart_eps_consistency, output_field, output_moments, output_mos_contravariant, &
      82              :         output_restart_linearized, print_timestep_info, read_restart_C, read_restart_density, &
      83              :         read_restart_info, read_restart_trace
      84              :    USE rt_bse_ri_rs,                    ONLY: compute_hartree_ri_rs,&
      85              :                                               compute_hartree_ri_rs_complex,&
      86              :                                               compute_hartree_ri_rs_from_diag,&
      87              :                                               rt_bse_ri_rs_ensure_V_grid,&
      88              :                                               rt_bse_ri_rs_ensure_W0_grid
      89              :    USE rt_bse_types,                    ONLY: create_rtbse_env,&
      90              :                                               release_rtbse_env,&
      91              :                                               rtbse_env_type
      92              :    USE rt_propagation_output,           ONLY: print_ft
      93              : #include "../base/base_uses.f90"
      94              : 
      95              :    IMPLICIT NONE
      96              : 
      97              :    PRIVATE
      98              : 
      99              :    CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'rt_bse_linearized'
     100              : 
     101              :    ! build_shared_sex_and_hartree input-convention selector. The mask choice also fixes the input
     102              :    ! Hermiticity, which gates the Hartree imaginary channel (Im computed iff non-Hermitian = OV only).
     103              :    INTEGER, PARAMETER, PRIVATE :: kernel_input_ov = 1, kernel_input_ovvo = 2, kernel_input_full = 3
     104              : 
     105              :    PUBLIC :: run_propagation_linearized_bse
     106              : 
     107              : CONTAINS
     108              : 
     109              : ! **************************************************************************************************
     110              : !> \brief Runs the electron-only real time propagation of the linearized BSE
     111              : !> \param force_env Force environment data, entry point of the calculation
     112              : ! **************************************************************************************************
     113           52 :    SUBROUTINE run_propagation_linearized_bse(force_env)
     114              :       TYPE(force_env_type), POINTER                      :: force_env
     115              : 
     116              :       CHARACTER(len=*), PARAMETER :: routineN = 'run_propagation_linearized_bse'
     117              : 
     118              :       INTEGER                                            :: handle, i, j
     119              :       REAL(kind=dp)                                      :: t_phys, t_start, timestep_walltime, &
     120              :                                                             timestep_walltime_start
     121              :       REAL(kind=dp), DIMENSION(2)                        :: enum_im, enum_re
     122           52 :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_lab
     123              :       TYPE(cp_logger_type), POINTER                      :: logger
     124              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     125              : 
     126              :       ! Per-spin (alpha/beta) electron numbers; only 1:n_spin entries are used.
     127              : 
     128           52 :       CALL timeset(routineN, handle)
     129              : 
     130              :       CALL cp_warn(__LOCATION__, &
     131              :                    "Linearized RT-BSE is under active development. Make sure you understand "// &
     132           52 :                    "the method and validate results before using it for production calculations.")
     133              : 
     134              :       ! To Do: Bibliography information
     135              : 
     136           52 :       logger => cp_get_default_logger()
     137              : 
     138              :       ! Run the initial SCF calculation / read SCF restart information
     139           52 :       CALL force_env_calc_energy_force(force_env, calc_force=.FALSE., consistent_energies=.FALSE.)
     140              : 
     141              :       ! Allocate all persistant storage and read input that does not need further processing
     142           52 :       CALL create_rtbse_env(rtbse_env, force_env, linearized=.TRUE.)
     143              : 
     144              :       ! Restart phase 1a: read sim_start + the original run's dt (from the trace header) BEFORE
     145              :       ! ENFORCE_MAX_DT, so the continuation inherits that dt instead of a window-dependent one.
     146           52 :       IF (rtbse_env%dft_control%rtp_control%initial_wfn == use_rt_restart) THEN
     147           14 :          CALL read_restart_info(rtbse_env)
     148              :       END IF
     149              : 
     150           52 :       CALL initialize_maximum_timestep(rtbse_env)
     151              : 
     152              :       ! Restart phase 1b: load the trace prefix now that ENFORCE_MAX_DT has sized the trace arrays.
     153           52 :       IF (rtbse_env%dft_control%rtp_control%initial_wfn == use_rt_restart) THEN
     154           14 :          IF (rtbse_env%sim_start >= rtbse_env%sim_nsteps) THEN
     155            0 :             CPABORT("RT_RESTART: restart step >= STEPS - increase MOTION%MD%STEPS")
     156              :          END IF
     157           14 :          CALL read_restart_trace(rtbse_env)
     158              :       END IF
     159              : 
     160           52 :       CALL print_linrtbse_header_info(rtbse_env)
     161              : 
     162              :       ! Build the truncated MO coefficient slabs C_active(:, first_active_mo..last_active_mo)
     163              :       ! used by all AO<->MO transforms in the linearized path.
     164           52 :       CALL populate_C_active(rtbse_env)
     165              : 
     166              :       ! Initiate iteration level "MD" in order to copy the structure of other RTP codes
     167           52 :       CALL cp_add_iter_level(logger%iter_info, "MD")
     168              :       ! Initialize non-trivial values
     169              :       !  - calculates the moment operators
     170           52 :       CALL initialize_moments(rtbse_env)
     171              :       !  - populates overlap and inverse overlap matrices
     172           52 :       CALL initialize_rtbse_env(rtbse_env)
     173              : 
     174              :       !  - populates the fresh SCF density matrix rho^0 (and the rho_orig reference for delta rho)
     175           52 :       CALL initialize_density_matrix(rtbse_env)
     176              : 
     177              :       ! Restart phase 2: overwrite rho from the lab-frame restart files, bridge into this run's MO
     178              :       ! gauge, then enter this run's rotating frame (rotate_rho_phase is a no-op when omega_shift=0)
     179           52 :       IF (rtbse_env%dft_control%rtp_control%initial_wfn == use_rt_restart) THEN
     180           14 :          CALL read_restart_density(rtbse_env)
     181           14 :          IF (rtbse_env%restart_extracted) THEN
     182           14 :             CALL apply_restart_basis_bridge(rtbse_env)
     183           14 :             t_start = REAL(rtbse_env%sim_start, dp)*rtbse_env%sim_dt
     184           30 :             DO i = 1, rtbse_env%n_spin
     185           30 :                CALL rotate_rho_phase(rtbse_env, rtbse_env%rho(i), i, -rtbse_env%omega_shift*t_start)
     186              :             END DO
     187              :          END IF
     188              :       END IF
     189              :       !  - calculates/populates the G0W0/KS Hamiltonian, respectively
     190           52 :       CALL initialize_singleparticle_hamiltonian(rtbse_env)
     191              :       ! Restart Hamiltonian-consistency heads-up: eps_active exists only now, so compare here
     192           52 :       CALL check_restart_eps_consistency(rtbse_env)
     193              :       ! Transform initial density matrix to AO basis for use in Hartree and self-energy calculations
     194          112 :       DO i = 1, rtbse_env%n_spin
     195          112 :          CALL transform_mo_to_ao_contravariant_cfm(rtbse_env, rtbse_env%rho_orig(i), rtbse_env%rho_ao_scratch(i), i)
     196              :       END DO
     197              :       !  - calculates the Hartree reference potential
     198          112 :       DO i = 1, rtbse_env%n_spin
     199          112 :          CALL cp_cfm_set_all(rtbse_env%ham_reference(i), CMPLX(0.0_dp, 0.0_dp, kind=dp))
     200              :       END DO
     201           52 :       CALL initialize_hartree_potential(rtbse_env)
     202              :       !  - calculates the SEX reference self-energy
     203           52 :       CALL initialize_sex_selfenergy(rtbse_env)
     204              : 
     205              :       ! Liouvillian eigenvalue diagnostic (one-shot at init, TDA or ABBA via dispatcher).
     206              :       ! Detached from the propagator; safe to call after the reference-init routines.
     207           52 :       IF (rtbse_env%diagnose_liouvillian_eig) THEN
     208           52 :          CALL diagnose_liouvillian_eigenvalues(rtbse_env)
     209              :       END IF
     210              : 
     211              :       ! Setup the time based on the starting step
     212              :       ! Assumes identical dt between two runs
     213           52 :       rtbse_env%sim_time = REAL(rtbse_env%sim_start, dp)*rtbse_env%sim_dt
     214           52 :       NULLIFY (rho_lab)
     215              :       ! Output 0 time moments and field
     216           52 :       IF (.NOT. rtbse_env%restart_extracted) THEN
     217           38 :          CALL output_field(rtbse_env)
     218           38 :          CALL build_rho_lab(rtbse_env, rtbse_env%rho, rtbse_env%sim_time, rho_lab)
     219           38 :          CALL output_moments(rtbse_env, rho_lab)
     220              :       END IF
     221              : 
     222              :       ! Do not apply the delta kick if we are doing a restart calculation
     223           52 :       IF (rtbse_env%dft_control%rtp_control%apply_delta_pulse .AND. (.NOT. rtbse_env%restart_extracted)) THEN
     224           38 :          CALL apply_delta_pulse_MO(rtbse_env)
     225              :       END IF
     226              : 
     227              :       ! ********************** Start the time loop **********************
     228              :       ! NOTE : Time-loop starts at index sim_start = 0, unless restarted or configured otherwise
     229          762 :       DO i = rtbse_env%sim_start, rtbse_env%sim_nsteps - 1
     230          710 :          timestep_walltime_start = m_walltime()
     231              : 
     232              :          ! Update the simulation time
     233          710 :          rtbse_env%sim_time = REAL(i, dp)*rtbse_env%sim_dt
     234          710 :          rtbse_env%sim_step = i
     235              : 
     236          710 :          CALL solve_rk4_timestep(rtbse_env, rtbse_env%rho, rtbse_env%rho_new)
     237              :          CALL get_electron_number_MO(rtbse_env, rtbse_env%rho_new, &
     238          710 :                                      enum_re(1:rtbse_env%n_spin), enum_im(1:rtbse_env%n_spin))
     239          710 :          timestep_walltime = m_walltime() - timestep_walltime_start
     240          710 :          CALL print_timestep_info(rtbse_env, i, enum_re(1:rtbse_env%n_spin), step_walltime=timestep_walltime)
     241          710 :          CALL cp_iterate(logger%iter_info, iter_nr=i, last=(i == rtbse_env%sim_nsteps - 1))
     242              : 
     243              :          ! Update rho
     244         1540 :          DO j = 1, rtbse_env%n_spin
     245         1540 :             CALL cp_cfm_to_cfm(rtbse_env%rho_new(j), rtbse_env%rho(j))
     246              :          END DO
     247              :          ! Print the updated field
     248          710 :          CALL output_field(rtbse_env)
     249              :          ! rho is the rotating-frame density at physical time t_phys = (i+1)*dt.
     250              :          ! Build a lab-frame copy once and feed it to all observable/restart sinks.
     251          710 :          t_phys = REAL(i + 1, dp)*rtbse_env%sim_dt
     252          710 :          CALL build_rho_lab(rtbse_env, rtbse_env%rho, t_phys, rho_lab)
     253              :          ! If needed, print out the density matrix in MO basis
     254          710 :          CALL output_mos_contravariant(rtbse_env, rho_lab, rtbse_env%rho_section)
     255              :          ! Also handles outputting to memory
     256          710 :          CALL output_moments(rtbse_env, rho_lab)
     257              :          ! Output restart files, so that the restart resumes at the step recorded in .info
     258          762 :          CALL output_restart_linearized(rtbse_env, rho_lab)
     259              :       END DO
     260              :       ! ********************** End the time loop **********************
     261              : 
     262           52 :       CALL cp_rm_iter_level(logger%iter_info, "MD")
     263              : 
     264              :       ! Carry out the FT
     265              :       CALL print_ft(rtbse_env%rtp_section, &
     266              :                     rtbse_env%moments_trace, &
     267              :                     rtbse_env%time_trace, &
     268              :                     rtbse_env%field_trace, &
     269              :                     rtbse_env%dft_control%rtp_control, &
     270           52 :                     info_opt=rtbse_env%unit_nr)
     271              : 
     272              :       ! Deallocate everything
     273           52 :       CALL release_rtbse_env(rtbse_env)
     274              : 
     275           52 :       CALL timestop(handle)
     276           52 :    END SUBROUTINE run_propagation_linearized_bse
     277              : 
     278              : ! **************************************************************************************************
     279              : !> \brief Computes the analytic RK4 stability bound t* = 2√2 / Ω_max (a.u.) from the largest active
     280              : !>        GW/KS gap, and (TDA + first-peak) the rotating-frame shift Ω_0 = ε^ai_min.
     281              : !>        Writes the timestep diagnostics to stdout; optionally rewrites TIMESTEP/STEPS under
     282              : !>        ENFORCE_MAX_DT; on restart it inherits the original dt from the trace and only rescales STEPS.
     283              : !> \param rtbse_env Entry point - rtbse environment
     284              : ! **************************************************************************************************
     285           52 :    SUBROUTINE initialize_maximum_timestep(rtbse_env)
     286              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     287              : 
     288              :       CHARACTER(len=*), PARAMETER :: routineN = 'initialize_maximum_timestep'
     289              : 
     290              :       CHARACTER(len=256)                                 :: hint_msg
     291              :       INTEGER                                            :: handle, i_first, i_last, ispin, &
     292              :                                                             n_steps_new, n_steps_old
     293              :       REAL(kind=dp) :: eps_max_ai, eps_min_ai, eps_occ_max, eps_occ_min, eps_virt_max, &
     294              :          eps_virt_min, ev_tmp, grace_factor, omega_max, sim_dt_as, total_time
     295              : 
     296           52 :       CALL timeset(routineN, handle)
     297              : 
     298           52 :       i_first = rtbse_env%first_active_mo
     299           52 :       i_last = rtbse_env%last_active_mo
     300              : 
     301           52 :       IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     302              :          omega_max = MAXVAL(rtbse_env%bs_env%eigenval_G0W0(i_first:i_last, :, :)) - &
     303         1760 :                      MINVAL(rtbse_env%bs_env%eigenval_G0W0(i_first:i_last, :, :))
     304              :       ELSE
     305              :          omega_max = MAXVAL(rtbse_env%bs_env%eigenval_scf_Gamma(i_first:i_last, :)) - &
     306            0 :                      MINVAL(rtbse_env%bs_env%eigenval_scf_Gamma(i_first:i_last, :))
     307              :       END IF
     308              : 
     309              :       ! First-peak shift (TDA only): set Ω_0 = eps_min_ai so the lowest
     310              :       ! active OV mode oscillates at zero frequency in the rotating frame
     311              :       ! (RK4-exact for peak 1). omega_max is the full active OV width
     312              :       ! Delta = eps_max_ai - eps_min_ai, where eps_ai = eps_a - eps_i runs
     313              :       ! over the active OV pairs only (i in active occupied, a in active
     314              :       ! virtual).
     315           52 :       rtbse_env%omega_shift = 0.0_dp
     316           52 :       IF (rtbse_env%tda_active .AND. rtbse_env%tda_shift_to_first_peak) THEN
     317            6 :          eps_occ_min = HUGE(0.0_dp)
     318            6 :          eps_occ_max = -HUGE(0.0_dp)
     319            6 :          eps_virt_min = HUGE(0.0_dp)
     320            6 :          eps_virt_max = -HUGE(0.0_dp)
     321           12 :          DO ispin = 1, rtbse_env%n_spin
     322              :             ! Active occupied window: first_active_mo .. n_occ(ispin)
     323            6 :             IF (rtbse_env%n_occ(ispin) >= i_first) THEN
     324            6 :                IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     325           18 :                   ev_tmp = MINVAL(rtbse_env%bs_env%eigenval_G0W0(i_first:rtbse_env%n_occ(ispin), :, ispin))
     326            6 :                   eps_occ_min = MIN(eps_occ_min, ev_tmp)
     327           18 :                   ev_tmp = MAXVAL(rtbse_env%bs_env%eigenval_G0W0(i_first:rtbse_env%n_occ(ispin), :, ispin))
     328            6 :                   eps_occ_max = MAX(eps_occ_max, ev_tmp)
     329              :                ELSE
     330            0 :                   ev_tmp = MINVAL(rtbse_env%bs_env%eigenval_scf_Gamma(i_first:rtbse_env%n_occ(ispin), ispin))
     331            0 :                   eps_occ_min = MIN(eps_occ_min, ev_tmp)
     332            0 :                   ev_tmp = MAXVAL(rtbse_env%bs_env%eigenval_scf_Gamma(i_first:rtbse_env%n_occ(ispin), ispin))
     333            0 :                   eps_occ_max = MAX(eps_occ_max, ev_tmp)
     334              :                END IF
     335              :             END IF
     336              :             ! Active virtual window: n_occ(ispin)+1 .. last_active_mo
     337           12 :             IF (rtbse_env%n_occ(ispin) < i_last) THEN
     338            6 :                IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     339           78 :                   ev_tmp = MINVAL(rtbse_env%bs_env%eigenval_G0W0(rtbse_env%n_occ(ispin) + 1:i_last, :, ispin))
     340            6 :                   eps_virt_min = MIN(eps_virt_min, ev_tmp)
     341           78 :                   ev_tmp = MAXVAL(rtbse_env%bs_env%eigenval_G0W0(rtbse_env%n_occ(ispin) + 1:i_last, :, ispin))
     342            6 :                   eps_virt_max = MAX(eps_virt_max, ev_tmp)
     343              :                ELSE
     344            0 :                   ev_tmp = MINVAL(rtbse_env%bs_env%eigenval_scf_Gamma(rtbse_env%n_occ(ispin) + 1:i_last, ispin))
     345            0 :                   eps_virt_min = MIN(eps_virt_min, ev_tmp)
     346            0 :                   ev_tmp = MAXVAL(rtbse_env%bs_env%eigenval_scf_Gamma(rtbse_env%n_occ(ispin) + 1:i_last, ispin))
     347            0 :                   eps_virt_max = MAX(eps_virt_max, ev_tmp)
     348              :                END IF
     349              :             END IF
     350              :          END DO
     351              : 
     352            6 :          IF (eps_occ_max > -HUGE(0.0_dp) .AND. eps_virt_min < HUGE(0.0_dp)) THEN
     353            6 :             eps_min_ai = eps_virt_min - eps_occ_max
     354            6 :             eps_max_ai = eps_virt_max - eps_occ_min
     355            6 :             rtbse_env%omega_shift = eps_min_ai
     356            6 :             omega_max = eps_max_ai - eps_min_ai
     357            6 :             IF (rtbse_env%unit_nr > 0) THEN
     358              :                WRITE (rtbse_env%unit_nr, '(A)') &
     359            3 :                   " RTBSE| ---------- First-peak shift diagnostics (TDA, active OV pairs) ----------"
     360              :                WRITE (rtbse_env%unit_nr, '(A,F14.6,A,F14.6)') &
     361            3 :                   " RTBSE|   eps_occ  [eV] min / max =", eps_occ_min*evolt, &
     362            6 :                   " /", eps_occ_max*evolt
     363              :                WRITE (rtbse_env%unit_nr, '(A,F14.6,A,F14.6)') &
     364            3 :                   " RTBSE|   eps_virt [eV] min / max =", eps_virt_min*evolt, &
     365            6 :                   " /", eps_virt_max*evolt
     366              :                WRITE (rtbse_env%unit_nr, '(A,F14.6,A,F14.6)') &
     367            3 :                   " RTBSE|   eps_ai   [eV] min / max =", eps_min_ai*evolt, &
     368            6 :                   " /", eps_max_ai*evolt
     369              :                WRITE (rtbse_env%unit_nr, '(A,F14.6)') &
     370            3 :                   " RTBSE|   omega_shift [eV]        =", rtbse_env%omega_shift*evolt
     371              :                WRITE (rtbse_env%unit_nr, '(A,F14.6)') &
     372            3 :                   " RTBSE|   omega_max   [eV] (full) =", omega_max*evolt
     373              :                WRITE (rtbse_env%unit_nr, '(A)') &
     374            3 :                   " RTBSE| ------------------------------------------------------------------------"
     375              :             END IF
     376              :          ELSE
     377              :             ! Active window has no genuine OV pair - fall back to no shift
     378            0 :             rtbse_env%omega_shift = 0.0_dp
     379            0 :             rtbse_env%tda_shift_to_first_peak = .FALSE.
     380              :          END IF
     381              :       END IF
     382              : 
     383           52 :       rtbse_env%omega_max = omega_max
     384              : 
     385           52 :       IF (omega_max > 0.0_dp) THEN
     386              :          ! t* = 2√2 / Ω_max  (a.u.; imaginary-axis RK4 bound |R(iy)| ≤ 1 at y = 2√2)
     387           52 :          rtbse_env%maximum_timestep = 2.0_dp*SQRT(2.0_dp)/omega_max
     388              :       ELSE
     389              :          CALL cp_abort(__LOCATION__, &
     390              :                        "Error in estimating maximum timestep: largest KS/GW gap is "// &
     391              :                        "non-positive. Check the active MO window (cutoffs) and the "// &
     392            0 :                        "eigenvalues.")
     393              :       END IF
     394              : 
     395           52 :       IF (rtbse_env%sim_dt <= 0.0_dp) THEN
     396              :          CALL cp_abort(__LOCATION__, &
     397              :                        "TIMESTEP must be positive for linearized RT-BSE. Use RTBSE%ENFORCE_MAX_DT "// &
     398            0 :                        "with a positive TIMESTEP to automatically rewrite TIMESTEP and STEPS.")
     399              :       END IF
     400              : 
     401           52 :       n_steps_old = MAX(0, rtbse_env%sim_nsteps)
     402              : 
     403           52 :       IF (rtbse_env%enforce_max_dt) THEN
     404            8 :          total_time = REAL(n_steps_old, dp)*rtbse_env%sim_dt
     405              :          ! Full code needs a grace factor of 4
     406            8 :          IF (rtbse_env%tda_active) THEN
     407              :             grace_factor = 1.0_dp
     408              :          ELSE
     409            0 :             grace_factor = 4.0_dp
     410              :          END IF
     411            8 :          IF (rtbse_env%dft_control%rtp_control%initial_wfn == use_rt_restart .AND. &
     412              :              rtbse_env%sim_dt_restart > 0.0_dp) THEN
     413              :             ! Continuation: dt is frozen in the trace, so inherit it and only rescale the step count
     414              :             ! to the requested window. Recomputing dt from the (longer) window would desync the trace
     415              :             ! time-grid and trip the continuation guard in read_restart_trace.
     416            2 :             rtbse_env%sim_dt = rtbse_env%sim_dt_restart
     417            2 :             n_steps_new = MAX(1, NINT(total_time/rtbse_env%sim_dt))
     418            2 :             rtbse_env%sim_nsteps = n_steps_new
     419            2 :             sim_dt_as = rtbse_env%sim_dt*seconds*1e18_dp
     420              :             WRITE (hint_msg, '(A,F16.4,A,I0,A)') &
     421            2 :                'ENFORCE_MAX_DT on restart: inheriting original TIMESTEP ', sim_dt_as, &
     422            4 :                ' as and setting STEPS to ', n_steps_new, '.'
     423            2 :             CALL cp_hint(__LOCATION__, TRIM(hint_msg))
     424              :             ! The inherited dt was stable in the original run; warn only if this run's stability
     425              :             ! window shrank below it (e.g. the recomputed GW eigenvalues shifted the gap).
     426            2 :             IF (rtbse_env%sim_dt > rtbse_env%maximum_timestep/grace_factor) THEN
     427              :                CALL cp_warn(__LOCATION__, &
     428              :                             "ENFORCE_MAX_DT restart: inherited dt exceeds this run's stability "// &
     429            0 :                             "limit - the recomputed Hamiltonian may make the propagation unstable.")
     430              :             END IF
     431              :          ELSE
     432            6 :             n_steps_new = MAX(1, CEILING(total_time/(rtbse_env%maximum_timestep/grace_factor)))
     433            6 :             rtbse_env%sim_dt = total_time/REAL(n_steps_new, dp)
     434            6 :             rtbse_env%sim_nsteps = n_steps_new
     435            6 :             sim_dt_as = rtbse_env%sim_dt*seconds*1e18_dp
     436              :             WRITE (hint_msg, '(A,F16.4,A,I0,A)') &
     437            6 :                'ENFORCE_MAX_DT enabled. Resetting TIMESTEP to ', sim_dt_as, &
     438           12 :                ' as and STEPS to ', n_steps_new, '.'
     439            6 :             CALL cp_hint(__LOCATION__, TRIM(hint_msg))
     440              :          END IF
     441              :       END IF
     442              : 
     443           52 :       IF (rtbse_env%sim_nsteps /= n_steps_old) THEN
     444            8 :          CALL reallocate_ft_traces(rtbse_env)
     445              :       END IF
     446              : 
     447           52 :       CALL timestop(handle)
     448           52 :    END SUBROUTINE initialize_maximum_timestep
     449              : 
     450              : ! **************************************************************************************************
     451              : !> \brief Reallocate FT trace buffers after ENFORCE_MAX_DT rewrites the step count.
     452              : !> \param rtbse_env Entry point - rtbse environment
     453              : ! **************************************************************************************************
     454            8 :    SUBROUTINE reallocate_ft_traces(rtbse_env)
     455              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     456              : 
     457              :       CHARACTER(len=*), PARAMETER :: routineN = 'reallocate_ft_traces'
     458              : 
     459              :       INTEGER                                            :: handle
     460              : 
     461            8 :       CALL timeset(routineN, handle)
     462              : 
     463            8 :       IF (ASSOCIATED(rtbse_env%moments_trace)) DEALLOCATE (rtbse_env%moments_trace)
     464            8 :       IF (ASSOCIATED(rtbse_env%field_trace)) DEALLOCATE (rtbse_env%field_trace)
     465            8 :       IF (ASSOCIATED(rtbse_env%time_trace)) DEALLOCATE (rtbse_env%time_trace)
     466              : 
     467              :       ALLOCATE (rtbse_env%moments_trace(rtbse_env%n_spin, 3, rtbse_env%sim_nsteps + 1), &
     468          536 :                 source=CMPLX(0.0_dp, 0.0_dp, kind=dp))
     469              :       ALLOCATE (rtbse_env%field_trace(3, rtbse_env%sim_nsteps + 1), &
     470          312 :                 source=CMPLX(0.0_dp, 0.0_dp, kind=dp))
     471           96 :       ALLOCATE (rtbse_env%time_trace(rtbse_env%sim_nsteps + 1), source=0.0_dp)
     472              : 
     473            8 :       CALL timestop(handle)
     474            8 :    END SUBROUTINE reallocate_ft_traces
     475              : 
     476              : ! **************************************************************************************************
     477              : !> \brief Prints the linRTBSE run header to stdout: active-MO window (first/last/count) and the
     478              : !>        occupied/virtual energy cutoffs.
     479              : !> \param rtbse_env Entry point - rtbse environment
     480              : ! **************************************************************************************************
     481           52 :    SUBROUTINE print_linrtbse_header_info(rtbse_env)
     482              :       TYPE(rtbse_env_type)                               :: rtbse_env
     483              : 
     484              :       INTEGER                                            :: ispin, n_steps
     485              :       REAL(kind=dp)                                      :: e_first, e_last, fft_resolution, &
     486              :                                                             nyquist_frequency, total_time
     487              :       TYPE(cp_logger_type), POINTER                      :: logger
     488              : 
     489           52 :       logger => cp_get_default_logger()
     490           52 :       n_steps = MAX(0, rtbse_env%sim_nsteps)
     491           52 :       total_time = REAL(n_steps, dp)*rtbse_env%sim_dt
     492           52 :       fft_resolution = 0.0_dp
     493           52 :       nyquist_frequency = 0.0_dp
     494           52 :       IF (total_time > 0.0_dp) fft_resolution = twopi/total_time
     495           52 :       IF (rtbse_env%sim_dt > 0.0_dp) nyquist_frequency = twopi/(2.0_dp*rtbse_env%sim_dt)
     496              : 
     497           52 :       IF (rtbse_env%unit_nr > 0) THEN
     498           26 :          WRITE (rtbse_env%unit_nr, *) ''
     499              :          WRITE (rtbse_env%unit_nr, '(A)') ' /-----------------------------------------------'// &
     500           26 :             '------------------------------\'
     501              :          WRITE (rtbse_env%unit_nr, '(A)') ' |                                               '// &
     502           26 :             '                              |'
     503              :          WRITE (rtbse_env%unit_nr, '(A)') ' |              Linearized Real Time Bethe-Salpeter Propagation'// &
     504           26 :             '                |'
     505              :          WRITE (rtbse_env%unit_nr, '(A)') ' |                                               '// &
     506           26 :             '                              |'
     507              :          WRITE (rtbse_env%unit_nr, '(A)') ' \-----------------------------------------------'// &
     508           26 :             '------------------------------/'
     509           26 :          WRITE (rtbse_env%unit_nr, *) ''
     510              : 
     511           26 :          WRITE (rtbse_env%unit_nr, '(A18,L62)') ' Apply delta pulse', &
     512           52 :             rtbse_env%dft_control%rtp_control%apply_delta_pulse
     513           26 :          WRITE (rtbse_env%unit_nr, '(A)') ''
     514           26 :          WRITE (rtbse_env%unit_nr, '(A18,L62)') ' Use Tamm-Dancoff approximation', &
     515           52 :             rtbse_env%tda_active
     516           26 :          IF (rtbse_env%tda_active) THEN
     517           18 :             WRITE (rtbse_env%unit_nr, '(A,T71,L10)') ' TDA first-peak shift active', &
     518           36 :                rtbse_env%tda_shift_to_first_peak
     519           18 :             IF (rtbse_env%tda_shift_to_first_peak) THEN
     520            3 :                WRITE (rtbse_env%unit_nr, '(A,T65,F16.6)') ' TDA first-peak shift Omega_0 [eV]:', &
     521            6 :                   rtbse_env%omega_shift*evolt
     522              :             END IF
     523              :          END IF
     524              : 
     525           26 :          WRITE (rtbse_env%unit_nr, '(A)') ''
     526              : 
     527           26 :          WRITE (rtbse_env%unit_nr, '(A,T65,F16.4)') ' Estimated maximum timestep within stability region [as]:', &
     528           52 :             rtbse_env%maximum_timestep*seconds*1e18_dp
     529           26 :          WRITE (rtbse_env%unit_nr, '(A,T65,F16.4)') ' Applied timestep [as]:', &
     530           52 :             rtbse_env%sim_dt*seconds*1e18_dp
     531           26 :          WRITE (rtbse_env%unit_nr, '(A,T71,I10)') ' Number of propagation steps:', n_steps
     532           26 :          WRITE (rtbse_env%unit_nr, '(A,T65,F16.4)') ' Total propagation time [as]:', &
     533           52 :             total_time*seconds*1e18_dp
     534           26 :          WRITE (rtbse_env%unit_nr, '(A,T65,F16.6)') ' Estimated FFT frequency resolution without interpolation [eV]:', &
     535           52 :             fft_resolution*evolt
     536           26 :          WRITE (rtbse_env%unit_nr, '(A,T65,F16.6)') ' Nyquist frequency [eV]:', &
     537           52 :             nyquist_frequency*evolt
     538           26 :          WRITE (rtbse_env%unit_nr, '(A,T65,F16.6)') ' Estimated maximum oscillation frequency (gap-based) [eV]:', &
     539           52 :             rtbse_env%omega_max*evolt
     540              : 
     541              :          ! Active MO window (energy-cutoff truncation) for linearized RT-BSE
     542           26 :          IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     543           26 :             WRITE (rtbse_env%unit_nr, '(A,T75,A6)') ' Active-window single-particle spectrum source:', '  G0W0'
     544              :          ELSE
     545            0 :             WRITE (rtbse_env%unit_nr, '(A,T75,A6)') ' Active-window single-particle spectrum source:', '    KS'
     546              :          END IF
     547           26 :          IF (rtbse_env%rtbse_energy_cutoff_occ > 0.0_dp) THEN
     548            1 :             WRITE (rtbse_env%unit_nr, '(A,T71,F10.3)') ' Active-window occupied energy cutoff [eV]:', &
     549            2 :                rtbse_env%rtbse_energy_cutoff_occ*evolt
     550              :          ELSE
     551           25 :             WRITE (rtbse_env%unit_nr, '(A,T71,A10)') ' Active-window occupied energy cutoff [eV]:', '  disabled'
     552              :          END IF
     553           26 :          IF (rtbse_env%rtbse_energy_cutoff_empty > 0.0_dp) THEN
     554            1 :             WRITE (rtbse_env%unit_nr, '(A,T71,F10.3)') ' Active-window virtual energy cutoff [eV]:', &
     555            2 :                rtbse_env%rtbse_energy_cutoff_empty*evolt
     556              :          ELSE
     557           25 :             WRITE (rtbse_env%unit_nr, '(A,T71,A10)') ' Active-window virtual energy cutoff [eV]:', '  disabled'
     558              :          END IF
     559           26 :          WRITE (rtbse_env%unit_nr, '(A,T71,I10)') ' First active occupied MO index:', rtbse_env%first_active_mo
     560           26 :          WRITE (rtbse_env%unit_nr, '(A,T71,I10)') ' Last active virtual MO index:', rtbse_env%last_active_mo
     561           26 :          WRITE (rtbse_env%unit_nr, '(A,T71,I10)') ' Number of active MOs:', rtbse_env%mo_active
     562           26 :          IF (rtbse_env%active_mo_truncation) THEN
     563              :             ! The window is cut on the DFT axis but propagated on the QP axis, so the QP edges may
     564              :             ! exceed the nominal cutoff. Print both so the window can be checked against the input.
     565            2 :             DO ispin = 1, rtbse_env%n_spin
     566              :                e_first = (rtbse_env%bs_env%eigenval_scf_Gamma(rtbse_env%first_active_mo, ispin) - &
     567            1 :                           rtbse_env%bs_env%eigenval_scf_Gamma(rtbse_env%n_occ(ispin), ispin))*evolt
     568              :                e_last = (rtbse_env%bs_env%eigenval_scf_Gamma(rtbse_env%last_active_mo, ispin) - &
     569            1 :                          rtbse_env%bs_env%eigenval_scf_Gamma(rtbse_env%n_occ(ispin) + 1, ispin))*evolt
     570            1 :                WRITE (rtbse_env%unit_nr, '(A,I1,A,T71,F10.3)') ' Spin ', ispin, &
     571            2 :                   ' first active MO, E - E_HOMO (KS) [eV]:', e_first
     572            1 :                WRITE (rtbse_env%unit_nr, '(A,I1,A,T71,F10.3)') ' Spin ', ispin, &
     573            2 :                   ' last active MO, E - E_LUMO (KS) [eV]:', e_last
     574            2 :                IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     575              :                   e_first = (rtbse_env%bs_env%eigenval_G0W0(rtbse_env%first_active_mo, 1, ispin) - &
     576            1 :                              rtbse_env%bs_env%eigenval_G0W0(rtbse_env%n_occ(ispin), 1, ispin))*evolt
     577              :                   e_last = (rtbse_env%bs_env%eigenval_G0W0(rtbse_env%last_active_mo, 1, ispin) - &
     578            1 :                             rtbse_env%bs_env%eigenval_G0W0(rtbse_env%n_occ(ispin) + 1, 1, ispin))*evolt
     579            1 :                   WRITE (rtbse_env%unit_nr, '(A,I1,A,T71,F10.3)') ' Spin ', ispin, &
     580            2 :                      ' first active MO, E - E_HOMO (QP) [eV]:', e_first
     581            1 :                   WRITE (rtbse_env%unit_nr, '(A,I1,A,T71,F10.3)') ' Spin ', ispin, &
     582            2 :                      ' last active MO, E - E_LUMO (QP) [eV]:', e_last
     583              :                END IF
     584              :             END DO
     585              :          END IF
     586              :       END IF
     587              : 
     588           52 :    END SUBROUTINE print_linrtbse_header_info
     589              : 
     590              : ! **************************************************************************************************
     591              : !> \brief Populates rtbse_env%C_active(i_spin) (n_ao x mo_active) by extracting columns
     592              : !>        first_active_mo..last_active_mo from bs_env%fm_mo_coeff_Gamma(i_spin).
     593              : !> \param rtbse_env RT-BSE environment
     594              : !> \author Maximilian Graml (05.26)
     595              : ! **************************************************************************************************
     596           52 :    SUBROUTINE populate_C_active(rtbse_env)
     597              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     598              : 
     599              :       CHARACTER(len=*), PARAMETER                        :: routineN = 'populate_C_active'
     600              : 
     601              :       INTEGER                                            :: handle, i
     602              : 
     603           52 :       CALL timeset(routineN, handle)
     604              : 
     605          112 :       DO i = 1, rtbse_env%n_spin
     606           60 :          CALL cp_fm_set_all(rtbse_env%C_active(i), 0.0_dp)
     607              :          CALL cp_fm_to_fm_submat_general( &
     608              :             rtbse_env%bs_env%fm_mo_coeff_Gamma(i), rtbse_env%C_active(i), &
     609              :             rtbse_env%n_ao, rtbse_env%mo_active, &
     610              :             1, rtbse_env%first_active_mo, &
     611              :             1, 1, &
     612          112 :             rtbse_env%bs_env%fm_mo_coeff_Gamma(i)%matrix_struct%context)
     613              :       END DO
     614              : 
     615           52 :       CALL timestop(handle)
     616           52 :    END SUBROUTINE populate_C_active
     617              : 
     618              : ! **************************************************************************************************
     619              : !> \brief Builds the dipole moment operators r_mn in the active-MO basis (per axis, per spin) from
     620              : !>        the AO moment matrices: moments(k,σ) at the reference point, moments_field(k,σ) at origin.
     621              : !> \param rtbse_env RT-BSE environment
     622              : !> \author Stepan Marek (09.24)
     623              : !> \author Maximilian Graml (03.26) - refactor in prep. of linearized propagation
     624              : ! **************************************************************************************************
     625           52 :    SUBROUTINE initialize_moments(rtbse_env)
     626              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     627              : 
     628              :       CHARACTER(len=*), PARAMETER :: routineN = 'initialize_moments'
     629              : 
     630              :       INTEGER                                            :: handle, i_spin, k
     631              :       REAL(kind=dp), DIMENSION(3)                        :: rpoint
     632              :       TYPE(cp_fm_type)                                   :: tmp_ao
     633           52 :       TYPE(dbcsr_p_type), DIMENSION(:), POINTER          :: matrix_s, moments_dbcsr_p
     634              :       TYPE(post_scf_bandstructure_type), POINTER         :: bs_env
     635              : 
     636           52 :       CALL timeset(routineN, handle)
     637              :       ! Get pointers to parameters from qs_env
     638           52 :       CALL get_qs_env(rtbse_env%qs_env, bs_env=bs_env, matrix_s=matrix_s)
     639              : 
     640              :       ! AO-sized scratch buffer for moment matrices before transform to MO basis
     641           52 :       CALL cp_fm_create(tmp_ao, bs_env%fm_s_Gamma%matrix_struct)
     642              : 
     643              :       ! ****** START MOMENTS OPERATOR CALCULATION
     644              :       ! Construct moments from dbcsr
     645              :       NULLIFY (moments_dbcsr_p)
     646          208 :       ALLOCATE (moments_dbcsr_p(3))
     647          208 :       DO k = 1, 3
     648              :          ! Make sure the pointer is empty
     649          156 :          NULLIFY (moments_dbcsr_p(k)%matrix)
     650              :          ! Allocate a new matrix that the pointer points to
     651          156 :          ALLOCATE (moments_dbcsr_p(k)%matrix)
     652              :          ! Create the matrix storage - matrix copies the structure of overlap matrix
     653          208 :          CALL dbcsr_copy(moments_dbcsr_p(k)%matrix, matrix_s(1)%matrix)
     654              :       END DO
     655              :       ! Run the moment calculation
     656              :       ! check for presence to prevent memory errors
     657           52 :       rpoint(:) = 0.0_dp
     658              :       CALL get_reference_point(rpoint, qs_env=rtbse_env%qs_env, &
     659           52 :                                reference=rtbse_env%moment_ref_type, ref_point=rtbse_env%user_moment_ref_point)
     660           52 :       CALL build_local_moment_matrix(rtbse_env%qs_env, moments_dbcsr_p, 1, rpoint)
     661              :       ! Copy to AO scratch then transform to MO-active
     662          208 :       DO k = 1, 3
     663          156 :          CALL copy_dbcsr_to_fm(moments_dbcsr_p(k)%matrix, tmp_ao)
     664          388 :          DO i_spin = 1, rtbse_env%n_spin
     665          336 :             CALL transform_ao_to_mo_covariant_fm(rtbse_env, tmp_ao, rtbse_env%moments(k, i_spin), i_spin)
     666              :          END DO
     667              :       END DO
     668              :       ! TODO: remove moments_field (only needed for the TDDFT comparison)
     669              :       ! Now, repeat without reference point to get the moments for field
     670              :       CALL get_reference_point(rpoint, qs_env=rtbse_env%qs_env, &
     671           52 :                                reference=use_mom_ref_zero)
     672           52 :       CALL build_local_moment_matrix(rtbse_env%qs_env, moments_dbcsr_p, 1, rpoint)
     673          208 :       DO k = 1, 3
     674          156 :          CALL copy_dbcsr_to_fm(moments_dbcsr_p(k)%matrix, tmp_ao)
     675          388 :          DO i_spin = 1, rtbse_env%n_spin
     676          336 :             CALL transform_ao_to_mo_covariant_fm(rtbse_env, tmp_ao, rtbse_env%moments_field(k, i_spin), i_spin)
     677              :          END DO
     678              :       END DO
     679              : 
     680              :       ! Now can deallocate dbcsr matrices
     681          208 :       DO k = 1, 3
     682          156 :          CALL dbcsr_release(moments_dbcsr_p(k)%matrix)
     683          208 :          DEALLOCATE (moments_dbcsr_p(k)%matrix)
     684              :       END DO
     685           52 :       DEALLOCATE (moments_dbcsr_p)
     686           52 :       CALL cp_fm_release(tmp_ao)
     687              :       ! ****** END MOMENTS OPERATOR CALCULATION
     688              : 
     689           52 :       CALL timestop(handle)
     690          104 :    END SUBROUTINE initialize_moments
     691              : 
     692              : ! **************************************************************************************************
     693              : !> \brief Initial MO density ρ^0_mn = f_m δ_mn (f_m = 1 on active occupied, 0 on virtual), copied to
     694              : !>        rho_orig as the reference for the δ-kick. Imaginary part zero.
     695              : !> \param rtbse_env RT-BSE environment
     696              : !> \author Stepan Marek (09.24)
     697              : !> \author Maximilian Graml (03.26) - adapted to the linearized active-MO path
     698              : ! **************************************************************************************************
     699          104 :    SUBROUTINE initialize_density_matrix(rtbse_env)
     700              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     701              : 
     702              :       CHARACTER(len=*), PARAMETER :: routineN = 'initialize_density_matrix'
     703              : 
     704              :       INTEGER                                            :: handle, i, i_row_global, ii, &
     705              :                                                             j_col_global, jj, ncol_global, &
     706              :                                                             ncol_local, nrow_global, nrow_local
     707           52 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
     708              : 
     709           52 :       CALL timeset(routineN, handle)
     710              : 
     711              :       ! Get distribution of MO-active workspace
     712              :       CALL cp_fm_get_info(rtbse_env%real_workspace_mo(1), &
     713              :                           nrow_global=nrow_global, ncol_global=ncol_global, &
     714              :                           nrow_local=nrow_local, ncol_local=ncol_local, &
     715           52 :                           row_indices=row_indices, col_indices=col_indices)
     716              : 
     717              :       ! Iterate over both spins
     718          112 :       DO i = 1, rtbse_env%n_spin
     719              :          !Ensure that workspace is set to 0
     720           60 :          CALL cp_fm_set_all(rtbse_env%real_workspace_mo(1), 0.0_dp)
     721          414 :          DO ii = 1, nrow_local
     722          354 :             i_row_global = row_indices(ii)
     723         4626 :             DO jj = 1, ncol_local
     724         4212 :                j_col_global = col_indices(jj)
     725         4212 :                IF (i_row_global == j_col_global .AND. &
     726          354 :                    (i_row_global + rtbse_env%first_active_mo - 1) <= rtbse_env%n_occ(i)) THEN
     727           30 :                   rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = 1.0_dp
     728              :                END IF
     729              :             END DO
     730              :          END DO
     731              :          ! Sets imaginary part to zero
     732           60 :          CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), mtarget=rtbse_env%rho(i))
     733              :          ! Save the reference value for the case of delta kick
     734          112 :          CALL cp_cfm_to_cfm(rtbse_env%rho(i), rtbse_env%rho_orig(i))
     735              :       END DO
     736              :       ! rho_orig stays the SCF reference for delta rho; a restart overwrites only rho, downstream in
     737              :       ! the driver (read_restart_density + apply_restart_basis_bridge + rotate_rho_phase).
     738              : 
     739           52 :       CALL timestop(handle)
     740           52 :    END SUBROUTINE initialize_density_matrix
     741              : 
     742              : ! **************************************************************************************************
     743              : !> \brief Single-particle reference Hamiltonian in the active-MO basis: H^0_mn = ε^GW_m δ_mn (or KS
     744              : !>        ε^scf), diagonal. TDA first-peak adds +Ω_0/2 on occupied, -Ω_0/2 on virtual diagonals.
     745              : !> \param rtbse_env RT-BSE environment
     746              : !> \author Stepan Marek (09.24)
     747              : !> \author Maximilian Graml (03.26) - refactor in prep. of linearized propagation
     748              : ! **************************************************************************************************
     749          104 :    SUBROUTINE initialize_singleparticle_hamiltonian(rtbse_env)
     750              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     751              : 
     752              :       CHARACTER(len=*), PARAMETER :: routineN = 'initialize_singleparticle_hamiltonian'
     753              : 
     754              :       INTEGER                                            :: abs_mo_idx, handle, i, i_row_global, ii, &
     755              :                                                             j_col_global, jj, ncol_global, &
     756              :                                                             ncol_local, nrow_global, nrow_local
     757           52 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
     758              :       TYPE(post_scf_bandstructure_type), POINTER         :: bs_env
     759              : 
     760           52 :       CALL timeset(routineN, handle)
     761              :       ! Get pointers to parameters from qs_env
     762           52 :       CALL get_qs_env(rtbse_env%qs_env, bs_env=bs_env)
     763              : 
     764              :       ! Get distribution of MO-active workspace
     765              :       CALL cp_fm_get_info(rtbse_env%real_workspace_mo(1), &
     766              :                           nrow_global=nrow_global, ncol_global=ncol_global, &
     767              :                           nrow_local=nrow_local, ncol_local=ncol_local, &
     768           52 :                           row_indices=row_indices, col_indices=col_indices)
     769              : 
     770              :       !Ensure that workspace is set to 0
     771           52 :       CALL cp_fm_set_all(rtbse_env%real_workspace_mo(1), 0.0_dp)
     772          820 :       rtbse_env%eps_active(:, :) = 0.0_dp
     773              :       ! ****** START SINGLE PARTICLE HAMILTONIAN CALCULATION
     774          112 :       DO i = 1, rtbse_env%n_spin
     775          414 :          DO ii = 1, nrow_local
     776          354 :             i_row_global = row_indices(ii)
     777         4626 :             DO jj = 1, ncol_local
     778         4212 :                j_col_global = col_indices(jj)
     779         4566 :                IF (i_row_global == j_col_global) THEN
     780          354 :                   abs_mo_idx = i_row_global + rtbse_env%first_active_mo - 1
     781          354 :                   IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     782              :                      ! G0W0 Hamiltonian
     783          354 :                      rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = bs_env%eigenval_G0W0(abs_mo_idx, 1, i)
     784              :                   ELSE
     785              :                      ! KS Hamiltonian
     786            0 :                      rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = bs_env%eigenval_scf_Gamma(abs_mo_idx, i)
     787              :                   END IF
     788              :                   ! First-peak shift (TDA only): rotate the single-particle Hamiltonian
     789              :                   ! into a frame where the lowest active OV mode oscillates at zero, with
     790              :                   ! Ω_0 = eps_min_ai. Adds +Ω_0/2 on active occupied diagonals and
     791              :                   ! -Ω_0/2 on active virtual diagonals so that [h', rho]_OV picks up an
     792              :                   ! overall (- eps_ai + Ω_0) and OO/VV blocks remain commutator-free.
     793          354 :                   IF (rtbse_env%tda_active .AND. rtbse_env%tda_shift_to_first_peak) THEN
     794           36 :                      IF (abs_mo_idx <= rtbse_env%n_occ(i)) THEN
     795              :                         rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = &
     796            3 :                            rtbse_env%real_workspace_mo(1)%local_data(ii, jj) + 0.5_dp*rtbse_env%omega_shift
     797              :                      ELSE
     798              :                         rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = &
     799           33 :                            rtbse_env%real_workspace_mo(1)%local_data(ii, jj) - 0.5_dp*rtbse_env%omega_shift
     800              :                      END IF
     801              :                   END IF
     802              :                   ! Mirror the finalized diagonal into the replicated active-energy array
     803          354 :                   rtbse_env%eps_active(i_row_global, i) = rtbse_env%real_workspace_mo(1)%local_data(ii, jj)
     804              :                END IF
     805              :             END DO
     806              :          END DO
     807          112 :          CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), mtarget=rtbse_env%ham_reference_singleparticle(i))
     808              :       END DO
     809              :       ! Each diagonal element was set on its single owner rank; sum to replicate eps_active.
     810         1588 :       CALL rtbse_env%real_workspace_mo(1)%matrix_struct%para_env%sum(rtbse_env%eps_active)
     811              :       ! ****** END SINGLE PARTICLE HAMILTONIAN CALCULATION
     812              : 
     813           52 :       CALL timestop(handle)
     814           52 :    END SUBROUTINE initialize_singleparticle_hamiltonian
     815              : 
     816              : ! **************************************************************************************************
     817              : !> \brief Reference Hartree subtraction: builds V^H[ρ^0] (AO-RI or RI-RS) and subtracts it into
     818              : !>        ham_reference, realizing H_eff = ... + V_H[ρ] - V_H[ρ^0]. Only for non-TDA n_spin=1 (in
     819              : !>        TDA the OV/VO projection of the OO-diagonal ρ^0 vanishes, so the reference is zero).
     820              : !> \param rtbse_env RT-BSE environment
     821              : !> \author Stepan Marek (09.24)
     822              : !> \author Maximilian Graml (03.26) - add transform to MO
     823              : ! **************************************************************************************************
     824           52 :    SUBROUTINE initialize_hartree_potential(rtbse_env)
     825              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     826              : 
     827              :       CHARACTER(len=*), PARAMETER :: routineN = 'initialize_hartree_potential'
     828              : 
     829              :       INTEGER                                            :: handle, i, n_grid
     830              :       LOGICAL                                            :: use_hartree_reference, use_rirs_kernel
     831              :       TYPE(post_scf_bandstructure_type), POINTER         :: bs_env
     832              : 
     833           52 :       CALL timeset(routineN, handle)
     834              :       ! Get pointers to parameters from qs_env
     835           52 :       CALL get_qs_env(rtbse_env%qs_env, bs_env=bs_env)
     836              :       use_hartree_reference = (.NOT. rtbse_env%tda_active) .AND. (rtbse_env%n_spin == 1) .AND. &
     837           52 :                               (.NOT. rtbse_env%debug_disable_hartree)
     838           52 :       use_rirs_kernel = rtbse_env%rirs_kernel
     839              : 
     840              :       ! Make sure the RI-RS V_grid kernel is available if we need it for Hartree.
     841           52 :       IF (use_rirs_kernel) CALL rt_bse_ri_rs_ensure_V_grid(bs_env, rtbse_env%qs_env)
     842              : 
     843              :       ! Spin-summed grid-density accumulators for the RI-RS Hartree reuse (diag(φρφ^T) harvested in SEX;
     844              :       ! mat_phi_mu_l is grid x AO, so its row count is n_grid). Allocated ONLY for RI-RS + Hartree, so the
     845              :       ! get_sigma harvest calls (passed unconditionally below) see an absent optional and self-disable on
     846              :       ! AO-RI / Hartree-off (F2008 unallocated-allocatable -> absent optional). Sole allocator, runs once.
     847           16 :       IF (use_rirs_kernel .AND. (.NOT. rtbse_env%debug_disable_hartree)) THEN
     848           16 :          CALL dbcsr_get_info(bs_env%ri_rs%mat_phi_mu_l, nfullrows_total=n_grid)
     849           64 :          ALLOCATE (rtbse_env%hartree_diag_re(n_grid), rtbse_env%hartree_diag_im(n_grid))
     850              :       END IF
     851              : 
     852              :       ! The RI-RS Hartree normally reuses the grid density harvested by the SEX kernel. With SEX
     853              :       ! disabled but Hartree on, that diagonal is never produced, so the Hartree must rebuild the full
     854              :       ! real-space density grid itself every RK4 stage - much slower. Warn once (this is a debug-only
     855              :       ! configuration); the rebuild fallback lives in the use_sex branches of the Hartree kernels.
     856           52 :       IF (use_rirs_kernel .AND. (.NOT. rtbse_env%debug_disable_hartree) .AND. rtbse_env%debug_disable_sex) THEN
     857              :          CALL cp_warn(__LOCATION__, &
     858              :                       "RI-RS Hartree rebuilds the full density grid every RK4 stage because SEX is "// &
     859              :                       "disabled (DEBUG_DISABLE_SEX) and no SEX-harvested diagonal is available to reuse. "// &
     860            0 :                       "This slows down the Hartree computation considerably.")
     861              :       END IF
     862              : 
     863              :       ! ****** START HARTREE POTENTIAL REFERENCE CALCULATION
     864              :       ! v_dbcsr is needed by either AO-RI Hartree (here) or by AO-RI SEX (W = V + W^c assembly).
     865              :       IF (.NOT. use_rirs_kernel) THEN
     866           36 :          CALL init_hartree(rtbse_env, rtbse_env%v_dbcsr)
     867              :       END IF
     868              :       ! Always zero ham_reference here (this routine is the first to touch it).
     869              :       ! The Hartree reference subtraction is then conditionally added below.
     870          112 :       DO i = 1, rtbse_env%n_spin
     871          112 :          CALL cp_cfm_set_all(rtbse_env%ham_reference(i), CMPLX(0.0_dp, 0.0_dp, kind=dp))
     872              :       END DO
     873              :       ! Calculate the original Hartree potential
     874              :       ! Uses rho_orig - same as rho for initial run but different for continued run
     875              :       ! In TDA the propagator evaluates separate OV/VO-projected kernel passes.
     876              :       ! rho_orig is OO-diagonal in the MO basis, so its OV/VO projections vanish
     877              :       ! and the corresponding Hartree reference is identically zero.
     878           52 :       IF (use_hartree_reference) THEN
     879           24 :          DO i = 1, rtbse_env%n_spin
     880           12 :             IF (use_rirs_kernel) THEN
     881              :                ! V^H_λσ = sum_l φ_λ(r_l) v_l φ_σ(r_l), v_l = sum_l' V_ll' n_l'  (RI-RS)
     882              :                ! AO-RI get_hartree uses only Re(rho); mirror that on the RI-RS path.
     883              :                CALL cp_cfm_to_fm(msource=rtbse_env%rho_ao_scratch(i), &
     884            4 :                                  mtargetr=rtbse_env%real_workspace(1))
     885              :                CALL compute_hartree_ri_rs(bs_env, rtbse_env%real_workspace(1), &
     886            4 :                                           rtbse_env%hartree_curr_ao(i))
     887              :             ELSE
     888              :                ! V^H_λσ = sum_PQ (λσ|P) V_PQ [sum_µν (µν|Q) ρ^0_µν]  (AO-RI; reference density)
     889            8 :                CALL get_hartree(rtbse_env, rtbse_env%rho_ao_scratch(i), rtbse_env%hartree_curr_ao(i))
     890              :             END IF
     891              :             ! Scaling by spin degeneracy
     892           12 :             CALL cp_fm_scale(rtbse_env%spin_degeneracy, rtbse_env%hartree_curr_ao(i))
     893              :             ! Transform to MO basis (AO scratch -> MO-active result)
     894           12 :             CALL transform_ao_to_mo_covariant_fm(rtbse_env, rtbse_env%hartree_curr_ao(i), rtbse_env%hartree_curr(i), i)
     895              :             ! Apply occupation factor f_n-f_m
     896           12 :             CALL transform_mo_occupation_factor_diff_fm(rtbse_env, rtbse_env%hartree_curr(i), i)
     897              :             ! Subtract the reference from the reference Hamiltonian
     898              :             ! following H_eff = ... + V_Hartree(rho) - V_Hartree(rho_0),
     899           12 :             CALL cp_fm_to_cfm(msourcer=rtbse_env%hartree_curr(i), mtarget=rtbse_env%ham_workspace(1))
     900              :             CALL cp_cfm_scale_and_add(CMPLX(1.0, 0.0, kind=dp), rtbse_env%ham_reference(i), &
     901           24 :                                       CMPLX(-1.0, 0.0, kind=dp), rtbse_env%ham_workspace(1))
     902              :          END DO
     903              :       END IF
     904              :       ! ****** END HARTREE POTENTIAL REFERENCE CALCULATION
     905              : 
     906           52 :       CALL timestop(handle)
     907           52 :    END SUBROUTINE initialize_hartree_potential
     908              : 
     909              : ! **************************************************************************************************
     910              : !> \brief Reference SEX self-energy subtraction into ham_reference (non-TDA, n_spin=1). Assembles
     911              : !>        W = V + W^c, then Σ^SX = -W ρ^0, (f_n - f_m)-weighted, subtracted.
     912              : !> \param rtbse_env RT-BSE environment
     913              : !> \author Stepan Marek (09.24)
     914              : !> \author Maximilian Graml (03.26) - add transform to MO
     915              : ! **************************************************************************************************
     916           52 :    SUBROUTINE initialize_sex_selfenergy(rtbse_env)
     917              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     918              : 
     919              :       CHARACTER(len=*), PARAMETER :: routineN = 'initialize_sex_selfenergy'
     920              : 
     921              :       INTEGER                                            :: handle, i
     922              :       LOGICAL                                            :: use_rirs_kernel, use_sex_reference
     923              : 
     924           52 :       CALL timeset(routineN, handle)
     925              :       use_sex_reference = (.NOT. rtbse_env%tda_active) .AND. (rtbse_env%n_spin == 1) .AND. &
     926           52 :                           (.NOT. rtbse_env%debug_disable_sex)
     927           52 :       use_rirs_kernel = rtbse_env%rirs_kernel
     928              : 
     929              :       ! Make sure the RI-RS W0_grid kernel is available if we need it for SEX.
     930           52 :       IF (use_rirs_kernel) CALL rt_bse_ri_rs_ensure_W0_grid(rtbse_env%bs_env, rtbse_env%qs_env)
     931              : 
     932              :       ! ****** START SEX REFERENCE CALCULATION
     933              :       ! w_dbcsr and screened_dbt are needed for get_sigma routines (AO-RI path only).
     934              :       ! For RI-RS the W = V + W^c kernel is on the real-space grid in mat_W0_grid_rtbse.
     935              :       IF (.NOT. use_rirs_kernel) THEN
     936           36 :          IF (rtbse_env%ham_reference_type == rtp_bse_ham_g0w0) THEN
     937              :             ! W(w=0) is built by the GW step only under its RTBSE rtp_method gate; reaching this
     938              :             ! consumer without it means gate and consumer disagree. The HF branch below needs no W.
     939           36 :             IF (.NOT. ASSOCIATED(rtbse_env%bs_env%fm_W_MIC_freq_zero%matrix_struct)) THEN
     940              :                CALL cp_abort(__LOCATION__, &
     941              :                              "RT-BSE AO-RI kernel needs the screened interaction W(w=0), which the "// &
     942              :                              "GW step did not build. Select the RT-BSE propagator with '&RTBSE' or "// &
     943            0 :                              "'&RTBSE RTBSE', not '&RTBSE TDDFT'.")
     944              :             END IF
     945              :             ! In a non-HF calculation, copy the actual correlation part of the interaction
     946           36 :             CALL copy_fm_to_dbcsr(rtbse_env%bs_env%fm_W_MIC_freq_zero, rtbse_env%w_dbcsr)
     947              :          ELSE
     948              :             ! In HF, correlation is set to zero
     949            0 :             CALL dbcsr_set(rtbse_env%w_dbcsr, 0.0_dp)
     950              :          END IF
     951              :          ! Add the Hartree to the screened_dbt tensor - now W = V + W^c
     952           36 :          CALL dbcsr_add(rtbse_env%w_dbcsr, rtbse_env%v_dbcsr, 1.0_dp, 1.0_dp)
     953           36 :          CALL dbt_copy_matrix_to_tensor(rtbse_env%w_dbcsr, rtbse_env%screened_dbt)
     954              :       END IF
     955              :       ! Calculate the SEX starting energies
     956          112 :       DO i = 1, rtbse_env%n_spin
     957              :          ! Calculate the exchange (SEX) part for this spin channel
     958              :          ! Uses rho_orig - same as rho for initial run but different for continued run
     959              :          ! For KS reference this is the time-dependent Fock exchange (w_dbcsr = v only).
     960              :          ! In TDA the propagator evaluates separate OV/VO-projected kernel passes.
     961              :          ! rho_orig is OO-diagonal in the MO basis, so its OV/VO projections
     962              :          ! vanish and the SEX reference must remain zero in TDA.
     963          112 :          IF (use_sex_reference) THEN
     964              :             ! Σ^SX_λσ = -sum_νQ [sum_µ (λµ|Q) ρ^0_µν][sum_P (νσ|P) W_PQ]  (reference)
     965           12 :             CALL get_sigma(rtbse_env, rtbse_env%sigma_SEX_ao(i), -1.0_dp, rtbse_env%rho_ao_scratch(i))
     966              :             ! Transform to MO basis (AO scratch -> MO-active result)
     967           12 :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%sigma_SEX_ao(i), rtbse_env%sigma_SEX(i), i)
     968              :             ! Apply occupation factor f_n-f_m
     969           12 :             CALL transform_mo_occupation_factor_diff_cfm(rtbse_env, rtbse_env%sigma_SEX(i), i)
     970              :             ! Subtract from the complex reference Hamiltonian
     971              :             CALL cp_cfm_scale_and_add(CMPLX(1.0, 0.0, kind=dp), rtbse_env%ham_reference(i), &
     972           12 :                                       CMPLX(-1.0, 0.0, kind=dp), rtbse_env%sigma_SEX(i))
     973              :          END IF
     974              :       END DO
     975              :       ! ****** END SEX REFERENCE CALCULATION
     976              : 
     977           52 :       CALL timestop(handle)
     978           52 :    END SUBROUTINE initialize_sex_selfenergy
     979              : 
     980              : ! **************************************************************************************************
     981              : !> \brief Propagates the density one timestep by RK4 for ∂_t ρ = f(t,ρ) =
     982              : !>        -i( Δε Δρ + (f_n - f_m) V_Hartree(Δρ) + ΔΣ(Δρ) ); see body for the 4-stage scheme.
     983              : !>        Spin loop is inner to each stage (cross-spin Hartree coupling).
     984              : !> \param rtbse_env Entry point - rtbse environment
     985              : !> \param rho_start Initial density matrix
     986              : !> \param rho_end Final density matrix
     987              : ! **************************************************************************************************
     988          710 :    SUBROUTINE solve_rk4_timestep(rtbse_env, rho_start, rho_end)
     989              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
     990              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_start, rho_end
     991              : 
     992              :       CHARACTER(len=*), PARAMETER :: routineN = 'solve_rk4_timestep'
     993              : 
     994              :       INTEGER                                            :: handle, i
     995              : 
     996          710 :       CALL timeset(routineN, handle)
     997              :       ! RK4 follows the typical scheme
     998              :       ! d/dt ρ = f(t, ρ)
     999              :       ! f(t,ρ) = -i( Δε Δρ + (f_n-f_m) V_Hartree(Δρ) + ΔΣ(Δρ) )
    1000              :       ! i.e. RK4 reads
    1001              :       ! k_1 = f(t, ρ_start)
    1002              :       ! k_2 = f(t + dt/2, ρ_start + dt/2 * k_1)
    1003              :       ! k_3 = f(t + dt/2, ρ_start + dt/2 * k_2)
    1004              :       ! k_4 = f(t + dt, ρ_start + dt * k_3)
    1005              :       ! ρ_end = ρ_start + dt/6 * (k_1 + 2*k_2 + 2*k_3 + k_4)
    1006              :       ! Note that the effective Hamiltonian needs to be updated for each evaluation of f,
    1007              :       ! as it depends on the density matrix at the respective time
    1008              : 
    1009              :       ! Spin loop is INNER to each RK4 stage (inside do_rk4_stage): cross-spin-coupled kernels (Hartree in
    1010              :       ! open shell) need every spin's stage density before any spin advances. rk4_coefficients(i) holds
    1011              :       ! spin i's CURRENT-stage k (indexed by spin, not stage - see its allocation in create_rtbse_env),
    1012              :       ! reused across stages; rho_workspace(i) holds spin i's stage density. Bit-identical for n_spin=1.
    1013         1540 :       DO i = 1, rtbse_env%n_spin
    1014         1540 :          CALL cp_cfm_to_cfm(rho_start(i), rho_end(i))
    1015              :       END DO
    1016              :       ! Each stage: do_rk4_stage evaluates k = f(stage density) for all spins, accumulates
    1017              :       ! rho_end += result_weight*dt*k (Butcher b = 1/6, 1/3, 1/3, 1/6) and forms the next stage density
    1018              :       ! rho_workspace = rho_start + advance_weight*dt*k (node c = 1/2, 1/2, 1; omitted on k_4, which only
    1019              :       ! accumulates). The dt factor is applied inside do_rk4_stage, so the calls show the bare weights.
    1020              :       ! k_1 = f(t, rho_start)
    1021              :       CALL do_rk4_stage(rtbse_env, rho_start, rho_start, rho_end, &
    1022          710 :                         result_weight=1.0_dp/6.0_dp, advance_weight=0.5_dp)
    1023              :       ! k_2 = f(t + dt/2, rho_start + dt/2 * k_1)
    1024              :       CALL do_rk4_stage(rtbse_env, rtbse_env%rho_workspace, rho_start, rho_end, &
    1025          710 :                         result_weight=1.0_dp/3.0_dp, advance_weight=0.5_dp)
    1026              :       ! k_3 = f(t + dt/2, rho_start + dt/2 * k_2)
    1027              :       CALL do_rk4_stage(rtbse_env, rtbse_env%rho_workspace, rho_start, rho_end, &
    1028          710 :                         result_weight=1.0_dp/3.0_dp, advance_weight=1.0_dp)
    1029              :       ! k_4 = f(t + dt, rho_start + dt * k_3)
    1030              :       CALL do_rk4_stage(rtbse_env, rtbse_env%rho_workspace, rho_start, rho_end, &
    1031          710 :                         result_weight=1.0_dp/6.0_dp)
    1032              : 
    1033              :       ! Update bookkeeping to the next timestep similar to logic of etrs_scf_loop
    1034          710 :       rtbse_env%sim_step = rtbse_env%sim_step + 1
    1035          710 :       rtbse_env%sim_time = rtbse_env%sim_time + rtbse_env%sim_dt
    1036              : 
    1037          710 :       CALL timestop(handle)
    1038          710 :    END SUBROUTINE solve_rk4_timestep
    1039              : 
    1040              : ! **************************************************************************************************
    1041              : !> \brief Takes one RK4 stage. Evaluates k = f(t, rho_eval) for every spin into
    1042              : !>        rtbse_env%rk4_coefficients, accumulates it into the running result (rho_end += result_weight*dt*k)
    1043              : !>        and - unless this is the last stage - forms the next stage density
    1044              : !>        (rho_workspace = rho_base + advance_weight*dt*k). Builds the stage's shared kernels first: the
    1045              : !>        cross-spin Hartree is built once per stage and consumed by every spin in update_effective_ham_MO.
    1046              : !>        All shell/kernel combinations go through build_shared_sex_and_hartree (mask_mode selects the
    1047              : !>        input convention); update_effective_ham_MO is then a pure consumer.
    1048              : !>        No timeset/timestop: the callees are individually timed and this runs 4x per RK4 timestep.
    1049              : !> \param rtbse_env RT-BSE environment
    1050              : !> \param rho_eval Per-spin MO density f is evaluated at (rho_start for k_1, rho_workspace otherwise)
    1051              : !> \param rho_base Per-spin MO density the next stage advances from (the step's rho_start)
    1052              : !> \param rho_end Per-spin RK4 result accumulator (= rho_start + dt/6*(k1+2k2+2k3+k4) after all 4 stages)
    1053              : !> \param result_weight RK4 Butcher weight b (dt factor applied inside) for accumulating k into rho_end
    1054              : !> \param advance_weight RK4 node c (dt factor applied inside) for the next stage density; ABSENT on the last stage
    1055              : !> \author Maximilian Graml
    1056              : ! **************************************************************************************************
    1057         2840 :    SUBROUTINE do_rk4_stage(rtbse_env, rho_eval, rho_base, rho_end, result_weight, advance_weight)
    1058              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1059              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_eval, rho_base, rho_end
    1060              :       REAL(kind=dp), INTENT(IN)                          :: result_weight
    1061              :       REAL(kind=dp), INTENT(IN), OPTIONAL                :: advance_weight
    1062              : 
    1063              :       INTEGER                                            :: i, mask_mode
    1064              : 
    1065              :       ! Input convention for this stage's shared kernel build:
    1066         2840 :       IF (rtbse_env%tda_active) THEN
    1067         1720 :          mask_mode = kernel_input_ov          ! TDA (any shell): OV only, non-Hermitian
    1068         1120 :       ELSE IF (rtbse_env%n_spin > 1) THEN
    1069          160 :          mask_mode = kernel_input_ovvo        ! open-shell ABBA: OV+VO, Hermitian
    1070              :       ELSE
    1071          960 :          mask_mode = kernel_input_full        ! closed-shell ABBA: full ρ, Hermitian
    1072              :       END IF
    1073              :       ! Build this stage's shared SEX + bare Hartree; consumers are per-spin in update_effective_ham_MO.
    1074         2840 :       CALL build_shared_sex_and_hartree(rtbse_env, rho_eval, mask_mode)
    1075         6160 :       DO i = 1, rtbse_env%n_spin
    1076         3320 :          CALL update_effective_ham_MO(rtbse_env, rho_eval(i), rtbse_env%rk4_coefficients(i), i)
    1077         6160 :          IF (rtbse_env%tda_active .OR. rtbse_env%n_spin > 1) THEN
    1078         2360 :             CALL project_drho_to_ov(rtbse_env, rtbse_env%rk4_coefficients(i), i)
    1079              :          END IF
    1080              :       END DO
    1081              :       ! Fold each spin's k into the RK4 result and (unless last stage) form the next stage density.
    1082              :       ! The dt factor lives here so the call sites carry the bare RK4 weights.
    1083         6160 :       DO i = 1, rtbse_env%n_spin
    1084              :          CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rho_end(i), &
    1085         3320 :                                    CMPLX(result_weight*rtbse_env%sim_dt, 0.0_dp, kind=dp), rtbse_env%rk4_coefficients(i))
    1086         6160 :          IF (PRESENT(advance_weight)) THEN
    1087         2490 :             CALL cp_cfm_to_cfm(rho_base(i), rtbse_env%rho_workspace(i))
    1088              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%rho_workspace(i), &
    1089         2490 :                                       CMPLX(advance_weight*rtbse_env%sim_dt, 0.0_dp, kind=dp), rtbse_env%rk4_coefficients(i))
    1090              :          END IF
    1091              :       END DO
    1092         2840 :    END SUBROUTINE do_rk4_stage
    1093              : 
    1094              : ! **************************************************************************************************
    1095              : !> \brief Builds the spin-summed complex Hartree potential in AO, once per RK4 stage (AO-RI or RI-RS).
    1096              : !>        rho_total = spin_degeneracy * sum_sigma (OV-masked rho^sigma -> AO); hartree_total_ao =
    1097              : !>        V_H[rho_total]. update_effective_ham_MO back-transforms it with each spin's C, so this
    1098              : !>        single call feeds every spin block - the cross-spin Hartree coupling of open shell.
    1099              : !>        Side effect: leaves rho_ao_scratch(sigma) = OV-masked AO density (recomputed per spin in
    1100              : !>        update_effective_ham_MO; built here only to form the sum).
    1101              : !> \param rtbse_env RT-BSE environment
    1102              : !> \param rho_stage Per-spin MO density at the current RK4 stage
    1103              : !> \param keep_ovvo .FALSE. = OV source mask (TDA); .TRUE. = OV+VO (open-shell ABBA).
    1104              : !> \author Maximilian Graml
    1105              : ! **************************************************************************************************
    1106          176 :    SUBROUTINE build_shared_hartree_ao(rtbse_env, rho_stage, keep_ovvo)
    1107              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1108              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_stage
    1109              :       LOGICAL, INTENT(IN)                                :: keep_ovvo
    1110              : 
    1111              :       CHARACTER(len=*), PARAMETER :: routineN = 'build_shared_hartree_ao'
    1112              : 
    1113              :       INTEGER                                            :: handle, isp
    1114              : 
    1115          176 :       CALL timeset(routineN, handle)
    1116              :       ! Per spin: mask the stage density and project to AO.
    1117          528 :       DO isp = 1, rtbse_env%n_spin
    1118          352 :          CALL cp_cfm_to_cfm(rho_stage(isp), rtbse_env%rho_delta_mo(isp))
    1119              :          CALL mask_mo_block_cfm(rtbse_env, rtbse_env%rho_delta_mo(isp), isp, &
    1120          352 :                                 keep_OV=.TRUE., keep_ovvo=keep_ovvo)
    1121              :          CALL transform_mo_to_ao_contravariant_cfm(rtbse_env, rtbse_env%rho_delta_mo(isp), &
    1122          528 :                                                    rtbse_env%rho_ao_scratch(isp), isp)
    1123              :       END DO
    1124              :       ! Sum: rho_total = spin_degeneracy * sum_isp rho_ao_scratch(isp).
    1125          176 :       CALL cp_cfm_set_all(rtbse_env%rho_total_ao_scratch, CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1126          528 :       DO isp = 1, rtbse_env%n_spin
    1127              :          CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%rho_total_ao_scratch, &
    1128              :                                    CMPLX(rtbse_env%spin_degeneracy, 0.0_dp, kind=dp), &
    1129          528 :                                    rtbse_env%rho_ao_scratch(isp))
    1130              :       END DO
    1131              :       ! One complex Hartree contraction on the summed density. RI backend = kernel choice: both
    1132              :       ! get_hartree_complex and compute_hartree_ri_rs_complex take AO x AO in/out and are spin-blind,
    1133              :       ! so the spin-summed cross-spin density feeds whichever kernel is active.
    1134          176 :       IF (rtbse_env%rirs_kernel) THEN
    1135              :          ! V^H_λσ = sum_l φ_λ(r_l) v_l φ_σ(r_l), v_l = sum_l' V_ll' n_l'[ρ^total]  (RI-RS)
    1136              :          CALL compute_hartree_ri_rs_complex(rtbse_env%bs_env, rtbse_env%rho_total_ao_scratch, &
    1137           88 :                                             rtbse_env%hartree_total_ao)
    1138              :       ELSE
    1139              :          ! V^H_λσ = sum_PQ (λσ|P) V_PQ [sum_µν (µν|Q) ρ^total_µν]  (AO-RI)
    1140              :          CALL get_hartree_complex(rtbse_env, rtbse_env%rho_total_ao_scratch, &
    1141           88 :                                   rtbse_env%hartree_total_ao, 1)
    1142              :       END IF
    1143          176 :       CALL timestop(handle)
    1144          176 :    END SUBROUTINE build_shared_hartree_ao
    1145              : 
    1146              : ! **************************************************************************************************
    1147              : !> \brief Unified cross-spin kernel builder, once per RK4 stage for every shell. Computes the
    1148              : !>        per-spin SEX self-energy (stashed in sigma_SEX_ao(σ)) and the single shared bare Hartree
    1149              : !>        (hartree_total_ao), so update_effective_ham_MO only consumes them.
    1150              : !>        mask_mode selects the source-density convention (OV / OV+VO / full-ρ); it also determines
    1151              : !>        the input Hermiticity, which gates the Hartree imaginary channel: Im computed only when
    1152              : !>        mask_mode = kernel_input_ov (non-Hermitian TDA input); for Hermitian input Im ≡ 0 analytically
    1153              : !>        and is skipped. Hartree emitted BARE (no spin_degeneracy); consumer scales g on the MO output.
    1154              : !> \param rtbse_env RT-BSE environment
    1155              : !> \param rho_stage Per-spin MO density at the current RK4 stage
    1156              : !> \param mask_mode Input-convention selector: kernel_input_ov / kernel_input_ovvo / kernel_input_full
    1157              : !> \author Maximilian Graml
    1158              : ! **************************************************************************************************
    1159         2840 :    SUBROUTINE build_shared_sex_and_hartree(rtbse_env, rho_stage, mask_mode)
    1160              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1161              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_stage
    1162              :       INTEGER, INTENT(IN)                                :: mask_mode
    1163              : 
    1164              :       CHARACTER(len=*), PARAMETER :: routineN = 'build_shared_sex_and_hartree'
    1165              : 
    1166              :       INTEGER                                            :: handle, isp
    1167              :       LOGICAL                                            :: harvest_im, use_hartree, &
    1168              :                                                             use_rirs_kernel, use_sex
    1169              : 
    1170         2840 :       CALL timeset(routineN, handle)
    1171         2840 :       use_hartree = .NOT. rtbse_env%debug_disable_hartree
    1172         2840 :       use_sex = .NOT. rtbse_env%debug_disable_sex
    1173         2840 :       use_rirs_kernel = rtbse_env%rirs_kernel
    1174              :       ! Im channel iff the masked input is non-Hermitian (OV-only, TDA). For Hermitian input (OV+VO
    1175              :       ! or full ρ) Im(ρ) is antisymmetric and Coulomb factors are symmetric, so V_H[Im] ≡ 0.
    1176         2840 :       harvest_im = (mask_mode == kernel_input_ov)
    1177              : 
    1178              :       ! Pre-zero the RI-RS spin-summed grid-diagonal accumulators (the SEX harvest target).
    1179         2840 :       IF (use_rirs_kernel .AND. use_sex .AND. use_hartree) THEN
    1180       325600 :          rtbse_env%hartree_diag_re(:) = 0.0_dp
    1181       185120 :          IF (harvest_im) rtbse_env%hartree_diag_im(:) = 0.0_dp
    1182              :       END IF
    1183              :       ! Per spin: stage ρ into AO (masked per mask_mode); SEX (stash sigma_SEX_ao(σ)); RI-RS harvests
    1184              :       ! the diagonal.
    1185         6160 :       DO isp = 1, rtbse_env%n_spin
    1186         3320 :          CALL cp_cfm_to_cfm(rho_stage(isp), rtbse_env%rho_delta_mo(isp))
    1187         2040 :          SELECT CASE (mask_mode)
    1188              :          CASE (kernel_input_ov)
    1189              :             CALL mask_mo_block_cfm(rtbse_env, rtbse_env%rho_delta_mo(isp), isp, &
    1190         2040 :                                    keep_OV=.TRUE., keep_ovvo=.FALSE.)
    1191              :          CASE (kernel_input_ovvo)
    1192              :             CALL mask_mo_block_cfm(rtbse_env, rtbse_env%rho_delta_mo(isp), isp, &
    1193          320 :                                    keep_OV=.TRUE., keep_ovvo=.TRUE.)
    1194              :          CASE (kernel_input_full)
    1195              :             ! no mask: full ρ (closed-shell ABBA; reference subtracted later via ham_reference)
    1196              :          CASE DEFAULT
    1197         3320 :             CPABORT("Unknown mask_mode in build_shared_sex_and_hartree")
    1198              :          END SELECT
    1199              :          CALL transform_mo_to_ao_contravariant_cfm(rtbse_env, rtbse_env%rho_delta_mo(isp), &
    1200         3320 :                                                    rtbse_env%rho_ao_scratch(isp), isp)
    1201         6160 :          IF (use_sex) THEN
    1202              :             ! Σ^SX_λσ = -sum_νQ [sum_µ (λµ|Q) Δρ_µν][sum_P (νσ|P) W_PQ]. Im accumulator passed only
    1203              :             ! when harvesting; absent (unallocated optional) on AO-RI / Hartree-off / Hermitian.
    1204         3000 :             IF (harvest_im) THEN
    1205              :                CALL get_sigma(rtbse_env, rtbse_env%sigma_SEX_ao(isp), -1.0_dp, rtbse_env%rho_ao_scratch(isp), &
    1206              :                               grid_diag_re_accum=rtbse_env%hartree_diag_re, &
    1207         1880 :                               grid_diag_im_accum=rtbse_env%hartree_diag_im)
    1208              :             ELSE
    1209              :                CALL get_sigma(rtbse_env, rtbse_env%sigma_SEX_ao(isp), -1.0_dp, rtbse_env%rho_ao_scratch(isp), &
    1210         1120 :                               grid_diag_re_accum=rtbse_env%hartree_diag_re)
    1211              :             END IF
    1212              :          END IF
    1213              :       END DO
    1214              :       ! Shared cross-spin Hartree, emitted BARE (no spin_degeneracy — consumer scales g on output).
    1215              :       ! RI-RS reuses the spin-summed grid diagonal; AO-RI contracts rho_total = sum_σ ρ_σ.
    1216              :       ! Hermitian input: real Hartree path (Im ≡ 0); non-Hermitian: complex.
    1217         2840 :       IF (use_hartree) THEN
    1218         2520 :          IF (use_rirs_kernel .AND. use_sex) THEN
    1219         1120 :             IF (harvest_im) THEN
    1220              :                CALL compute_hartree_ri_rs_from_diag(rtbse_env%bs_env, rtbse_env%hartree_diag_re, &
    1221          640 :                                                     rtbse_env%hartree_total_ao, n_im=rtbse_env%hartree_diag_im)
    1222              :             ELSE
    1223              :                CALL compute_hartree_ri_rs_from_diag(rtbse_env%bs_env, rtbse_env%hartree_diag_re, &
    1224          480 :                                                     rtbse_env%hartree_total_ao)
    1225              :             END IF
    1226              :          ELSE
    1227         1400 :             CALL cp_cfm_set_all(rtbse_env%rho_total_ao_scratch, CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1228         2960 :             DO isp = 1, rtbse_env%n_spin
    1229              :                CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%rho_total_ao_scratch, &
    1230         2960 :                                          CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%rho_ao_scratch(isp))
    1231              :             END DO
    1232         1400 :             IF (use_rirs_kernel) THEN
    1233            0 :                IF (harvest_im) THEN
    1234              :                   CALL compute_hartree_ri_rs_complex(rtbse_env%bs_env, rtbse_env%rho_total_ao_scratch, &
    1235            0 :                                                      rtbse_env%hartree_total_ao)
    1236              :                ELSE
    1237              :                   ! Hermitian input: real RI-RS on Re(ρ_total) -> cfm with Im ≡ 0.
    1238              :                   CALL cp_cfm_to_fm(msource=rtbse_env%rho_total_ao_scratch, &
    1239            0 :                                     mtargetr=rtbse_env%real_workspace(1))
    1240              :                   CALL compute_hartree_ri_rs(rtbse_env%bs_env, rtbse_env%real_workspace(1), &
    1241            0 :                                              rtbse_env%hartree_curr_ao(1))
    1242            0 :                   CALL cp_cfm_set_all(rtbse_env%hartree_total_ao, CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1243              :                   CALL cp_fm_to_cfm(msourcer=rtbse_env%hartree_curr_ao(1), &
    1244            0 :                                     mtarget=rtbse_env%hartree_total_ao)
    1245              :                END IF
    1246              :             ELSE
    1247         1400 :                IF (harvest_im) THEN
    1248              :                   CALL get_hartree_complex(rtbse_env, rtbse_env%rho_total_ao_scratch, &
    1249          920 :                                            rtbse_env%hartree_total_ao, 1)
    1250              :                ELSE
    1251              :                   ! Hermitian input: real AO-RI on Re(ρ_total) -> cfm with Im ≡ 0.
    1252              :                   CALL get_hartree(rtbse_env, rtbse_env%rho_total_ao_scratch, &
    1253          480 :                                    rtbse_env%hartree_curr_ao(1))
    1254          480 :                   CALL cp_cfm_set_all(rtbse_env%hartree_total_ao, CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1255              :                   CALL cp_fm_to_cfm(msourcer=rtbse_env%hartree_curr_ao(1), &
    1256          480 :                                     mtarget=rtbse_env%hartree_total_ao)
    1257              :                END IF
    1258              :             END IF
    1259              :          END IF
    1260              :       END IF
    1261         2840 :       CALL timestop(handle)
    1262         2840 :    END SUBROUTINE build_shared_sex_and_hartree
    1263              : 
    1264              :    ! **************************************************************************************************
    1265              : !> \brief Assembles the linearized RT-BSE right-hand side in the MO basis and returns it scaled by -i:
    1266              : !>        ham_effective <- -i( [H^0, ρ] + (f_n - f_m)(ΔΣ^SX[Δρ] + ΔV^H[Δρ]) ), i.e. the
    1267              : !>        f(t,ρ) of ∂_t Δρ_mn = -i( (ε_m - ε_n)Δρ_mn + (f_n - f_m)(V^H_mn + Σ^SX_mn) ).
    1268              : !>        ham_reference already carries KS+G0W0 minus the reference SEX/Hartree, so the kernels
    1269              : !>        enter as differences vs the reference. Forks: tda_active (drop B-coupling: one OV kernel
    1270              : !>        pass + VO conjugate) vs full ABBA (OV+VO); n_spin and the KERNEL_RI (rirs_kernel) flag
    1271              : !>        select the AO-RI or RI-RS backend per term.
    1272              : !> \param rtbse_env Entry point of the calculation - contains current state of variables
    1273              : !> \param rho Real and imaginary parts ( + spin) of the density at current time
    1274              : !> \param ham_effective Effective Hamiltonian in the MO basis that is updated in this routine
    1275              : !> \param ispin Spin channel σ being assembled
    1276              : ! **************************************************************************************************
    1277         6640 :    SUBROUTINE update_effective_ham_MO(rtbse_env, rho, ham_effective, ispin)
    1278              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1279              :       TYPE(cp_cfm_type)                                  :: rho, ham_effective
    1280              :       INTEGER                                            :: ispin
    1281              : 
    1282              :       CHARACTER(len=*), PARAMETER :: routineN = 'update_effective_ham_MO'
    1283              : 
    1284              :       INTEGER                                            :: handle, i_global, i_loc, j_global, &
    1285              :                                                             j_loc, ncl, nrl
    1286         3320 :       INTEGER, DIMENSION(:), POINTER                     :: c_idx, r_idx
    1287              :       LOGICAL                                            :: use_hartree, use_sex
    1288              : 
    1289         3320 :       CALL timeset(routineN, handle)
    1290         3320 :       use_hartree = .NOT. rtbse_env%debug_disable_hartree
    1291         3320 :       use_sex = .NOT. rtbse_env%debug_disable_sex
    1292              : 
    1293              :       ! Reset the effective Hamiltonian to KS Hamiltonian + G0W0 - reference SEX - reference Hartree
    1294              :       ! Sets the imaginary part to zero
    1295         3320 :       CALL cp_cfm_to_cfm(rtbse_env%ham_reference(ispin), ham_effective)
    1296              :       ! [H^0, ρ]_mn = (ε_m - ε_n) ρ_mn exactly (H^0 diagonal in active-MO basis). Element-wise
    1297              :       ! local pass replacing the two gemms; ham_effective and rho share fm_struct_mo_active so
    1298              :       ! their local layouts coincide. Covers all (m,n) (OO/VV retained for closed-shell ABBA).
    1299              :       CALL cp_cfm_get_info(matrix=ham_effective, nrow_local=nrl, ncol_local=ncl, &
    1300         3320 :                            row_indices=r_idx, col_indices=c_idx)
    1301        42200 :       DO j_loc = 1, ncl
    1302        38880 :          j_global = c_idx(j_loc)
    1303       272600 :          DO i_loc = 1, nrl
    1304       230400 :             i_global = r_idx(i_loc)
    1305              :             ham_effective%local_data(i_loc, j_loc) = ham_effective%local_data(i_loc, j_loc) &
    1306              :                                             + CMPLX(rtbse_env%eps_active(i_global, ispin) - rtbse_env%eps_active(j_global, ispin), &
    1307       269280 :                                                              0.0_dp, kind=dp)*rho%local_data(i_loc, j_loc)
    1308              :          END DO
    1309              :       END DO
    1310              :       ! Determine the field at current time
    1311         3320 :       IF (rtbse_env%dft_control%apply_efield_field) THEN
    1312              :          CALL cp_abort(__LOCATION__, &
    1313              :                        "Continuous/pulsed E(t) field coupling is not implemented for linearized "// &
    1314              :                        "RT-BSE. Only the delta-kick (impulsive) absorption spectrum is supported; "// &
    1315            0 :                        "use APPLY_DELTA_PULSE.")
    1316              :       ELSE
    1317              :          ! No field
    1318        13280 :          rtbse_env%field(:) = 0.0_dp
    1319              :       END IF
    1320         3320 :       IF (.NOT. rtbse_env%tda_active) THEN
    1321              :          ! ===== ABBA: consume the prebuilt per-spin SEX (sigma_SEX_ao(σ)) and shared bare Hartree
    1322              :          ! (hartree_total_ao). (f_n-f_m) zeros OO/VV and sets OV/VO signs.  Closed shell uses full-ρ
    1323              :          ! input (no mask, reference subtracted via ham_reference); open shell uses OV+VO mask. =====
    1324         1280 :          IF (use_sex) THEN
    1325              :             ! Σ^SX_λσ = -sum_νQ [sum_µ (λµ|Q) Δρ_µν][sum_P (νσ|P) W_PQ]
    1326              :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%sigma_SEX_ao(ispin), &
    1327         1120 :                                                   rtbse_env%sigma_SEX(ispin), ispin)
    1328         1120 :             CALL transform_mo_occupation_factor_diff_cfm(rtbse_env, rtbse_env%sigma_SEX(ispin), ispin)
    1329              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), ham_effective, &
    1330         1120 :                                       CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%sigma_SEX(ispin))
    1331              :          END IF
    1332         1280 :          IF (use_hartree) THEN
    1333              :             ! Builder emits bare V_H (no spin_degeneracy); fold g here (post-occ-factor).
    1334              :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%hartree_total_ao, &
    1335         1120 :                                                   rtbse_env%ham_workspace(1), ispin)
    1336         1120 :             CALL transform_mo_occupation_factor_diff_cfm(rtbse_env, rtbse_env%ham_workspace(1), ispin)
    1337         1120 :             CALL cp_cfm_scale(CMPLX(rtbse_env%spin_degeneracy, 0.0_dp, kind=dp), rtbse_env%ham_workspace(1))
    1338              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), ham_effective, &
    1339         1120 :                                       CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%ham_workspace(1))
    1340              :          END IF
    1341              :       ELSE
    1342              :          ! ----- TDA: drop B-coupling. Consume the OV-input kernels prebuilt by
    1343              :          ! build_shared_sex_and_hartree. K_MO[Δρ_VO] = (K_MO[Δρ_OV])^C (real C + symmetric AO kernels),
    1344              :          ! so evaluate on OV, mask MO to OV, add VO as conjugate transpose.
    1345              :          ! Signs: (f_n - f_m) = -1 on OV, +1 on VO; applied explicitly. -----
    1346              : 
    1347              :          ! SEX: AO->MO, mask OV, stash (assembled after Hartree).
    1348         2040 :          IF (use_sex) THEN
    1349              :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%sigma_SEX_ao(ispin), &
    1350         1880 :                                                   rtbse_env%sigma_SEX(ispin), ispin)
    1351         1880 :             CALL mask_mo_block_cfm(rtbse_env, rtbse_env%sigma_SEX(ispin), ispin, keep_OV=.TRUE.)
    1352              :          END IF
    1353              : 
    1354              :          ! Hartree: hartree_total_ao is bare (no spin_degeneracy); fold g here (post-mask).
    1355              :          ! VO = (OV)^C; rho_delta_mo(ispin) is idle scratch.
    1356         2040 :          IF (use_hartree) THEN
    1357              :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%hartree_total_ao, &
    1358         1880 :                                                   rtbse_env%ham_workspace(1), ispin)
    1359         1880 :             CALL mask_mo_block_cfm(rtbse_env, rtbse_env%ham_workspace(1), ispin, keep_OV=.TRUE.)
    1360         1880 :             CALL cp_cfm_scale(CMPLX(rtbse_env%spin_degeneracy, 0.0_dp, kind=dp), rtbse_env%ham_workspace(1))
    1361              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), ham_effective, &
    1362         1880 :                                       CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%ham_workspace(1))
    1363         1880 :             CALL cp_cfm_transpose(rtbse_env%ham_workspace(1), 'C', rtbse_env%rho_delta_mo(ispin))
    1364              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), ham_effective, &
    1365         1880 :                                       CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%rho_delta_mo(ispin))
    1366              :          END IF
    1367              : 
    1368              :          ! SEX assembled (stashed sigma_SEX, OV-masked). OV sign -1; VO = (OV)^C sign +1.
    1369         2040 :          IF (use_sex) THEN
    1370              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), ham_effective, &
    1371         1880 :                                       CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%sigma_SEX(ispin))
    1372         1880 :             CALL cp_cfm_transpose(rtbse_env%sigma_SEX(ispin), 'C', rtbse_env%rho_delta_mo(ispin))
    1373              :             CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), ham_effective, &
    1374         1880 :                                       CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%rho_delta_mo(ispin))
    1375              :          END IF
    1376              : 
    1377              :          ! Restore rho_ao_scratch to the AO image of the current full rho, so any
    1378              :          ! post-routine consumer (output_mos) sees the same invariant.
    1379         2040 :          CALL transform_mo_to_ao_contravariant_cfm(rtbse_env, rho, rtbse_env%rho_ao_scratch(ispin), ispin)
    1380              :       END IF
    1381              :       ! Return the actual RHS f(t,rho) = -i * A(rho) for RK4
    1382         3320 :       CALL cp_cfm_scale(CMPLX(0.0_dp, -1.0_dp, kind=dp), ham_effective)
    1383              : 
    1384         3320 :       CALL timestop(handle)
    1385         3320 :    END SUBROUTINE update_effective_ham_MO
    1386              : 
    1387              : ! **************************************************************************************************
    1388              : !> \brief Single-spin Liouvillian matvec: apply L^sigma_sigma to one spin's Delta rho_MO and return
    1389              : !>        L * Delta rho_MO in MO basis, OV+VO blocks populated. Drives the n_spin=1 TDA diagnostic
    1390              : !>        and the (n_spin=1) ABBA diagnostic; the open-shell TDA path uses the array routine
    1391              : !>        apply_liouvillian_to_drho instead. Detached from the propagator (does NOT touch
    1392              : !>        rho / rho_orig / ham_effective / ham_reference); only env scratches drho_probe(ispin),
    1393              : !>        rho_ao_scratch, sigma_SEX_ao, hartree_total_ao, ham_workspace(1), sigma_SEX,
    1394              : !>        rho_delta_mo(ispin), real_workspace_mo(1) are used.
    1395              : !>
    1396              : !>        Matrix elements correspond to the Casida-A matrix:
    1397              : !>            L_{ia,jb} = (eps_a - eps_i) delta_{ij} delta_{ab} + (ia|jb) - W_{ij,ab}
    1398              : !>        with no (f_n - f_m) factor (the propagator path applies -1 on OV; we want raw +K).
    1399              : !>        Honors rtbse_env%rirs_kernel to dispatch Hartree to the RI-RS grid kernel
    1400              : !>        (compute_hartree_ri_rs_complex); the SX RI-RS dispatch also reads rirs_kernel inside get_sigma.
    1401              : !>        AO-RI path: get_hartree_complex / get_sigma.
    1402              : !>
    1403              : !> \param rtbse_env RT-BSE environment (TDA, n_spin=1).
    1404              : !> \param drho_in Input Delta rho_MO (mo_active x mo_active complex).
    1405              : !> \param L_drho_out Output L * Delta rho_MO; OV+VO blocks populated; OO/VV zero.
    1406              : !> \param ispin Spin index.
    1407              : !> \author Maximilian Graml (05.26)
    1408              : ! **************************************************************************************************
    1409          944 :    SUBROUTINE apply_liouvillian_to_drho_spin(rtbse_env, drho_in, L_drho_out, ispin)
    1410              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1411              :       TYPE(cp_cfm_type), INTENT(IN)                      :: drho_in
    1412              :       TYPE(cp_cfm_type), INTENT(INOUT)                   :: L_drho_out
    1413              :       INTEGER, INTENT(IN)                                :: ispin
    1414              : 
    1415              :       CHARACTER(len=*), PARAMETER :: routineN = 'apply_liouvillian_to_drho_spin'
    1416              : 
    1417              :       INTEGER                                            :: abs_mo_idx, handle, i_row_global, ii, &
    1418              :                                                             j_col_global, jj, ncol_local, &
    1419              :                                                             nrow_local
    1420          472 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    1421              :       LOGICAL                                            :: use_hartree, use_sex
    1422              : 
    1423          472 :       CALL timeset(routineN, handle)
    1424              : 
    1425              :       ! Mirror update_effective_ham_MO kernel gating: only Hartree + SX are gated
    1426              :       ! (no COH kernel in linRTBSE).
    1427          472 :       use_hartree = .NOT. rtbse_env%debug_disable_hartree
    1428          472 :       use_sex = .NOT. rtbse_env%debug_disable_sex
    1429              :       ! RI-RS dispatch reads rtbse_env%rirs_kernel directly below.
    1430              :       ! V_grid / W0_grid are populated by initialize_hartree_potential /
    1431              :       ! initialize_sex_selfenergy, which run before this diagnostic.
    1432              : 
    1433              :       ! 1. Stage drho_in into drho_probe. TDA: mask to OV (propagator carries only OV
    1434              :       !    and the VO contribution is recovered later via the (.)^C shortcut). ABBA: keep
    1435              :       !    the full OV+VO content - drho_in carries both blocks independently.
    1436          472 :       CALL cp_cfm_to_cfm(drho_in, rtbse_env%drho_probe(ispin))
    1437          472 :       IF (rtbse_env%tda_active) THEN
    1438          330 :          CALL mask_mo_block_cfm(rtbse_env, rtbse_env%drho_probe(ispin), ispin, keep_OV=.TRUE.)
    1439              :       END IF
    1440              : 
    1441              :       ! 2. Project Delta rho_OV (MO -> AO, contravariant).
    1442              :       CALL transform_mo_to_ao_contravariant_cfm(rtbse_env, rtbse_env%drho_probe(ispin), &
    1443          472 :                                                 rtbse_env%rho_ao_scratch(ispin), ispin)
    1444              : 
    1445              :       ! 3. Initialize the output accumulator.
    1446          472 :       CALL cp_cfm_set_all(L_drho_out, CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1447              : 
    1448              :       ! 4. eps_{ai} diagonal contribution, kept before the kernel steps (5/6). drho_in aliases
    1449              :       !    rtbse_env%drho_probe(ispin) at the caller; the kernels' VO Hermitian-conjugate scratch
    1450              :       !    is rho_delta_mo(ispin) (NOT drho_probe), so they no longer corrupt drho_in - but eps
    1451              :       !    first is the clean ordering. Build H_eps as a real diagonal fm in real_workspace_mo(1),
    1452              :       !    convert to cfm in ham_workspace(1), accumulate [drho_in, H_eps] = drho * H - H * drho.
    1453              :       !    On OV: ([drho, H])_{ia} = (eps_a - eps_i) * drho_{ia} = +eps_{ai} * drho_{ia}.
    1454              :       !    On VO: -eps_{ai} * drho_{ai} (sign flips); irrelevant - driver only reads OV.
    1455              :       !    Bare GW eigenvalues (lab frame) so eigenvalues compare 1:1 to bse_full.F.
    1456              :       CALL cp_fm_get_info(rtbse_env%real_workspace_mo(1), &
    1457              :                           nrow_local=nrow_local, ncol_local=ncol_local, &
    1458          472 :                           row_indices=row_indices, col_indices=col_indices)
    1459          472 :       CALL cp_fm_set_all(rtbse_env%real_workspace_mo(1), 0.0_dp)
    1460         3274 :       DO ii = 1, nrow_local
    1461         2802 :          i_row_global = row_indices(ii)
    1462        36718 :          DO jj = 1, ncol_local
    1463        33444 :             j_col_global = col_indices(jj)
    1464        36246 :             IF (i_row_global == j_col_global) THEN
    1465         2802 :                abs_mo_idx = i_row_global + rtbse_env%first_active_mo - 1
    1466              :                rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = &
    1467         2802 :                   rtbse_env%bs_env%eigenval_G0W0(abs_mo_idx, 1, ispin)
    1468              :             END IF
    1469              :          END DO
    1470              :       END DO
    1471              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), &
    1472          472 :                         mtarget=rtbse_env%ham_workspace(1))
    1473              :       ! drho * H_eps
    1474              :       CALL cp_cfm_gemm('N', 'N', rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    1475              :                        CMPLX(1.0_dp, 0.0_dp, kind=dp), drho_in, rtbse_env%ham_workspace(1), &
    1476          472 :                        CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out)
    1477              :       ! -H_eps * drho
    1478              :       CALL cp_cfm_gemm('N', 'N', rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    1479              :                        CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%ham_workspace(1), drho_in, &
    1480          472 :                        CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out)
    1481              : 
    1482              :       ! 5. Hartree contribution: get_hartree_complex (Re/Im split) on AO Delta rho, then
    1483              :       !    AO->MO covariant, mask OV+VO, accumulate +spin_degeneracy on OV and on VO.
    1484              :       !    No (f_n - f_m) factor: we want raw +K (Casida convention), not the propagator's -K_OV.
    1485              :       !    VO contribution comes from the Hermitian conjugate of the OV result (P2 in
    1486              :       !    rt_bse_pitfalls_physics.md): for real C_active + AO-pair-symmetric kernel,
    1487              :       !    K_MO[Delta rho_VO] = (K_MO[Delta rho_OV])^C.
    1488          472 :       IF (use_hartree) THEN
    1489          428 :          IF (rtbse_env%rirs_kernel) THEN
    1490              :             ! V^H_λσ = sum_l φ_λ(r_l) v_l φ_σ(r_l), v_l = sum_l' V_ll' n_l'  (RI-RS)
    1491              :             CALL compute_hartree_ri_rs_complex(rtbse_env%bs_env, rtbse_env%rho_ao_scratch(ispin), &
    1492          132 :                                                rtbse_env%hartree_total_ao)
    1493              :          ELSE
    1494              :             ! V^H_λσ = sum_PQ (λσ|P) V_PQ [sum_µν (µν|Q) Δρ_µν]  (AO-RI)
    1495              :             CALL get_hartree_complex(rtbse_env, rtbse_env%rho_ao_scratch(ispin), &
    1496          296 :                                      rtbse_env%hartree_total_ao, ispin)
    1497              :          END IF
    1498              :          CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%hartree_total_ao, &
    1499          428 :                                                rtbse_env%ham_workspace(1), ispin)
    1500              :          CALL add_K_MO_to_L_drho(rtbse_env, rtbse_env%ham_workspace(1), L_drho_out, &
    1501          428 :                                  CMPLX(rtbse_env%spin_degeneracy, 0.0_dp, kind=dp), ispin)
    1502              :       END IF
    1503              : 
    1504              :       ! 6. Screened-exchange contribution: get_sigma is already complex-aware. The
    1505              :       !    -1.0_dp factor passed to get_sigma builds the -W contribution; we then add
    1506              :       !    +1.0 here (no occupation-factor flip), giving raw K^SX = -W as required by
    1507              :       !    K = (ia|jb) - W_{ij,ab} (Casida convention with +K).
    1508          472 :       IF (use_sex) THEN
    1509              :          ! Σ^SX_λσ = -sum_νQ [sum_µ (λµ|Q) Δρ_µν][sum_P (νσ|P) W_PQ]
    1510          428 :          CALL get_sigma(rtbse_env, rtbse_env%sigma_SEX_ao(ispin), -1.0_dp, rtbse_env%rho_ao_scratch(ispin))
    1511              :          CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%sigma_SEX_ao(ispin), &
    1512          428 :                                                rtbse_env%sigma_SEX(ispin), ispin)
    1513              :          CALL add_K_MO_to_L_drho(rtbse_env, rtbse_env%sigma_SEX(ispin), L_drho_out, &
    1514          428 :                                  CMPLX(1.0_dp, 0.0_dp, kind=dp), ispin)
    1515              :       END IF
    1516              : 
    1517          472 :       CALL timestop(handle)
    1518          472 :    END SUBROUTINE apply_liouvillian_to_drho_spin
    1519              : 
    1520              : ! **************************************************************************************************
    1521              : !> \brief Adds the MO-domain kernel contribution K_MO * Delta rho to L_drho_out, branching on
    1522              : !>        rtbse_env%tda_active. TDA: masks K_MO to OV in place, adds scale*K_MO on OV, then
    1523              : !>        builds the VO contribution as (K_MO_OV)^C and adds it (valid only under the TDA
    1524              : !>        assumption Delta rho_VO = (Delta rho_OV)^C). ABBA: adds the full mo_active x mo_active
    1525              : !>        K_MO directly - OV+VO blocks are computed naturally from the full OV+VO input.
    1526              : !>        K_MO is INTENT(INOUT); in the TDA branch it is masked in place (treat as scratch
    1527              : !>        after this call). rho_delta_mo(ispin) is used as the VO-transpose scratch in the TDA
    1528              : !>        branch (NOT drho_probe, which the open-shell array driver keeps as the live probe).
    1529              : !> \param rtbse_env RT-BSE environment.
    1530              : !> \param K_MO Kernel contribution in MO basis (mo_active x mo_active). Scratched in TDA branch.
    1531              : !> \param L_drho_out Accumulator (mo_active x mo_active).
    1532              : !> \param scale Complex scale factor (spin_degeneracy for Hartree, 1.0 for SX).
    1533              : !> \param ispin Spin index.
    1534              : !> \author Maximilian Graml (05.26)
    1535              : ! **************************************************************************************************
    1536         1560 :    SUBROUTINE add_K_MO_to_L_drho(rtbse_env, K_MO, L_drho_out, scale, ispin)
    1537              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1538              :       TYPE(cp_cfm_type), INTENT(INOUT)                   :: K_MO, L_drho_out
    1539              :       COMPLEX(kind=dp), INTENT(IN)                       :: scale
    1540              :       INTEGER, INTENT(IN)                                :: ispin
    1541              : 
    1542              :       CHARACTER(len=*), PARAMETER :: routineN = 'add_K_MO_to_L_drho'
    1543              : 
    1544              :       INTEGER                                            :: handle
    1545              : 
    1546         1560 :       CALL timeset(routineN, handle)
    1547              : 
    1548         1560 :       IF (rtbse_env%tda_active) THEN
    1549         1144 :          CALL mask_mo_block_cfm(rtbse_env, K_MO, ispin, keep_OV=.TRUE.)
    1550         1144 :          CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out, scale, K_MO)
    1551         1144 :          CALL cp_cfm_transpose(K_MO, 'C', rtbse_env%rho_delta_mo(ispin))
    1552         1144 :          CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out, scale, rtbse_env%rho_delta_mo(ispin))
    1553              :       ELSE
    1554          416 :          CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out, scale, K_MO)
    1555              :       END IF
    1556              : 
    1557         1560 :       CALL timestop(handle)
    1558         1560 :    END SUBROUTINE add_K_MO_to_L_drho
    1559              : 
    1560              : ! **************************************************************************************************
    1561              : !> \brief Open-shell TDA Liouvillian matvec: apply the joint spin-block L_TDA to a per-spin probe
    1562              : !>        and return L * Delta rho on every spin block. A probe on spin sigma feeds the diagonal
    1563              : !>        block A^{sigma,sigma} (eps + Coulomb + SX) AND the off-diagonal Coulomb block
    1564              : !>        A^{sigma',sigma} for every other output spin sigma' (cross-spin Hartree - the term that
    1565              : !>        produces the singlet/triplet split). AO-RI only: the Phase-D guard forbids RIRS
    1566              : !>        for n_spin>1, so there is no RIRS branch here; the n_spin=1 diagnostic routes
    1567              : !>        through apply_liouvillian_to_drho_spin instead (which keeps the RIRS path).
    1568              : !> \param rtbse_env RT-BSE environment (TDA).
    1569              : !> \param drho_in Per-spin probe Delta rho_MO (mo_active x mo_active each); zero on non-probed spins.
    1570              : !> \param L_drho_out Per-spin output L * Delta rho; OV+VO blocks populated, OO/VV zero.
    1571              : !> \author Maximilian Graml
    1572              : ! **************************************************************************************************
    1573          352 :    SUBROUTINE apply_liouvillian_to_drho(rtbse_env, drho_in, L_drho_out)
    1574              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1575              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: drho_in, L_drho_out
    1576              : 
    1577              :       CHARACTER(len=*), PARAMETER :: routineN = 'apply_liouvillian_to_drho'
    1578              : 
    1579              :       INTEGER                                            :: abs_mo_idx, handle, i_row_global, ii, &
    1580              :                                                             isp, isp_out, j_col_global, jj, &
    1581              :                                                             ncol_local, nrow_local
    1582          176 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    1583              :       LOGICAL                                            :: use_hartree, use_sex
    1584              : 
    1585          176 :       CALL timeset(routineN, handle)
    1586              : 
    1587          176 :       use_hartree = .NOT. rtbse_env%debug_disable_hartree
    1588          176 :       use_sex = .NOT. rtbse_env%debug_disable_sex
    1589              : 
    1590              :       ! Zero every output spin block before accumulating.
    1591          528 :       DO isp = 1, rtbse_env%n_spin
    1592          528 :          CALL cp_cfm_set_all(L_drho_out(isp), CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1593              :       END DO
    1594              : 
    1595              :       ! eps^sigma commutator -> diagonal block only (eps is spin-diagonal). MUST precede the
    1596              :       ! kernel steps (add_K_MO_to_L_drho scratches MO buffers). Lab-frame bare GW eigenvalues
    1597              :       ! so they compare 1:1 to bse_full_diag.F.
    1598              :       CALL cp_fm_get_info(rtbse_env%real_workspace_mo(1), &
    1599              :                           nrow_local=nrow_local, ncol_local=ncol_local, &
    1600          176 :                           row_indices=row_indices, col_indices=col_indices)
    1601          528 :       DO isp = 1, rtbse_env%n_spin
    1602          352 :          CALL cp_fm_set_all(rtbse_env%real_workspace_mo(1), 0.0_dp)
    1603         2464 :          DO ii = 1, nrow_local
    1604         2112 :             i_row_global = row_indices(ii)
    1605        27808 :             DO jj = 1, ncol_local
    1606        25344 :                j_col_global = col_indices(jj)
    1607        27456 :                IF (i_row_global == j_col_global) THEN
    1608         2112 :                   abs_mo_idx = i_row_global + rtbse_env%first_active_mo - 1
    1609              :                   rtbse_env%real_workspace_mo(1)%local_data(ii, jj) = &
    1610         2112 :                      rtbse_env%bs_env%eigenval_G0W0(abs_mo_idx, 1, isp)
    1611              :                END IF
    1612              :             END DO
    1613              :          END DO
    1614              :          CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), &
    1615          352 :                            mtarget=rtbse_env%ham_workspace(1))
    1616              :          ! [drho, H_eps] = drho * H_eps - H_eps * drho
    1617              :          CALL cp_cfm_gemm('N', 'N', rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    1618              :                           CMPLX(1.0_dp, 0.0_dp, kind=dp), drho_in(isp), rtbse_env%ham_workspace(1), &
    1619          352 :                           CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out(isp))
    1620              :          CALL cp_cfm_gemm('N', 'N', rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    1621              :                           CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%ham_workspace(1), drho_in(isp), &
    1622          528 :                           CMPLX(1.0_dp, 0.0_dp, kind=dp), L_drho_out(isp))
    1623              :       END DO
    1624              : 
    1625              :       ! Project per-spin OV-masked rho_ao(sigma) (consumed by SX) and build the single cross-spin
    1626              :       ! Hartree V_H[spin_degeneracy * sum_sigma rho_ao(sigma)] (Coulomb is spin-blind).
    1627              :       ! build_shared_hartree_ao does both; skip only when neither kernel is active.
    1628          176 :       IF (use_hartree .OR. use_sex) THEN
    1629          176 :          CALL build_shared_hartree_ao(rtbse_env, drho_in, keep_ovvo=.FALSE.)
    1630              :       END IF
    1631              : 
    1632              :       ! Hartree: the one shared V_H read back with each output spin's C fills the diagonal
    1633              :       ! A^{sigma,sigma} AND the off-diagonal A^{sigma',sigma}. Coeff 1.0 (spin_degeneracy lives in
    1634              :       ! the summed density); no (f_n - f_m) factor (Casida +K convention).
    1635          176 :       IF (use_hartree) THEN
    1636          528 :          DO isp_out = 1, rtbse_env%n_spin
    1637              :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%hartree_total_ao, &
    1638          352 :                                                   rtbse_env%ham_workspace(1), isp_out)
    1639              :             CALL add_K_MO_to_L_drho(rtbse_env, rtbse_env%ham_workspace(1), L_drho_out(isp_out), &
    1640          528 :                                     CMPLX(1.0_dp, 0.0_dp, kind=dp), isp_out)
    1641              :          END DO
    1642              :       END IF
    1643              : 
    1644              :       ! Screened exchange: spin-diagonal (W^sigma acts only within spin sigma). get_sigma builds
    1645              :       ! -W; add raw +1 (no occupation-factor flip), giving K^SX = -W.
    1646          176 :       IF (use_sex) THEN
    1647          528 :          DO isp = 1, rtbse_env%n_spin
    1648              :             ! Σ^SX_λσ = -sum_νQ [sum_µ (λµ|Q) Δρ_µν][sum_P (νσ|P) W_PQ]  (per spin)
    1649          352 :             CALL get_sigma(rtbse_env, rtbse_env%sigma_SEX_ao(isp), -1.0_dp, rtbse_env%rho_ao_scratch(isp))
    1650              :             CALL transform_ao_to_mo_covariant_cfm(rtbse_env, rtbse_env%sigma_SEX_ao(isp), &
    1651          352 :                                                   rtbse_env%sigma_SEX(isp), isp)
    1652              :             CALL add_K_MO_to_L_drho(rtbse_env, rtbse_env%sigma_SEX(isp), L_drho_out(isp), &
    1653          528 :                                     CMPLX(1.0_dp, 0.0_dp, kind=dp), isp)
    1654              :          END DO
    1655              :       END IF
    1656              : 
    1657          176 :       CALL timestop(handle)
    1658          176 :    END SUBROUTINE apply_liouvillian_to_drho
    1659              : 
    1660              : ! **************************************************************************************************
    1661              : !> \brief Public entry for the Liouvillian eigenvalue diagnostic.
    1662              : !>        Dispatches to the TDA branch (Casida-A via cp_cfm_heevd) or the ABBA branch
    1663              : !>        (Furche reduction via cp_cfm_power) based on rtbse_env%tda_active.
    1664              : !>        Called once at job init from run_propagation_linearized_bse, gated on
    1665              : !>        rtbse_env%diagnose_liouvillian_eig (n_spin = 1 enforced at env creation).
    1666              : !> \param rtbse_env RT-BSE environment with diagnostic scratch already allocated.
    1667              : !> \author Maximilian Graml (05.26)
    1668              : ! **************************************************************************************************
    1669           52 :    SUBROUTINE diagnose_liouvillian_eigenvalues(rtbse_env)
    1670              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1671              : 
    1672              :       CHARACTER(len=*), PARAMETER :: routineN = 'diagnose_liouvillian_eigenvalues'
    1673              : 
    1674              :       INTEGER                                            :: handle
    1675              : 
    1676           52 :       CALL timeset(routineN, handle)
    1677              : 
    1678           52 :       IF (rtbse_env%tda_active) THEN
    1679           36 :          CALL diagnose_TDA_liouvillian(rtbse_env)
    1680              :       ELSE
    1681           16 :          CALL diagnose_ABBA_liouvillian(rtbse_env)
    1682              :       END IF
    1683              : 
    1684           52 :       CALL timestop(handle)
    1685           52 :    END SUBROUTINE diagnose_liouvillian_eigenvalues
    1686              : 
    1687              : ! **************************************************************************************************
    1688              : !> \brief TDA branch of the Liouvillian eigenvalue diagnostic. Probes the Liouvillian with
    1689              : !>        canonical OV unit vectors and assembles the joint spin-block Casida-A matrix as L_pairs
    1690              : !>        (N_OV_joint x N_OV_joint, spin blocks stacked), then diagonalizes via cp_cfm_heevd.
    1691              : !>        A probe on spin sigma fills its column block and the response on every output spin lands
    1692              : !>        in that spin's row block (the off-diagonal blocks carry the cross-spin Coulomb that
    1693              : !>        splits singlet/triplet). The matvec is dispatched on n_spin: n_spin=1 uses the
    1694              : !>        single-spin apply_liouvillian_to_drho_spin (keeps the RIRS path, bit-identical to the
    1695              : !>        closed-shell baseline); n_spin=2 uses the AO-RI array apply_liouvillian_to_drho.
    1696              : !>        Eigenvalues go to stdout (RTBSE|) and to the LIOUVILLIAN_EIG .dat file. Detached from
    1697              : !>        RK4 state. Called from the dispatcher when tda_active=.TRUE.
    1698              : !> \param rtbse_env RT-BSE environment with TDA diagnostic scratch already allocated.
    1699              : !> \author Maximilian Graml (05.26)
    1700              : ! **************************************************************************************************
    1701           36 :    SUBROUTINE diagnose_TDA_liouvillian(rtbse_env)
    1702              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1703              : 
    1704              :       CHARACTER(len=*), PARAMETER :: routineN = 'diagnose_TDA_liouvillian'
    1705              : 
    1706           36 :       COMPLEX(kind=dp), ALLOCATABLE, DIMENSION(:, :)     :: ov_block
    1707              :       INTEGER                                            :: b, eig_unit, handle, j, k_col, k_local, &
    1708              :                                                             n, n_ov_joint, sigma_out, sigma_probe
    1709           36 :       INTEGER, ALLOCATABLE, DIMENSION(:)                 :: n_act_occ, n_act_virt, n_ov, off
    1710              :       REAL(kind=dp)                                      :: residual_max
    1711              :       TYPE(cp_logger_type), POINTER                      :: logger
    1712              : 
    1713           36 :       CALL timeset(routineN, handle)
    1714           36 :       logger => cp_get_default_logger()
    1715              : 
    1716              :       ! Per-spin OV counts + offsets into the stacked joint Liouvillian. off(1)=0,
    1717              :       ! off(2)=n_ov(1); n_ov_joint = sum_sigma n_ov(sigma). n_spin=1 -> single block.
    1718              :       ALLOCATE (n_act_occ(rtbse_env%n_spin), n_act_virt(rtbse_env%n_spin), &
    1719          216 :                 n_ov(rtbse_env%n_spin), off(rtbse_env%n_spin))
    1720           36 :       n_ov_joint = 0
    1721           78 :       DO sigma_probe = 1, rtbse_env%n_spin
    1722           42 :          n_act_occ(sigma_probe) = rtbse_env%n_occ(sigma_probe) - rtbse_env%first_active_mo + 1
    1723           42 :          n_act_virt(sigma_probe) = rtbse_env%last_active_mo - rtbse_env%n_occ(sigma_probe)
    1724           42 :          n_ov(sigma_probe) = n_act_occ(sigma_probe)*n_act_virt(sigma_probe)
    1725           42 :          off(sigma_probe) = n_ov_joint
    1726           78 :          n_ov_joint = n_ov_joint + n_ov(sigma_probe)
    1727              :       END DO
    1728              : 
    1729           36 :       IF (rtbse_env%unit_nr > 0) THEN
    1730           18 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE| ----- TDA Liouvillian diagnostic -----'
    1731              :          WRITE (rtbse_env%unit_nr, '(A,I0,A,I0)') &
    1732           18 :             ' RTBSE| n_spin = ', rtbse_env%n_spin, ', joint N_OV = ', n_ov_joint
    1733           18 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE|'
    1734              :       END IF
    1735              : 
    1736              :       ! Single joint block to the .dat file (REWIND). ignore_should_output=.TRUE. so this fires
    1737              :       ! at init regardless of MD-iteration cadence.
    1738              :       eig_unit = cp_print_key_unit_nr(logger, rtbse_env%eig_section, &
    1739              :                                       extension=".dat", &
    1740              :                                       file_form="FORMATTED", &
    1741              :                                       file_position="REWIND", &
    1742           36 :                                       ignore_should_output=.TRUE.)
    1743           36 :       IF (eig_unit > 0) THEN
    1744           18 :          WRITE (eig_unit, '(A)') '# Joint spin-block TDA Liouvillian eigenvalues'
    1745           18 :          IF (rtbse_env%n_spin == 1) THEN
    1746           15 :             WRITE (eig_unit, '(A,I0,A,I0)') '# n_spin = ', rtbse_env%n_spin, ', N_OV = ', n_ov(1)
    1747              :          ELSE
    1748            3 :             WRITE (eig_unit, '(A,I0,A,I0,A,I0)') '# n_spin = ', rtbse_env%n_spin, &
    1749            6 :                ', N_OV(1) = ', n_ov(1), ', N_OV(2) = ', n_ov(2)
    1750              :          END IF
    1751              :       END IF
    1752              : 
    1753              :       ! Assemble the joint Casida-A matrix. A probe on spin sigma_probe with canonical OV unit
    1754              :       ! vector e_{(j,b)} fills column off(sigma_probe)+k_local; the response on each output spin
    1755              :       ! sigma_out lands in its row block [off(sigma_out)+1 .. +n_ov(sigma_out)]. Block-level
    1756              :       ! transfers keep this at O(N_OV_joint) collective ops. Column-major OV index
    1757              :       ! k_local = (b_local-1)*n_act_occ + j_local matches the Fortran layout of ov_block, so
    1758              :       ! RESHAPE without padding gives the right (n_ov, 1) column.
    1759           78 :       DO sigma_probe = 1, rtbse_env%n_spin
    1760          540 :          DO b = rtbse_env%n_occ(sigma_probe) + 1, rtbse_env%last_active_mo
    1761          966 :             DO j = rtbse_env%first_active_mo, rtbse_env%n_occ(sigma_probe)
    1762              :                k_local = (b - rtbse_env%n_occ(sigma_probe) - 1)*n_act_occ(sigma_probe) + &
    1763          462 :                          (j - rtbse_env%first_active_mo + 1)
    1764          462 :                k_col = off(sigma_probe) + k_local
    1765              : 
    1766         1056 :                DO sigma_out = 1, rtbse_env%n_spin
    1767         1056 :                   CALL cp_cfm_set_all(rtbse_env%drho_probe(sigma_out), CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1768              :                END DO
    1769              :                CALL cp_cfm_set_element(rtbse_env%drho_probe(sigma_probe), &
    1770              :                                        j - rtbse_env%first_active_mo + 1, &
    1771              :                                        b - rtbse_env%first_active_mo + 1, &
    1772          462 :                                        CMPLX(1.0_dp, 0.0_dp, kind=dp))
    1773              : 
    1774              :                ! n_spin=1 keeps the single-spin matvec (RIRS-capable, bit-identical baseline);
    1775              :                ! n_spin=2 is AO-RI-only (Phase-D guard) -> cross-spin array matvec.
    1776          462 :                IF (rtbse_env%n_spin == 1) THEN
    1777              :                   CALL apply_liouvillian_to_drho_spin(rtbse_env, rtbse_env%drho_probe(1), &
    1778          330 :                                                       rtbse_env%L_drho(1), 1)
    1779              :                ELSE
    1780          132 :                   CALL apply_liouvillian_to_drho(rtbse_env, rtbse_env%drho_probe, rtbse_env%L_drho)
    1781              :                END IF
    1782              : 
    1783              :                ! Stack each output spin's OV response into the joint column.
    1784         1518 :                DO sigma_out = 1, rtbse_env%n_spin
    1785         2376 :                   ALLOCATE (ov_block(n_act_occ(sigma_out), n_act_virt(sigma_out)))
    1786              :                   CALL cp_cfm_get_submatrix(rtbse_env%L_drho(sigma_out), ov_block, &
    1787              :                                             start_row=1, start_col=n_act_occ(sigma_out) + 1, &
    1788          594 :                                             n_rows=n_act_occ(sigma_out), n_cols=n_act_virt(sigma_out))
    1789              :                   CALL cp_cfm_set_submatrix(rtbse_env%L_pairs, &
    1790              :                                             RESHAPE(ov_block, [n_ov(sigma_out), 1]), &
    1791              :                                             start_row=off(sigma_out) + 1, start_col=k_col, &
    1792         1782 :                                             n_rows=n_ov(sigma_out), n_cols=1)
    1793         1056 :                   DEALLOCATE (ov_block)
    1794              :                END DO
    1795              :             END DO
    1796              :          END DO
    1797              :       END DO
    1798              : 
    1799              :       ! Hermitian residual on the joint matrix: real-orbital BSE => L real symmetric, so
    1800              :       ! ||L - L^H||_max should be at the FP floor. D = L^H - L into eigvecs_pairs (idle here;
    1801              :       ! heevd overwrites it), then its max-element norm via the BLACS-native pzlange path.
    1802           36 :       CALL cp_cfm_transpose(rtbse_env%L_pairs, 'C', rtbse_env%eigvecs_pairs)
    1803              :       CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%eigvecs_pairs, &
    1804           36 :                                 CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%L_pairs)
    1805           36 :       residual_max = cp_cfm_norm(rtbse_env%eigvecs_pairs, 'M')
    1806           36 :       IF (rtbse_env%unit_nr > 0) THEN
    1807              :          WRITE (rtbse_env%unit_nr, '(A,ES16.6)') &
    1808           18 :             ' RTBSE| Hermitian residual ||L - L^H||_max = ', residual_max
    1809           18 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE|'
    1810              :       END IF
    1811           36 :       IF (residual_max > 1.0E-6_dp) THEN
    1812            0 :          CPABORT("Liouvillian Hermitian residual > 1e-6 - check kernel signs / symmetry.")
    1813              :       END IF
    1814              : 
    1815              :       ! Diagonalize the joint matrix. cp_cfm_heevd returns ascending real eigenvalues.
    1816              :       CALL cp_cfm_heevd(rtbse_env%L_pairs, rtbse_env%eigvecs_pairs, &
    1817           36 :                         rtbse_env%eigenvalues_liouvillian)
    1818              : 
    1819              :       ! Stdout table (eV, F12.4 right-aligned to col 80, L-7) + .dat (a.u. + eV).
    1820           36 :       IF (rtbse_env%unit_nr > 0) THEN
    1821              :          WRITE (rtbse_env%unit_nr, '(A,T26,A,T59,A)') &
    1822           18 :             ' RTBSE|', "Excitation index n", "Excitation energy (eV)"
    1823          249 :          DO n = 1, n_ov_joint
    1824              :             WRITE (rtbse_env%unit_nr, '(A,T40,I4,T69,F12.4)') &
    1825          249 :                ' RTBSE|', n, rtbse_env%eigenvalues_liouvillian(n)*evolt
    1826              :          END DO
    1827           18 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE|'
    1828              :       END IF
    1829           36 :       IF (eig_unit > 0) THEN
    1830           18 :          WRITE (eig_unit, '(A)') '#   n          Omega [a.u.]               Omega [eV]'
    1831          249 :          DO n = 1, n_ov_joint
    1832          231 :             WRITE (eig_unit, '(I5,4X,ES24.14E3,4X,ES24.14E3)') n, &
    1833          231 :                rtbse_env%eigenvalues_liouvillian(n), &
    1834          480 :                rtbse_env%eigenvalues_liouvillian(n)*evolt
    1835              :          END DO
    1836              :       END IF
    1837              : 
    1838           36 :       CALL cp_print_key_finished_output(eig_unit, logger, rtbse_env%eig_section)
    1839              : 
    1840           36 :       DEALLOCATE (n_act_occ, n_act_virt, n_ov, off)
    1841              : 
    1842           36 :       CALL timestop(handle)
    1843           72 :    END SUBROUTINE diagnose_TDA_liouvillian
    1844              : 
    1845              : ! **************************************************************************************************
    1846              : !> \brief ABBA branch of the Liouvillian eigenvalue diagnostic. Assembles the joint spin-block
    1847              : !>        A and B by probing apply_liouvillian_to_drho with canonical OV unit vectors: a probe on
    1848              : !>        spin sigma_probe fills joint column off(sigma_probe)+k_local; each output spin's OV
    1849              : !>        response -> A, VO response -> -B^* (recovered by sign-flip + conjugation). One joint
    1850              : !>        Furche reduction follows: (A-B)>0 gate (independent cp_cfm_heevd), (A-B)^{1/2} via
    1851              : !>        cp_cfm_power, C = (A-B)^{1/2}(A+B)(A-B)^{1/2}, cp_cfm_heevd, Ω_n = √(C). The
    1852              : !>        matvec is dispatched on n_spin: n_spin=1 -> apply_liouvillian_to_drho_spin (RIRS-capable,
    1853              : !>        bit-identical to the closed-shell baseline); n_spin=2 -> the AO-RI cross-spin array
    1854              : !>        apply_liouvillian_to_drho. For n_spin=1 the routine reduces to the single-block path.
    1855              : !>        Output: a single joint spectrum (stdout RTBSE| + LIOUVILLIAN_EIG .dat). All eigenvalues
    1856              : !>        are retained, including optically dark triplet modes (the kernel-correctness gate).
    1857              : !> \param rtbse_env RT-BSE environment with ABBA diagnostic scratch allocated.
    1858              : !> \author Maximilian Graml (05.26)
    1859              : ! **************************************************************************************************
    1860           16 :    SUBROUTINE diagnose_ABBA_liouvillian(rtbse_env)
    1861              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    1862              : 
    1863              :       CHARACTER(len=*), PARAMETER :: routineN = 'diagnose_ABBA_liouvillian'
    1864              : 
    1865           16 :       COMPLEX(kind=dp), ALLOCATABLE, DIMENSION(:, :)     :: ov_block, vo_block
    1866           16 :       COMPLEX(kind=dp), DIMENSION(:, :), POINTER         :: B_local
    1867              :       INTEGER                                            :: b, eig_unit, handle, j, k_col, k_local, &
    1868              :                                                             n, n_ov_joint, sigma_out, sigma_probe
    1869           16 :       INTEGER, ALLOCATABLE, DIMENSION(:)                 :: n_act_occ, n_act_virt, n_ov, off
    1870              :       REAL(kind=dp)                                      :: lambda_min_AmB, residual_A, residual_B
    1871              :       TYPE(cp_logger_type), POINTER                      :: logger
    1872              : 
    1873           16 :       CALL timeset(routineN, handle)
    1874           16 :       logger => cp_get_default_logger()
    1875              : 
    1876              :       ! Per-spin OV counts + offsets into the stacked joint A/B (same layout as the TDA
    1877              :       ! diagnostic). off(1)=0, off(2)=n_ov(1); n_ov_joint = sum_sigma n_ov(sigma). For
    1878              :       ! n_spin=1 this is the single block, bit-identical to the closed-shell ABBA path.
    1879              :       ALLOCATE (n_act_occ(rtbse_env%n_spin), n_act_virt(rtbse_env%n_spin), &
    1880           96 :                 n_ov(rtbse_env%n_spin), off(rtbse_env%n_spin))
    1881           16 :       n_ov_joint = 0
    1882           34 :       DO sigma_probe = 1, rtbse_env%n_spin
    1883           18 :          n_act_occ(sigma_probe) = rtbse_env%n_occ(sigma_probe) - rtbse_env%first_active_mo + 1
    1884           18 :          n_act_virt(sigma_probe) = rtbse_env%last_active_mo - rtbse_env%n_occ(sigma_probe)
    1885           18 :          n_ov(sigma_probe) = n_act_occ(sigma_probe)*n_act_virt(sigma_probe)
    1886           18 :          off(sigma_probe) = n_ov_joint
    1887           34 :          n_ov_joint = n_ov_joint + n_ov(sigma_probe)
    1888              :       END DO
    1889              : 
    1890           16 :       IF (rtbse_env%unit_nr > 0) THEN
    1891            8 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE| ----- ABBA Liouvillian diagnostic -----'
    1892              :          WRITE (rtbse_env%unit_nr, '(A,I0,A,I0)') &
    1893            8 :             ' RTBSE| n_spin = ', rtbse_env%n_spin, ', joint N_OV = ', n_ov_joint
    1894            8 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE|'
    1895              :       END IF
    1896              : 
    1897              :       eig_unit = cp_print_key_unit_nr(logger, rtbse_env%eig_section, &
    1898              :                                       extension=".dat", &
    1899              :                                       file_form="FORMATTED", &
    1900              :                                       file_position="REWIND", &
    1901           16 :                                       ignore_should_output=.TRUE.)
    1902           16 :       IF (eig_unit > 0) THEN
    1903            8 :          WRITE (eig_unit, '(A)') '# Joint spin-block ABBA Liouvillian eigenvalues'
    1904            8 :          IF (rtbse_env%n_spin == 1) THEN
    1905            7 :             WRITE (eig_unit, '(A,I0,A,I0)') '# n_spin = ', rtbse_env%n_spin, ', N_OV = ', n_ov(1)
    1906              :          ELSE
    1907            1 :             WRITE (eig_unit, '(A,I0,A,I0,A,I0)') '# n_spin = ', rtbse_env%n_spin, &
    1908            2 :                ', N_OV(1) = ', n_ov(1), ', N_OV(2) = ', n_ov(2)
    1909              :          END IF
    1910              :       END IF
    1911              : 
    1912              :       ! Assemble joint A and B. Probe on spin sigma_probe, OV pair (j,b) -> joint column
    1913              :       ! k_col = off(sigma_probe)+k_local (column-major k_local, as TDA). Each output spin's
    1914              :       ! OV response -> A rows [off(sigma_out)+1 ..]; VO response -> B rows (TRANSPOSE to OV
    1915              :       ! layout), recovered as -B^* below. The n_spin=2 array matvec produces the full OV+VO
    1916              :       ! readout (add_K_MO_to_L_drho .NOT.tda_active branch); one cross-spin Hartree fills
    1917              :       ! both A^{s,s'} and B^{s,s'} Coulomb; SX stays spin-diagonal.
    1918           34 :       DO sigma_probe = 1, rtbse_env%n_spin
    1919          220 :          DO b = rtbse_env%n_occ(sigma_probe) + 1, rtbse_env%last_active_mo
    1920          390 :             DO j = rtbse_env%first_active_mo, rtbse_env%n_occ(sigma_probe)
    1921              :                k_local = (b - rtbse_env%n_occ(sigma_probe) - 1)*n_act_occ(sigma_probe) + &
    1922          186 :                          (j - rtbse_env%first_active_mo + 1)
    1923          186 :                k_col = off(sigma_probe) + k_local
    1924              : 
    1925          416 :                DO sigma_out = 1, rtbse_env%n_spin
    1926          416 :                   CALL cp_cfm_set_all(rtbse_env%drho_probe(sigma_out), CMPLX(0.0_dp, 0.0_dp, kind=dp))
    1927              :                END DO
    1928              :                CALL cp_cfm_set_element(rtbse_env%drho_probe(sigma_probe), &
    1929              :                                        j - rtbse_env%first_active_mo + 1, &
    1930              :                                        b - rtbse_env%first_active_mo + 1, &
    1931          186 :                                        CMPLX(1.0_dp, 0.0_dp, kind=dp))
    1932              : 
    1933          186 :                IF (rtbse_env%n_spin == 1) THEN
    1934              :                   CALL apply_liouvillian_to_drho_spin(rtbse_env, rtbse_env%drho_probe(1), &
    1935          142 :                                                       rtbse_env%L_drho(1), 1)
    1936              :                ELSE
    1937           44 :                   CALL apply_liouvillian_to_drho(rtbse_env, rtbse_env%drho_probe, rtbse_env%L_drho)
    1938              :                END IF
    1939              : 
    1940          602 :                DO sigma_out = 1, rtbse_env%n_spin
    1941          920 :                   ALLOCATE (ov_block(n_act_occ(sigma_out), n_act_virt(sigma_out)))
    1942          690 :                   ALLOCATE (vo_block(n_act_virt(sigma_out), n_act_occ(sigma_out)))
    1943              :                   CALL cp_cfm_get_submatrix(rtbse_env%L_drho(sigma_out), ov_block, &
    1944              :                                             start_row=1, start_col=n_act_occ(sigma_out) + 1, &
    1945          230 :                                             n_rows=n_act_occ(sigma_out), n_cols=n_act_virt(sigma_out))
    1946              :                   CALL cp_cfm_set_submatrix(rtbse_env%A_mat, &
    1947              :                                             RESHAPE(ov_block, [n_ov(sigma_out), 1]), &
    1948              :                                             start_row=off(sigma_out) + 1, start_col=k_col, &
    1949          690 :                                             n_rows=n_ov(sigma_out), n_cols=1)
    1950              :                   ! TRANSPOSE puts vo_block in (n_act_occ, n_act_virt) layout, matching the OV pack.
    1951              :                   CALL cp_cfm_get_submatrix(rtbse_env%L_drho(sigma_out), vo_block, &
    1952              :                                             start_row=n_act_occ(sigma_out) + 1, start_col=1, &
    1953          230 :                                             n_rows=n_act_virt(sigma_out), n_cols=n_act_occ(sigma_out))
    1954              :                   CALL cp_cfm_set_submatrix(rtbse_env%B_mat, &
    1955              :                                             RESHAPE(TRANSPOSE(vo_block), [n_ov(sigma_out), 1]), &
    1956              :                                             start_row=off(sigma_out) + 1, start_col=k_col, &
    1957          690 :                                             n_rows=n_ov(sigma_out), n_cols=1)
    1958          416 :                   DEALLOCATE (ov_block, vo_block)
    1959              :                END DO
    1960              :             END DO
    1961              :          END DO
    1962              :       END DO
    1963              : 
    1964              :       ! Recover B from -B^* via rank-local pass on the cfm's MPI-local data (documented
    1965              :       ! exception to the fm/cfm-routines-only rule); B_recovered = -CONJG(stored).
    1966           16 :       B_local => rtbse_env%B_mat%local_data
    1967         1437 :       B_local = -CONJG(B_local)
    1968              : 
    1969              :       ! Block-symmetry residuals on the JOINT matrices: A Hermitian, B real-symmetric.
    1970           16 :       CALL cp_cfm_transpose(rtbse_env%A_mat, 'C', rtbse_env%eigvecs_pairs)
    1971              :       CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%eigvecs_pairs, &
    1972           16 :                                 CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%A_mat)
    1973           16 :       residual_A = cp_cfm_norm(rtbse_env%eigvecs_pairs, 'M')
    1974              : 
    1975           16 :       CALL cp_cfm_transpose(rtbse_env%B_mat, 'T', rtbse_env%eigvecs_pairs)
    1976              :       CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%eigvecs_pairs, &
    1977           16 :                                 CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%B_mat)
    1978           16 :       residual_B = cp_cfm_norm(rtbse_env%eigvecs_pairs, 'M')
    1979              : 
    1980           16 :       IF (rtbse_env%unit_nr > 0) THEN
    1981              :          WRITE (rtbse_env%unit_nr, '(A,ES16.6)') &
    1982            8 :             ' RTBSE| Hermitian residual ||A - A^H||_max = ', residual_A
    1983              :          WRITE (rtbse_env%unit_nr, '(A,ES16.6)') &
    1984            8 :             ' RTBSE| Symmetry residual  ||B - B^T||_max = ', residual_B
    1985              :       END IF
    1986           16 :       IF (residual_A > 1.0E-6_dp) THEN
    1987            0 :          CPABORT("A is not Hermitian within 1e-6 - check kernel signs / symmetry.")
    1988              :       END IF
    1989           16 :       IF (residual_B > 1.0E-6_dp) THEN
    1990            0 :          CPABORT("B is not symmetric within 1e-6 - check kernel signs / symmetry.")
    1991              :       END IF
    1992              : 
    1993              :       ! A +/- B in scratches (both Hermitian).
    1994           16 :       CALL cp_cfm_to_cfm(rtbse_env%A_mat, rtbse_env%AmB_scratch)
    1995              :       CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%AmB_scratch, &
    1996           16 :                                 CMPLX(-1.0_dp, 0.0_dp, kind=dp), rtbse_env%B_mat)
    1997           16 :       CALL cp_cfm_to_cfm(rtbse_env%A_mat, rtbse_env%ApB_scratch)
    1998              :       CALL cp_cfm_scale_and_add(CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%ApB_scratch, &
    1999           16 :                                 CMPLX(1.0_dp, 0.0_dp, kind=dp), rtbse_env%B_mat)
    2000              : 
    2001              :       ! (A-B) positivity gate on the joint matrix: heevd on a copy; abort if lambda_min < 0.
    2002           16 :       CALL cp_cfm_to_cfm(rtbse_env%AmB_scratch, rtbse_env%L_pairs)
    2003           16 :       CALL cp_cfm_heevd(rtbse_env%L_pairs, rtbse_env%eigvecs_pairs, rtbse_env%eigenvalues_liouvillian)
    2004           16 :       lambda_min_AmB = rtbse_env%eigenvalues_liouvillian(1)
    2005           16 :       IF (rtbse_env%unit_nr > 0) THEN
    2006              :          WRITE (rtbse_env%unit_nr, '(A,ES16.6,A,F12.6,A)') &
    2007            8 :             ' RTBSE| lambda_min(A - B)                       = ', &
    2008           16 :             lambda_min_AmB, '  a.u.  (', lambda_min_AmB*evolt, ' eV)'
    2009              :       END IF
    2010              :       ! Hard abort by design: a non-positive (A-B) breaks the Furche reduction.
    2011           16 :       IF (lambda_min_AmB < 0.0_dp) THEN
    2012              :          CALL cp_abort(__LOCATION__, &
    2013              :                        "(A - B) not positive definite - this may hint at a triplet or "// &
    2014            0 :                        "charge-transfer instability of the reference state.")
    2015              :       END IF
    2016              : 
    2017              :       ! In-place AmB_scratch -> (A-B)^{1/2} via cp_cfm_power. threshold=0 substitution
    2018              :       ! codepath is unreachable here (lambda_min_AmB > 0 already enforced above).
    2019           16 :       CALL cp_cfm_power(rtbse_env%AmB_scratch, threshold=0.0_dp, exponent=0.5_dp)
    2020              : 
    2021              :       ! C = (A-B)^{1/2}(A+B)(A-B)^{1/2}. B_mat free after step "A+/-B" -> reuse as T scratch.
    2022              :       CALL cp_cfm_gemm('N', 'N', n_ov_joint, n_ov_joint, n_ov_joint, &
    2023              :                        CMPLX(1.0_dp, 0.0_dp, kind=dp), &
    2024              :                        rtbse_env%AmB_scratch, rtbse_env%ApB_scratch, &
    2025              :                        CMPLX(0.0_dp, 0.0_dp, kind=dp), &
    2026           16 :                        rtbse_env%B_mat)
    2027              :       CALL cp_cfm_gemm('N', 'N', n_ov_joint, n_ov_joint, n_ov_joint, &
    2028              :                        CMPLX(1.0_dp, 0.0_dp, kind=dp), &
    2029              :                        rtbse_env%B_mat, rtbse_env%AmB_scratch, &
    2030              :                        CMPLX(0.0_dp, 0.0_dp, kind=dp), &
    2031           16 :                        rtbse_env%L_pairs)
    2032              : 
    2033              :       ! Diagonalize C -> Ω_n^2; take +√. Safety clamp on tiny-negative noise.
    2034           16 :       CALL cp_cfm_heevd(rtbse_env%L_pairs, rtbse_env%eigvecs_pairs, rtbse_env%eigenvalues_liouvillian)
    2035          202 :       DO n = 1, n_ov_joint
    2036          202 :          IF (rtbse_env%eigenvalues_liouvillian(n) < 0.0_dp) THEN
    2037            0 :             rtbse_env%eigenvalues_liouvillian(n) = 0.0_dp
    2038              :          ELSE
    2039          186 :             rtbse_env%eigenvalues_liouvillian(n) = SQRT(rtbse_env%eigenvalues_liouvillian(n))
    2040              :          END IF
    2041              :       END DO
    2042              : 
    2043           16 :       IF (rtbse_env%unit_nr > 0) THEN
    2044            8 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE|'
    2045              :          WRITE (rtbse_env%unit_nr, '(A,T26,A,T59,A)') &
    2046            8 :             ' RTBSE|', "Excitation index n", "Excitation energy (eV)"
    2047          101 :          DO n = 1, n_ov_joint
    2048              :             WRITE (rtbse_env%unit_nr, '(A,T40,I4,T69,F12.4)') &
    2049          101 :                ' RTBSE|', n, rtbse_env%eigenvalues_liouvillian(n)*evolt
    2050              :          END DO
    2051            8 :          WRITE (rtbse_env%unit_nr, '(A)') ' RTBSE|'
    2052              :       END IF
    2053           16 :       IF (eig_unit > 0) THEN
    2054            8 :          WRITE (eig_unit, '(A)') '#   n          Omega [a.u.]               Omega [eV]'
    2055          101 :          DO n = 1, n_ov_joint
    2056           93 :             WRITE (eig_unit, '(I5,4X,ES24.14E3,4X,ES24.14E3)') n, &
    2057           93 :                rtbse_env%eigenvalues_liouvillian(n), &
    2058          194 :                rtbse_env%eigenvalues_liouvillian(n)*evolt
    2059              :          END DO
    2060              :       END IF
    2061              : 
    2062           16 :       CALL cp_print_key_finished_output(eig_unit, logger, rtbse_env%eig_section)
    2063              : 
    2064           16 :       DEALLOCATE (n_act_occ, n_act_virt, n_ov, off)
    2065              : 
    2066           16 :       CALL timestop(handle)
    2067           32 :    END SUBROUTINE diagnose_ABBA_liouvillian
    2068              : 
    2069              : ! **************************************************************************************************
    2070              : !> \brief Covariant AO->MO transform of an operator (real): M^MO_mn = sum_µν C_µm M^AO_µν C_νn.
    2071              : !>        For operator-like kernels (Σ^SX, V^H); the density uses the contravariant routine.
    2072              : !> \param rtbse_env Entry point of the calculation - contains current state of variables
    2073              : !> \param fm_ao operator in the AO basis (n_ao x n_ao), input
    2074              : !> \param fm_mo operator in the active-MO basis (mo_active x mo_active), output
    2075              : !> \param i_spin spin channel σ; selects C_active(σ)
    2076              : ! **************************************************************************************************
    2077          372 :    SUBROUTINE transform_ao_to_mo_covariant_fm(rtbse_env, fm_ao, fm_mo, i_spin)
    2078              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2079              :       TYPE(cp_fm_type)                                   :: fm_ao, fm_mo
    2080              :       INTEGER, INTENT(IN)                                :: i_spin
    2081              : 
    2082              :       CHARACTER(len=*), PARAMETER :: routineN = 'transform_ao_to_mo_covariant_fm'
    2083              : 
    2084              :       INTEGER                                            :: handle
    2085              : 
    2086          372 :       CALL timeset(routineN, handle)
    2087              : 
    2088              :       ! step 1: T_µn = sum_ν M^AO_µν C_νn  (n_ao x mo_active)
    2089              :       CALL parallel_gemm("N", "N", rtbse_env%n_ao, rtbse_env%mo_active, rtbse_env%n_ao, &
    2090              :                          1.0_dp, fm_ao, rtbse_env%C_active(i_spin), &
    2091          372 :                          0.0_dp, rtbse_env%ao_mo_workspace(1))
    2092              :       ! step 2: M^MO_mn = sum_µ C_µm T_µn  (mo_active x mo_active)
    2093              :       CALL parallel_gemm("T", "N", rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%n_ao, &
    2094              :                          1.0_dp, rtbse_env%C_active(i_spin), rtbse_env%ao_mo_workspace(1), &
    2095          372 :                          0.0_dp, fm_mo)
    2096              : 
    2097          372 :       CALL timestop(handle)
    2098          372 :    END SUBROUTINE transform_ao_to_mo_covariant_fm
    2099              : 
    2100              : ! **************************************************************************************************
    2101              : !> \brief Covariant AO->MO transform (complex): M^MO_mn = sum_µν C_µm M^AO_µν C_νn, applied to the
    2102              : !>        real and imaginary AO parts separately (2x the real cost).
    2103              : !> \param rtbse_env Entry point of the calculation - contains current state of variables
    2104              : !> \param fm_ao operator in the AO basis (n_ao x n_ao) cfm, input
    2105              : !> \param fm_mo operator in the active-MO basis (mo_active x mo_active) cfm, output
    2106              : !> \param i_spin spin channel σ; selects C_active(σ)
    2107              : ! **************************************************************************************************
    2108         7572 :    SUBROUTINE transform_ao_to_mo_covariant_cfm(rtbse_env, fm_ao, fm_mo, i_spin)
    2109              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2110              :       TYPE(cp_cfm_type)                                  :: fm_ao, fm_mo
    2111              :       INTEGER, INTENT(IN)                                :: i_spin
    2112              : 
    2113              :       CHARACTER(len=*), PARAMETER :: routineN = 'transform_ao_to_mo_covariant_cfm'
    2114              : 
    2115              :       INTEGER                                            :: handle
    2116              : 
    2117         7572 :       CALL timeset(routineN, handle)
    2118              : 
    2119              :       ! Decompose into real/imag AO-sized parts
    2120              :       CALL cp_cfm_to_fm(msource=fm_ao, mtargetr=rtbse_env%real_workspace(1), &
    2121         7572 :                         mtargeti=rtbse_env%real_workspace(2))
    2122              :       ! Re(M^MO)_mn = sum_µν C_µm Re(M^AO)_µν C_νn  (two gemms via ao_mo_workspace)
    2123              :       CALL parallel_gemm("N", "N", rtbse_env%n_ao, rtbse_env%mo_active, rtbse_env%n_ao, &
    2124              :                          1.0_dp, rtbse_env%real_workspace(1), rtbse_env%C_active(i_spin), &
    2125         7572 :                          0.0_dp, rtbse_env%ao_mo_workspace(1))
    2126              :       CALL parallel_gemm("T", "N", rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%n_ao, &
    2127              :                          1.0_dp, rtbse_env%C_active(i_spin), rtbse_env%ao_mo_workspace(1), &
    2128         7572 :                          0.0_dp, rtbse_env%real_workspace_mo(1))
    2129              :       ! Im(M^MO)_mn = sum_µν C_µm Im(M^AO)_µν C_νn
    2130              :       CALL parallel_gemm("N", "N", rtbse_env%n_ao, rtbse_env%mo_active, rtbse_env%n_ao, &
    2131              :                          1.0_dp, rtbse_env%real_workspace(2), rtbse_env%C_active(i_spin), &
    2132         7572 :                          0.0_dp, rtbse_env%ao_mo_workspace(1))
    2133              :       CALL parallel_gemm("T", "N", rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%n_ao, &
    2134              :                          1.0_dp, rtbse_env%C_active(i_spin), rtbse_env%ao_mo_workspace(1), &
    2135         7572 :                          0.0_dp, rtbse_env%real_workspace_mo(2))
    2136              :       ! Reassemble into MO-sized cfm
    2137              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), &
    2138              :                         msourcei=rtbse_env%real_workspace_mo(2), &
    2139         7572 :                         mtarget=fm_mo)
    2140              : 
    2141         7572 :       CALL timestop(handle)
    2142         7572 :    END SUBROUTINE transform_ao_to_mo_covariant_cfm
    2143              : 
    2144              : ! **************************************************************************************************
    2145              : !> \brief Contravariant MO->AO transform of the density (complex): Δρ^AO_µν = sum_mn C_µm Δρ^MO_mn C_νn.
    2146              : !>        Density-like (expands MO indices), unlike the covariant operator transform.
    2147              : !> \param rtbse_env Entry point of the calculation - contains current state of variables
    2148              : !> \param fm_mo density in the active-MO basis (mo_active x mo_active) cfm, input
    2149              : !> \param fm_ao density in the AO basis (n_ao x n_ao) cfm, output
    2150              : !> \param i_spin spin channel σ; selects C_active(σ)
    2151              : ! **************************************************************************************************
    2152         6244 :    SUBROUTINE transform_mo_to_ao_contravariant_cfm(rtbse_env, fm_mo, fm_ao, i_spin)
    2153              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2154              :       TYPE(cp_cfm_type)                                  :: fm_mo, fm_ao
    2155              :       INTEGER, INTENT(IN)                                :: i_spin
    2156              : 
    2157              :       CHARACTER(len=*), PARAMETER :: routineN = 'transform_mo_to_ao_contravariant_cfm'
    2158              : 
    2159              :       INTEGER                                            :: handle
    2160              : 
    2161         6244 :       CALL timeset(routineN, handle)
    2162              : 
    2163              :       ! Re/Im split of Δρ^MO (mo_active x mo_active) into persistent MO-sized scratch
    2164              :       CALL cp_cfm_to_fm(msource=fm_mo, mtargetr=rtbse_env%real_workspace_mo(1), &
    2165         6244 :                         mtargeti=rtbse_env%real_workspace_mo(2))
    2166              :       ! Re(Δρ^AO)_µν = sum_mn C_µm Re(Δρ^MO)_mn C_νn  (C·ρ via ao_mo_workspace, then ·C^T)
    2167              :       CALL parallel_gemm("N", "N", rtbse_env%n_ao, rtbse_env%mo_active, rtbse_env%mo_active, &
    2168              :                          1.0_dp, rtbse_env%C_active(i_spin), rtbse_env%real_workspace_mo(1), &
    2169         6244 :                          0.0_dp, rtbse_env%ao_mo_workspace(1))
    2170              :       CALL parallel_gemm("N", "T", rtbse_env%n_ao, rtbse_env%n_ao, rtbse_env%mo_active, &
    2171              :                          1.0_dp, rtbse_env%ao_mo_workspace(1), rtbse_env%C_active(i_spin), &
    2172         6244 :                          0.0_dp, rtbse_env%real_workspace(1))
    2173              :       ! Im(Δρ^AO)_µν = sum_mn C_µm Im(Δρ^MO)_mn C_νn
    2174              :       CALL parallel_gemm("N", "N", rtbse_env%n_ao, rtbse_env%mo_active, rtbse_env%mo_active, &
    2175              :                          1.0_dp, rtbse_env%C_active(i_spin), rtbse_env%real_workspace_mo(2), &
    2176         6244 :                          0.0_dp, rtbse_env%ao_mo_workspace(1))
    2177              :       CALL parallel_gemm("N", "T", rtbse_env%n_ao, rtbse_env%n_ao, rtbse_env%mo_active, &
    2178              :                          1.0_dp, rtbse_env%ao_mo_workspace(1), rtbse_env%C_active(i_spin), &
    2179         6244 :                          0.0_dp, rtbse_env%real_workspace(2))
    2180              :       ! Reassemble into AO-sized cfm
    2181              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace(1), &
    2182         6244 :                         msourcei=rtbse_env%real_workspace(2), mtarget=fm_ao)
    2183              : 
    2184         6244 :       CALL timestop(handle)
    2185         6244 :    END SUBROUTINE transform_mo_to_ao_contravariant_cfm
    2186              : 
    2187              : ! **************************************************************************************************
    2188              : !> \brief Scales each MO-active element (m,n) by the occupation prefactor (f_n - f_m), f in {0,1}
    2189              : !>        (complex): OV -> -1, VO -> +1, OO/VV -> 0. The only trace of "ρ^0 diagonal in MO".
    2190              : !>        Applied to whatever kernel the caller passes (Σ^SX, V^H in MO) - bound by the caller.
    2191              : !> \param rtbse_env Entry point of the calculation - contains current state of variables
    2192              : !> \param cfm MO-active kernel matrix (mo_active x mo_active) cfm, scaled in place
    2193              : !> \param i_spin spin channel σ; OV/VO boundary set by n_occ(σ)
    2194              : ! **************************************************************************************************
    2195         2252 :    SUBROUTINE transform_mo_occupation_factor_diff_cfm(rtbse_env, cfm, i_spin)
    2196              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2197              :       TYPE(cp_cfm_type)                                  :: cfm
    2198              :       INTEGER                                            :: i_spin
    2199              : 
    2200              :       CHARACTER(len=*), PARAMETER :: routineN = 'transform_mo_occupation_factor_diff_cfm'
    2201              : 
    2202              :       INTEGER                                            :: handle
    2203              : 
    2204         2252 :       CALL timeset(routineN, handle)
    2205              : 
    2206              :       CALL cp_cfm_to_fm(msource=cfm, mtargetr=rtbse_env%real_workspace_mo(1), &
    2207         2252 :                         mtargeti=rtbse_env%real_workspace_mo(2))
    2208              :       ! (f_n - f_m) applied to the real part
    2209         2252 :       CALL transform_mo_occupation_factor_diff_fm(rtbse_env, rtbse_env%real_workspace_mo(1), i_spin)
    2210              :       ! (f_n - f_m) applied to the imaginary part
    2211         2252 :       CALL transform_mo_occupation_factor_diff_fm(rtbse_env, rtbse_env%real_workspace_mo(2), i_spin)
    2212              :       ! Copy back to cfm
    2213              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), &
    2214              :                         msourcei=rtbse_env%real_workspace_mo(2), &
    2215         2252 :                         mtarget=cfm)
    2216              : 
    2217         2252 :       CALL timestop(handle)
    2218         2252 :    END SUBROUTINE transform_mo_occupation_factor_diff_cfm
    2219              : 
    2220              : ! **************************************************************************************************
    2221              : !> \brief Scales each MO-active element (m,n) by the occupation prefactor (f_n - f_m), f in {0,1}
    2222              : !>        (real): OV -> -1, VO -> +1, OO/VV -> 0. Real-input worker for the cfm variant; the only
    2223              : !>        trace of "ρ^0 diagonal in MO". Bound by the caller to the kernel being scaled.
    2224              : !> \param rtbse_env Entry point of the calculation - contains current state of variables
    2225              : !> \param fm MO-active kernel matrix (mo_active x mo_active) fm, scaled in place
    2226              : !> \param i_spin spin channel σ; OV/VO boundary set by n_occ(σ)
    2227              : ! **************************************************************************************************
    2228         9032 :    SUBROUTINE transform_mo_occupation_factor_diff_fm(rtbse_env, fm, i_spin)
    2229              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2230              :       TYPE(cp_fm_type)                                   :: fm
    2231              :       INTEGER                                            :: i_spin
    2232              : 
    2233              :       CHARACTER(len=*), PARAMETER :: routineN = 'transform_mo_occupation_factor_diff_fm'
    2234              : 
    2235              :       INTEGER                                            :: handle, i_global, i_global_mo, i_local, &
    2236              :                                                             j_global, j_global_mo, j_local, n_occ, &
    2237              :                                                             ncol_local, nrow_local, shift
    2238         4516 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2239              :       REAL(kind=dp)                                      :: occ_factor
    2240         4516 :       REAL(kind=dp), DIMENSION(:, :), POINTER            :: local_data
    2241              : 
    2242         4516 :       CALL timeset(routineN, handle)
    2243              : 
    2244         4516 :       n_occ = rtbse_env%n_occ(i_spin)
    2245              :       ! Shift mapping local active-window index to absolute MO index
    2246         4516 :       shift = rtbse_env%first_active_mo - 1
    2247              : 
    2248              :       CALL cp_fm_get_info(matrix=fm, &
    2249              :                           nrow_local=nrow_local, ncol_local=ncol_local, &
    2250         4516 :                           row_indices=row_indices, col_indices=col_indices)
    2251              : 
    2252         4516 :       local_data => fm%local_data
    2253              : 
    2254        29674 :       DO i_local = 1, nrow_local
    2255        25158 :          i_global = row_indices(i_local)
    2256        25158 :          i_global_mo = i_global + shift
    2257       319942 :          DO j_local = 1, ncol_local
    2258       290268 :             j_global = col_indices(j_local)
    2259       290268 :             j_global_mo = j_global + shift
    2260              : 
    2261       290268 :             IF (i_global_mo <= n_occ .AND. j_global_mo > n_occ) THEN
    2262              :                occ_factor = -1.0_dp
    2263       267368 :             ELSE IF (i_global_mo > n_occ .AND. j_global_mo <= n_occ) THEN
    2264              :                occ_factor = 1.0_dp
    2265              :             ELSE
    2266       244468 :                occ_factor = 0.0_dp
    2267              :             END IF
    2268              : 
    2269       315426 :             local_data(i_local, j_local) = occ_factor*local_data(i_local, j_local)
    2270              :          END DO
    2271              :       END DO
    2272              : 
    2273         4516 :       CALL timestop(handle)
    2274         4516 :    END SUBROUTINE transform_mo_occupation_factor_diff_fm
    2275              : 
    2276              : ! **************************************************************************************************
    2277              : !> \brief Mask an MO-active cfm: keep either OV or VO block, zero everything else.
    2278              : !> \param rtbse_env RT-BSE environment
    2279              : !> \param cfm MO-active cfm to mask in place
    2280              : !> \param i_spin Spin index
    2281              : !> \param keep_OV .TRUE. keeps the (occ row, virt col) block; .FALSE. keeps (virt row, occ col)
    2282              : !> \param keep_ovvo if present and .TRUE., keep BOTH off-diagonal blocks (OV and VO) and zero
    2283              : !>        OO/VV; overrides keep_OV. Absent/false reproduces the keep_OV behaviour.
    2284              : ! **************************************************************************************************
    2285        15892 :    SUBROUTINE mask_mo_block_cfm(rtbse_env, cfm, i_spin, keep_OV, keep_ovvo)
    2286              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2287              :       TYPE(cp_cfm_type)                                  :: cfm
    2288              :       INTEGER, INTENT(IN)                                :: i_spin
    2289              :       LOGICAL, INTENT(IN)                                :: keep_OV
    2290              :       LOGICAL, INTENT(IN), OPTIONAL                      :: keep_ovvo
    2291              : 
    2292              :       CHARACTER(len=*), PARAMETER                        :: routineN = 'mask_mo_block_cfm'
    2293              : 
    2294         7946 :       COMPLEX(kind=dp), DIMENSION(:, :), POINTER         :: local_data
    2295              :       INTEGER                                            :: handle, i_global, i_global_mo, i_local, &
    2296              :                                                             j_global, j_global_mo, j_local, n_occ, &
    2297              :                                                             ncol_local, nrow_local, shift
    2298         7946 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2299              :       LOGICAL                                            :: keep, l_keep_ovvo
    2300              : 
    2301         7946 :       CALL timeset(routineN, handle)
    2302              : 
    2303         7946 :       l_keep_ovvo = .FALSE.
    2304         7946 :       IF (PRESENT(keep_ovvo)) l_keep_ovvo = keep_ovvo
    2305              : 
    2306         7946 :       n_occ = rtbse_env%n_occ(i_spin)
    2307         7946 :       shift = rtbse_env%first_active_mo - 1
    2308              : 
    2309              :       CALL cp_cfm_get_info(matrix=cfm, &
    2310              :                            nrow_local=nrow_local, ncol_local=ncol_local, &
    2311         7946 :                            row_indices=row_indices, col_indices=col_indices)
    2312              : 
    2313         7946 :       local_data => cfm%local_data
    2314              : 
    2315        55622 :       DO i_local = 1, nrow_local
    2316        47676 :          i_global = row_indices(i_local)
    2317        47676 :          i_global_mo = i_global + shift
    2318       627734 :          DO j_local = 1, ncol_local
    2319       572112 :             j_global = col_indices(j_local)
    2320       572112 :             j_global_mo = j_global + shift
    2321       572112 :             IF (l_keep_ovvo) THEN
    2322              :                ! keep both off-diagonal blocks (OV and VO); drop OO/VV
    2323        23040 :                keep = ((i_global_mo <= n_occ) .NEQV. (j_global_mo <= n_occ))
    2324       549072 :             ELSE IF (keep_OV) THEN
    2325       549072 :                keep = (i_global_mo <= n_occ .AND. j_global_mo > n_occ)
    2326              :             ELSE
    2327            0 :                keep = (i_global_mo > n_occ .AND. j_global_mo <= n_occ)
    2328              :             END IF
    2329       619788 :             IF (.NOT. keep) local_data(i_local, j_local) = CMPLX(0.0_dp, 0.0_dp, kind=dp)
    2330              :          END DO
    2331              :       END DO
    2332              : 
    2333         7946 :       CALL timestop(handle)
    2334         7946 :    END SUBROUTINE mask_mo_block_cfm
    2335              : 
    2336              : ! **************************************************************************************************
    2337              : !> \brief Mask an MO-active fm: keep either OV or VO block, zero everything else.
    2338              : !> \param rtbse_env RT-BSE environment
    2339              : !> \param fm MO-active fm to mask in place
    2340              : !> \param i_spin Spin index
    2341              : !> \param keep_OV .TRUE. keeps the (occ row, virt col) block; .FALSE. keeps (virt row, occ col)
    2342              : ! **************************************************************************************************
    2343            0 :    SUBROUTINE mask_mo_block_fm(rtbse_env, fm, i_spin, keep_OV)
    2344              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2345              :       TYPE(cp_fm_type)                                   :: fm
    2346              :       INTEGER, INTENT(IN)                                :: i_spin
    2347              :       LOGICAL, INTENT(IN)                                :: keep_OV
    2348              : 
    2349              :       CHARACTER(len=*), PARAMETER                        :: routineN = 'mask_mo_block_fm'
    2350              : 
    2351              :       INTEGER                                            :: handle, i_global, i_global_mo, i_local, &
    2352              :                                                             j_global, j_global_mo, j_local, n_occ, &
    2353              :                                                             ncol_local, nrow_local, shift
    2354            0 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2355              :       LOGICAL                                            :: keep
    2356            0 :       REAL(kind=dp), DIMENSION(:, :), POINTER            :: local_data
    2357              : 
    2358            0 :       CALL timeset(routineN, handle)
    2359              : 
    2360            0 :       n_occ = rtbse_env%n_occ(i_spin)
    2361            0 :       shift = rtbse_env%first_active_mo - 1
    2362              : 
    2363              :       CALL cp_fm_get_info(matrix=fm, &
    2364              :                           nrow_local=nrow_local, ncol_local=ncol_local, &
    2365            0 :                           row_indices=row_indices, col_indices=col_indices)
    2366              : 
    2367            0 :       local_data => fm%local_data
    2368              : 
    2369            0 :       DO i_local = 1, nrow_local
    2370            0 :          i_global = row_indices(i_local)
    2371            0 :          i_global_mo = i_global + shift
    2372            0 :          DO j_local = 1, ncol_local
    2373            0 :             j_global = col_indices(j_local)
    2374            0 :             j_global_mo = j_global + shift
    2375            0 :             IF (keep_OV) THEN
    2376            0 :                keep = (i_global_mo <= n_occ .AND. j_global_mo > n_occ)
    2377              :             ELSE
    2378            0 :                keep = (i_global_mo > n_occ .AND. j_global_mo <= n_occ)
    2379              :             END IF
    2380            0 :             IF (.NOT. keep) local_data(i_local, j_local) = 0.0_dp
    2381              :          END DO
    2382              :       END DO
    2383              : 
    2384            0 :       CALL timestop(handle)
    2385            0 :    END SUBROUTINE mask_mo_block_fm
    2386              : 
    2387              : ! **************************************************************************************************
    2388              : !> \brief Multiply a MO-active cfm rho by the TDA symmetric-shift rotation:
    2389              : !>        rho_OV *= exp(i * phase),  rho_VO *= exp(-i * phase).
    2390              : !>        OO/VV blocks are left untouched. For direction='to_lab' pass phase = -Ω_0*t;
    2391              : !>        for direction='to_rotating' pass phase = +Ω_0*t. No-op when omega_shift = 0.
    2392              : !>        Preserves Hermiticity since the two phases are complex conjugates of each other.
    2393              : !> \param rtbse_env RT-BSE environment
    2394              : !> \param rho MO-active cfm rotated in place
    2395              : !> \param i_spin Spin index
    2396              : !> \param phase Real phase argument (radians); typically +/- Ω_0 * t
    2397              : ! **************************************************************************************************
    2398          164 :    SUBROUTINE rotate_rho_phase(rtbse_env, rho, i_spin, phase)
    2399              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2400              :       TYPE(cp_cfm_type)                                  :: rho
    2401              :       INTEGER, INTENT(IN)                                :: i_spin
    2402              :       REAL(kind=dp), INTENT(IN)                          :: phase
    2403              : 
    2404              :       CHARACTER(len=*), PARAMETER                        :: routineN = 'rotate_rho_phase'
    2405              : 
    2406              :       COMPLEX(kind=dp)                                   :: phase_ov, phase_vo
    2407           64 :       COMPLEX(kind=dp), DIMENSION(:, :), POINTER         :: local_data
    2408              :       INTEGER                                            :: handle, i_global, i_global_mo, i_local, &
    2409              :                                                             j_global, j_global_mo, j_local, n_occ, &
    2410              :                                                             ncol_local, nrow_local, shift
    2411           64 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2412              : 
    2413           14 :       IF (rtbse_env%omega_shift == 0.0_dp) RETURN
    2414              : 
    2415           50 :       CALL timeset(routineN, handle)
    2416              : 
    2417           50 :       n_occ = rtbse_env%n_occ(i_spin)
    2418           50 :       shift = rtbse_env%first_active_mo - 1
    2419           50 :       phase_ov = CMPLX(COS(phase), SIN(phase), kind=dp)
    2420           50 :       phase_vo = CONJG(phase_ov)
    2421              : 
    2422              :       CALL cp_cfm_get_info(matrix=rho, &
    2423              :                            nrow_local=nrow_local, ncol_local=ncol_local, &
    2424           50 :                            row_indices=row_indices, col_indices=col_indices)
    2425           50 :       local_data => rho%local_data
    2426              : 
    2427          350 :       DO i_local = 1, nrow_local
    2428          300 :          i_global = row_indices(i_local)
    2429          300 :          i_global_mo = i_global + shift
    2430         3950 :          DO j_local = 1, ncol_local
    2431         3600 :             j_global = col_indices(j_local)
    2432         3600 :             j_global_mo = j_global + shift
    2433         3900 :             IF (i_global_mo <= n_occ .AND. j_global_mo > n_occ) THEN
    2434              :                ! ρ_OV *= e^{+iφ}
    2435          275 :                local_data(i_local, j_local) = phase_ov*local_data(i_local, j_local)
    2436         3325 :             ELSE IF (i_global_mo > n_occ .AND. j_global_mo <= n_occ) THEN
    2437              :                ! ρ_VO *= e^{-iφ}
    2438          275 :                local_data(i_local, j_local) = phase_vo*local_data(i_local, j_local)
    2439              :             END IF
    2440              :          END DO
    2441              :       END DO
    2442              : 
    2443           50 :       CALL timestop(handle)
    2444           64 :    END SUBROUTINE rotate_rho_phase
    2445              : 
    2446              : ! **************************************************************************************************
    2447              : !> \brief Build a lab-frame copy of the (possibly rotating-frame) density rho for I/O.
    2448              : !>        On the TDA + symmetric-shift path returns rho_lab(t) by multiplying OV/VO by
    2449              : !>        exp(-/+ i Ω_0 t). Otherwise returns a plain copy. Writes into rho_new_last
    2450              : !>        (mo_struct-sized, idle outside ETRS) and returns a pointer to it; falls back to
    2451              : !>        the input rho when no scratch is available.
    2452              : !> \param rtbse_env RT-BSE environment
    2453              : !> \param rho_in Rotating-frame density (per spin)
    2454              : !> \param t_phys Physical time associated with rho_in
    2455              : !> \param rho_lab On exit, points to a per-spin cfm array holding rho in the lab frame.
    2456              : ! **************************************************************************************************
    2457          796 :    SUBROUTINE build_rho_lab(rtbse_env, rho_in, t_phys, rho_lab)
    2458              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2459              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_in
    2460              :       REAL(kind=dp), INTENT(IN)                          :: t_phys
    2461              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho_lab
    2462              : 
    2463              :       CHARACTER(len=*), PARAMETER                        :: routineN = 'build_rho_lab'
    2464              : 
    2465              :       INTEGER                                            :: handle, i
    2466              : 
    2467          748 :       IF (rtbse_env%omega_shift == 0.0_dp .OR. .NOT. ASSOCIATED(rtbse_env%rho_new_last)) THEN
    2468          700 :          rho_lab => rho_in
    2469          700 :          RETURN
    2470              :       END IF
    2471              : 
    2472           48 :       CALL timeset(routineN, handle)
    2473              : 
    2474           96 :       DO i = 1, rtbse_env%n_spin
    2475           48 :          CALL cp_cfm_to_cfm(rho_in(i), rtbse_env%rho_new_last(i))
    2476              :          ! Lab-frame rho_OV(t) = exp(+i*Ω_0*t) * rho_tilde_OV(t)
    2477              :          ! (sign derived from [h_shifted, rho_tilde]_OV = (-eps_ai + Ω_0) rho_tilde_OV).
    2478           96 :          CALL rotate_rho_phase(rtbse_env, rtbse_env%rho_new_last(i), i, rtbse_env%omega_shift*t_phys)
    2479              :       END DO
    2480           48 :       rho_lab => rtbse_env%rho_new_last
    2481              : 
    2482           48 :       CALL timestop(handle)
    2483              :    END SUBROUTINE build_rho_lab
    2484              : 
    2485              : ! **************************************************************************************************
    2486              : !> \brief Bridge the restart density from the previous run's active-MO gauge into this run's:
    2487              : !>        overlap-metric basis change  U_mn = sum_µν C2_µm S_µν C1_νn   (mo_active × mo_active),
    2488              : !>        then  ρ_mn ← sum_pq U_mp ρ_pq U_nq   (ρ ← U ρ U^T, U real orthogonal up to FP).
    2489              : !>        Exact under per-MO sign flips and degenerate-subspace rotations of the SCF solution.
    2490              : !>        Diagnostics per spin: max|U−1| (total gauge correction), sign-flip count, max off-diag
    2491              : !>        (degenerate rotation), max|U^T U−1| (representability loss; warn ≥1e-10, abort ≥1e-3),
    2492              : !>        max|U_OV| (occ/virt mixing; warn ≥1e-6). No-op (U=1) when the two gauges agree.
    2493              : !> \param rtbse_env RT-BSE environment
    2494              : ! **************************************************************************************************
    2495           14 :    SUBROUTINE apply_restart_basis_bridge(rtbse_env)
    2496              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2497              : 
    2498              :       CHARACTER(len=*), PARAMETER :: routineN = 'apply_restart_basis_bridge'
    2499              :       COMPLEX(kind=dp), PARAMETER :: c_one = CMPLX(1.0_dp, 0.0_dp, kind=dp), &
    2500              :          c_zero = CMPLX(0.0_dp, 0.0_dp, kind=dp)
    2501              : 
    2502              :       INTEGER                                            :: handle, i, i_glob, i_mo, ii, j_glob, &
    2503              :                                                             j_mo, jj, n_flip, ncol_local, &
    2504              :                                                             nrow_local
    2505           14 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2506              :       LOGICAL                                            :: found
    2507              :       REAL(kind=dp)                                      :: dev_ident, dev_offdiag, dev_ov, &
    2508              :                                                             dev_unitary
    2509           14 :       REAL(kind=dp), ALLOCATABLE, DIMENSION(:)           :: u_diag
    2510              :       REAL(kind=dp), CONTIGUOUS, DIMENSION(:, :), &
    2511           14 :          POINTER                                         :: u_data
    2512              :       TYPE(cp_fm_type)                                   :: SC_old
    2513           14 :       TYPE(cp_fm_type), DIMENSION(:), POINTER            :: C_old
    2514              : 
    2515           14 :       CALL timeset(routineN, handle)
    2516              : 
    2517           14 :       NULLIFY (C_old)
    2518           58 :       ALLOCATE (C_old(rtbse_env%n_spin))
    2519           30 :       DO i = 1, rtbse_env%n_spin
    2520           30 :          CALL cp_fm_create(C_old(i), rtbse_env%fm_struct_ao_mo_active)
    2521              :       END DO
    2522           14 :       CALL read_restart_C(rtbse_env, C_old, found)
    2523           14 :       IF (.NOT. found) THEN
    2524            0 :          DO i = 1, rtbse_env%n_spin
    2525            0 :             CALL cp_fm_release(C_old(i))
    2526              :          END DO
    2527            0 :          DEALLOCATE (C_old)
    2528            0 :          CALL timestop(handle)
    2529              :          RETURN
    2530              :       END IF
    2531              : 
    2532           14 :       CALL cp_fm_create(SC_old, rtbse_env%fm_struct_ao_mo_active)
    2533           42 :       ALLOCATE (u_diag(rtbse_env%mo_active))
    2534              : 
    2535           30 :       DO i = 1, rtbse_env%n_spin
    2536              :          ! S C1 : [S C1]_µn = sum_ν S_µν C1_νn
    2537              :          CALL parallel_gemm("N", "N", rtbse_env%n_ao, rtbse_env%mo_active, rtbse_env%n_ao, &
    2538           16 :                             1.0_dp, rtbse_env%S_fm, C_old(i), 0.0_dp, SC_old)
    2539              :          ! U = C2^T (S C1) : U_mn = sum_µ C2_µm [S C1]_µn   -> real_workspace_mo(1)
    2540              :          CALL parallel_gemm("T", "N", rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%n_ao, &
    2541           16 :                             1.0_dp, rtbse_env%C_active(i), SC_old, 0.0_dp, rtbse_env%real_workspace_mo(1))
    2542              : 
    2543              :          ! Diagnostics on U (local blocks + global MAX reduction; diagonal is gathered globally)
    2544           16 :          CALL cp_fm_get_diag(rtbse_env%real_workspace_mo(1), u_diag)
    2545          208 :          n_flip = COUNT(u_diag < 0.0_dp)
    2546              :          CALL cp_fm_get_info(rtbse_env%real_workspace_mo(1), nrow_local=nrow_local, ncol_local=ncol_local, &
    2547           16 :                              row_indices=row_indices, col_indices=col_indices, local_data=u_data)
    2548           16 :          dev_ident = 0.0_dp; dev_offdiag = 0.0_dp; dev_ov = 0.0_dp
    2549          112 :          DO ii = 1, nrow_local
    2550           96 :             i_glob = row_indices(ii)
    2551           96 :             i_mo = i_glob + rtbse_env%first_active_mo - 1
    2552         1264 :             DO jj = 1, ncol_local
    2553         1152 :                j_glob = col_indices(jj)
    2554         1152 :                j_mo = j_glob + rtbse_env%first_active_mo - 1
    2555         1152 :                IF (i_glob == j_glob) THEN
    2556           96 :                   dev_ident = MAX(dev_ident, ABS(u_data(ii, jj) - 1.0_dp))
    2557              :                ELSE
    2558         1056 :                   dev_ident = MAX(dev_ident, ABS(u_data(ii, jj)))
    2559         1056 :                   dev_offdiag = MAX(dev_offdiag, ABS(u_data(ii, jj)))
    2560              :                END IF
    2561         1248 :                IF ((i_mo <= rtbse_env%n_occ(i)) .NEQV. (j_mo <= rtbse_env%n_occ(i))) THEN
    2562          176 :                   dev_ov = MAX(dev_ov, ABS(u_data(ii, jj)))
    2563              :                END IF
    2564              :             END DO
    2565              :          END DO
    2566              :          ! U^T U − 1 : [U^T U]_mn = sum_p U_pm U_pn   -> real_workspace_mo(2)
    2567              :          CALL parallel_gemm("T", "N", rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    2568              :                             1.0_dp, rtbse_env%real_workspace_mo(1), rtbse_env%real_workspace_mo(1), &
    2569           16 :                             0.0_dp, rtbse_env%real_workspace_mo(2))
    2570              :          CALL cp_fm_get_info(rtbse_env%real_workspace_mo(2), nrow_local=nrow_local, ncol_local=ncol_local, &
    2571           16 :                              row_indices=row_indices, col_indices=col_indices, local_data=u_data)
    2572           16 :          dev_unitary = 0.0_dp
    2573          112 :          DO ii = 1, nrow_local
    2574           96 :             i_glob = row_indices(ii)
    2575         1264 :             DO jj = 1, ncol_local
    2576         1152 :                j_glob = col_indices(jj)
    2577         2304 :                dev_unitary = MAX(dev_unitary, ABS(u_data(ii, jj) - MERGE(1.0_dp, 0.0_dp, i_glob == j_glob)))
    2578              :             END DO
    2579              :          END DO
    2580           16 :          CALL rtbse_env%real_workspace_mo(1)%matrix_struct%para_env%max(dev_ident)
    2581           16 :          CALL rtbse_env%real_workspace_mo(1)%matrix_struct%para_env%max(dev_offdiag)
    2582           16 :          CALL rtbse_env%real_workspace_mo(1)%matrix_struct%para_env%max(dev_ov)
    2583           16 :          CALL rtbse_env%real_workspace_mo(1)%matrix_struct%para_env%max(dev_unitary)
    2584              : 
    2585           16 :          IF (rtbse_env%unit_nr > 0) THEN
    2586            8 :             WRITE (rtbse_env%unit_nr, '(A,I3,A)') " RTBSE| Restart basis bridge U = C2^T S C1 (spin ", i, "):"
    2587            8 :             WRITE (rtbse_env%unit_nr, '(A,ES12.3,A)') " RTBSE|   max |U - 1|         ", dev_ident, &
    2588           16 :                "  (total gauge correction)"
    2589            8 :             WRITE (rtbse_env%unit_nr, '(A,I12)') " RTBSE|   sign flips (U_ii<0)", n_flip
    2590            8 :             WRITE (rtbse_env%unit_nr, '(A,ES12.3,A)') " RTBSE|   max offdiag |U_ij| ", dev_offdiag, &
    2591           16 :                "  (degenerate-subspace rotation)"
    2592            8 :             WRITE (rtbse_env%unit_nr, '(A,ES12.3,A)') " RTBSE|   max |U^T U - 1|    ", dev_unitary, &
    2593           16 :                "  (representability loss)"
    2594            8 :             WRITE (rtbse_env%unit_nr, '(A,ES12.3,A)') " RTBSE|   max |U_OV|         ", dev_ov, &
    2595           16 :                "  (occ/virt structure change)"
    2596              :          END IF
    2597           16 :          IF (dev_unitary >= 1.0e-3_dp) THEN
    2598              :             CALL cp_abort(__LOCATION__, &
    2599            0 :                           "Restart basis bridge: active spaces of the two runs differ severely (|U^T U - 1| >= 1e-3)")
    2600              :          END IF
    2601           16 :          IF (dev_unitary >= 1.0e-10_dp .AND. dev_unitary < 1.0e-3_dp) THEN
    2602              :             CALL cp_warn(__LOCATION__, &
    2603            0 :                          "Restart basis bridge: representability loss above 1e-10 - active windows differ slightly.")
    2604              :          END IF
    2605              :          ! 1e-6 floor: benign SCF reconvergence gives ~1e-9 occ/virt gauge noise (the bridge maps it
    2606              :          ! correctly either way); only a genuine occupation-structure change reaches this threshold.
    2607           16 :          IF (dev_ov >= 1.0e-6_dp) THEN
    2608              :             CALL cp_warn(__LOCATION__, &
    2609            0 :                          "Restart basis bridge: occupied/virtual mixing above 1e-6 - occupation structure changed.")
    2610              :          END IF
    2611              : 
    2612              :          ! ρ ← U ρ U^T : lift U to complex, [Uρ]_mn = sum_p U_mp ρ_pn, then ρ_mn = sum_q [Uρ]_mq U_nq
    2613           16 :          CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace_mo(1), mtarget=rtbse_env%rho_workspace(1))
    2614              :          CALL cp_cfm_gemm('N', 'N', rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    2615           16 :                           c_one, rtbse_env%rho_workspace(1), rtbse_env%rho(i), c_zero, rtbse_env%rho_workspace(2))
    2616              :          CALL cp_cfm_gemm('N', 'C', rtbse_env%mo_active, rtbse_env%mo_active, rtbse_env%mo_active, &
    2617           62 :                           c_one, rtbse_env%rho_workspace(2), rtbse_env%rho_workspace(1), c_zero, rtbse_env%rho(i))
    2618              :       END DO
    2619              : 
    2620           14 :       DEALLOCATE (u_diag)
    2621           14 :       CALL cp_fm_release(SC_old)
    2622           30 :       DO i = 1, rtbse_env%n_spin
    2623           30 :          CALL cp_fm_release(C_old(i))
    2624              :       END DO
    2625           14 :       DEALLOCATE (C_old)
    2626           14 :       CALL timestop(handle)
    2627           42 :    END SUBROUTINE apply_restart_basis_bridge
    2628              : 
    2629              : ! **************************************************************************************************
    2630              : !> \brief Complex-linear Hartree contraction. Calls the real-input get_hartree on
    2631              : !>        Re(rho_AO) and on Im(rho_AO) separately and assembles
    2632              : !>           v_AO = V_H[Re(rho_AO)] + i * V_H[Im(rho_AO)] .
    2633              : !>        Required by the TDA propagator where the per-pass input Delta rho_OV (or
    2634              : !>        Delta rho_VO) is non-Hermitian, so the imaginary part must be carried.
    2635              : !>        The real kernel get_hartree realises V^H_λσ = sum_PQ (λσ|P) V_PQ [sum_µν (µν|Q) Δρ_µν].
    2636              : !> \param rtbse_env RT-BSE environment
    2637              : !> \param rho_cfm AO complex input density
    2638              : !> \param v_cfm AO complex Hartree output (overwritten)
    2639              : !> \param ispin Spin index (selects scratch slots in rtbse_env)
    2640              : ! **************************************************************************************************
    2641         1304 :    SUBROUTINE get_hartree_complex(rtbse_env, rho_cfm, v_cfm, ispin)
    2642              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2643              :       TYPE(cp_cfm_type), INTENT(IN)                      :: rho_cfm
    2644              :       TYPE(cp_cfm_type)                                  :: v_cfm
    2645              :       INTEGER, INTENT(IN)                                :: ispin
    2646              : 
    2647              :       CHARACTER(len=*), PARAMETER :: routineN = 'get_hartree_complex'
    2648              : 
    2649              :       INTEGER                                            :: handle
    2650              : 
    2651              :       MARK_USED(ispin)
    2652              : 
    2653         1304 :       CALL timeset(routineN, handle)
    2654              : 
    2655              :       ! Mirrors get_sigma_complex: split rho_cfm into real and imaginary fm parts,
    2656              :       ! call the real-input Hartree contraction on each, then assemble
    2657              :       ! v_cfm = V_H[Re(rho_cfm)] + i*V_H[Im(rho_cfm)].
    2658              :       ! Scratch usage:
    2659              :       !   real_workspace(1) - holds Re(rho) then V_H[Re]
    2660              :       !   real_workspace(2) - holds Im(rho) then V_H[Im]
    2661              :       !   sigma_complex_workspace(1) - cfm wrapper feeding the real-input slot of get_hartree
    2662              : 
    2663              :       ! V^H[Re(Δρ^AO)] -> real_workspace(1)
    2664         1304 :       CALL cp_cfm_to_fm(msource=rho_cfm, mtargetr=rtbse_env%real_workspace(1))
    2665         1304 :       CALL cp_cfm_set_all(rtbse_env%sigma_complex_workspace(1), CMPLX(0.0_dp, 0.0_dp, kind=dp))
    2666              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace(1), &
    2667         1304 :                         mtarget=rtbse_env%sigma_complex_workspace(1))
    2668              :       CALL get_hartree(rtbse_env, rtbse_env%sigma_complex_workspace(1), &
    2669         1304 :                        rtbse_env%real_workspace(1))
    2670              : 
    2671              :       ! Imaginary part: extract Im(rho_cfm) into real_workspace(2)
    2672         1304 :       CALL cp_cfm_to_fm(msource=rho_cfm, mtargeti=rtbse_env%real_workspace(2))
    2673         1304 :       CALL cp_cfm_set_all(rtbse_env%sigma_complex_workspace(1), CMPLX(0.0_dp, 0.0_dp, kind=dp))
    2674              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace(2), &
    2675         1304 :                         mtarget=rtbse_env%sigma_complex_workspace(1))
    2676              :       CALL get_hartree(rtbse_env, rtbse_env%sigma_complex_workspace(1), &
    2677         1304 :                        rtbse_env%real_workspace(2))
    2678              : 
    2679              :       ! Assemble v_cfm = real_workspace(1) + i * real_workspace(2)
    2680              :       CALL cp_fm_to_cfm(msourcer=rtbse_env%real_workspace(1), &
    2681              :                         msourcei=rtbse_env%real_workspace(2), &
    2682         1304 :                         mtarget=v_cfm)
    2683              : 
    2684         1304 :       CALL timestop(handle)
    2685         1304 :    END SUBROUTINE get_hartree_complex
    2686              : 
    2687              : ! **************************************************************************************************
    2688              : !> \brief δ-kick (Marek2025) seeding the linearized EOM: builds the MO-active dipole operator
    2689              : !>        A = intensity * sum_k kvec_k r_k (MO basis), Hermitizes it, and propagates ρ by exp(-iA),
    2690              : !>        so Δρ^+_nm = i(f_n - f_m) A_nm excites only the OV/VO blocks.
    2691              : !> \param rtbse_env RT-BSE environment
    2692              : !> \author Stepan Marek (09.24)
    2693              : !> \author Maximilian Graml - trafo to MO and linearized version following 10.1021/acs.jctc.2c00644 (03.26)
    2694              : ! **************************************************************************************************
    2695           38 :    SUBROUTINE apply_delta_pulse_MO(rtbse_env)
    2696              :       TYPE(rtbse_env_type), POINTER                      :: rtbse_env
    2697              : 
    2698              :       CHARACTER(len=*), PARAMETER :: routineN = 'apply_delta_pulse_MO'
    2699              : 
    2700              :       INTEGER                                            :: handle, i, k
    2701              :       REAL(kind=dp)                                      :: intensity, metric
    2702              :       REAL(kind=dp), DIMENSION(3)                        :: kvec
    2703              : 
    2704           38 :       CALL timeset(routineN, handle)
    2705              : 
    2706              :       ! Report application
    2707           38 :       IF (rtbse_env%unit_nr > 0) WRITE (rtbse_env%unit_nr, '(A28)') ' RTBSE| Applying delta pulse'
    2708              :       ! Extra minus for the propagation of density
    2709           38 :       intensity = -rtbse_env%dft_control%rtp_control%delta_pulse_scale
    2710              :       metric = 0.0_dp
    2711          152 :       kvec(:) = rtbse_env%dft_control%rtp_control%delta_pulse_direction(:)
    2712           38 :       IF (rtbse_env%unit_nr > 0) WRITE (rtbse_env%unit_nr, '(A38,E14.4E3,E14.4E3,E14.4E3)') &
    2713           76 :          " RTBSE| Delta pulse elements (a.u.) : ", intensity*kvec(:)
    2714              :       ! Per-spin kick: each spin uses its own MO-active dipole operator (C_active(i_spin) basis)
    2715           82 :       DO i = 1, rtbse_env%n_spin
    2716           44 :          CALL cp_fm_set_all(rtbse_env%real_workspace_mo(1), 0.0_dp)
    2717          176 :          DO k = 1, 3
    2718              :             CALL cp_fm_scale_and_add(1.0_dp, rtbse_env%real_workspace_mo(1), &
    2719          176 :                                      kvec(k), rtbse_env%moments_field(k, i))
    2720              :          END DO
    2721              :          ! enforce hermiticity of the effective Hamiltonian
    2722           44 :          CALL cp_fm_transpose(rtbse_env%real_workspace_mo(1), rtbse_env%real_workspace_mo(2))
    2723              :          CALL cp_fm_scale_and_add(0.5_dp, rtbse_env%real_workspace_mo(1), &
    2724           44 :                                   0.5_dp, rtbse_env%real_workspace_mo(2))
    2725              :          ! multiply by intensity, set as the imaginary exponent for this spin
    2726           44 :          CALL cp_fm_scale(intensity, rtbse_env%real_workspace_mo(1))
    2727           82 :          CALL cp_fm_to_cfm(msourcei=rtbse_env%real_workspace_mo(1), mtarget=rtbse_env%ham_workspace(i))
    2728              :       END DO
    2729              :       ! Propagate the density by the effect of the delta pulse
    2730           38 :       CALL propagate_density(rtbse_env, rtbse_env%ham_workspace, rtbse_env%rho, rtbse_env%rho_new)
    2731           38 :       metric = rho_metric(rtbse_env%rho_new, rtbse_env%rho, rtbse_env%n_spin)
    2732           38 :       IF (rtbse_env%unit_nr > 0) WRITE (rtbse_env%unit_nr, ('(A42,E38.8E3)')) " RTBSE| Metric difference after delta kick", metric
    2733              :       ! Copy the new density to the old density
    2734           82 :       DO i = 1, rtbse_env%n_spin
    2735           82 :          CALL cp_cfm_to_cfm(rtbse_env%rho_new(i), rtbse_env%rho(i))
    2736              :       END DO
    2737              : 
    2738           38 :       CALL timestop(handle)
    2739           38 :    END SUBROUTINE apply_delta_pulse_MO
    2740              : 
    2741              : ! **************************************************************************************************
    2742              : !> \brief Zero the OO and VV blocks of an MO-basis derivative cfm in place. Used to enforce
    2743              : !>        strict linear response on the RK4 derivatives so that the linearized propagator
    2744              : !>        only carries the OV/VO branches and the OO/VV orbital-energy spreads do not enter
    2745              : !>        the RK4 stability bound.
    2746              : !> \param rtbse_env Entry point - rtbse environment
    2747              : !> \param cfm Derivative-like cfm in MO basis for one spin channel (modified in place)
    2748              : !> \param i_spin Spin index
    2749              : ! **************************************************************************************************
    2750         2360 :    SUBROUTINE project_drho_to_ov(rtbse_env, cfm, i_spin)
    2751              :       TYPE(rtbse_env_type), INTENT(IN)                   :: rtbse_env
    2752              :       TYPE(cp_cfm_type), INTENT(INOUT)                   :: cfm
    2753              :       INTEGER, INTENT(IN)                                :: i_spin
    2754              : 
    2755         2360 :       COMPLEX(kind=dp), DIMENSION(:, :), POINTER         :: local_data
    2756              :       INTEGER                                            :: i_global, i_global_mo, i_local, &
    2757              :                                                             j_global, j_global_mo, j_local, n_occ, &
    2758              :                                                             ncol_local, nrow_local, shift
    2759         2360 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2760              : 
    2761         2360 :       n_occ = rtbse_env%n_occ(i_spin)
    2762         2360 :       shift = rtbse_env%first_active_mo - 1
    2763              : 
    2764              :       CALL cp_cfm_get_info(matrix=cfm, &
    2765              :                            nrow_local=nrow_local, ncol_local=ncol_local, &
    2766         2360 :                            row_indices=row_indices, col_indices=col_indices)
    2767         2360 :       local_data => cfm%local_data
    2768              : 
    2769              :       ! keep OV/VO, zero OO/VV: project Δρ onto the δ-kick sectors
    2770        30680 :       DO j_local = 1, ncol_local
    2771        28320 :          j_global = col_indices(j_local)
    2772        28320 :          j_global_mo = j_global + shift
    2773       200600 :          DO i_local = 1, nrow_local
    2774       169920 :             i_global = row_indices(i_local)
    2775       169920 :             i_global_mo = i_global + shift
    2776       169920 :             IF ((i_global_mo <= n_occ .AND. j_global_mo <= n_occ) .OR. &
    2777        28320 :                 (i_global_mo > n_occ .AND. j_global_mo > n_occ)) THEN
    2778       143960 :                local_data(i_local, j_local) = CMPLX(0.0_dp, 0.0_dp, kind=dp)
    2779              :             END IF
    2780              :          END DO
    2781              :       END DO
    2782         2360 :    END SUBROUTINE project_drho_to_ov
    2783              : 
    2784              : ! **************************************************************************************************
    2785              : !> \brief Per-spin electron numbers from the MO density: N_e^σ = spin_degeneracy * Re Tr[ρ^σ],
    2786              : !>        returned as one entry per spin channel (alpha/beta). The node-local diagonal partial
    2787              : !>        sums are reduced over the BLACS grid before scaling; the imaginary trace is a
    2788              : !>        non-Hermiticity diagnostic.
    2789              : !> \param rtbse_env Entry point - rtbse environment
    2790              : !> \param rho Density matrix in MO basis (per spin)
    2791              : !> \param electron_n_re Real electron number per spin channel (size n_spin)
    2792              : !> \param electron_n_im Imaginary electron number per spin channel (numerical non-hermiticity)
    2793              : ! **************************************************************************************************
    2794          710 :    SUBROUTINE get_electron_number_MO(rtbse_env, rho, electron_n_re, electron_n_im)
    2795              :       TYPE(rtbse_env_type)                               :: rtbse_env
    2796              :       TYPE(cp_cfm_type), DIMENSION(:), POINTER           :: rho
    2797              :       REAL(kind=dp), DIMENSION(:), INTENT(OUT)           :: electron_n_re, electron_n_im
    2798              : 
    2799              :       CHARACTER(len=*), PARAMETER :: routineN = 'get_electron_number_MO'
    2800              : 
    2801          710 :       COMPLEX(kind=dp), DIMENSION(:, :), POINTER         :: local_data
    2802              :       INTEGER                                            :: handle, i_global, i_local, j, j_global, &
    2803              :                                                             j_local, ncol_local, nrow_local
    2804          710 :       INTEGER, DIMENSION(:), POINTER                     :: col_indices, row_indices
    2805              : 
    2806          710 :       CALL timeset(routineN, handle)
    2807         1540 :       electron_n_re(:) = 0.0_dp
    2808         1540 :       electron_n_im(:) = 0.0_dp
    2809         1540 :       DO j = 1, rtbse_env%n_spin
    2810              :          CALL cp_cfm_get_info(matrix=rho(j), &
    2811              :                               nrow_local=nrow_local, &
    2812              :                               ncol_local=ncol_local, &
    2813              :                               row_indices=row_indices, &
    2814          830 :                               col_indices=col_indices)
    2815          830 :          local_data => rho(j)%local_data
    2816              :          ! accumulate Tr[ρ^σ] = sum_m ρ^σ_mm (real and imaginary parts separately)
    2817         5690 :          DO i_local = 1, nrow_local
    2818         4860 :             i_global = row_indices(i_local)
    2819              :             ! Search column indices for the diagonal position
    2820        32060 :             DO j_local = 1, ncol_local
    2821        31230 :                j_global = col_indices(j_local)
    2822        31230 :                IF (j_global == i_global) THEN
    2823              :                   ! Found diagonal element
    2824         4860 :                   electron_n_re(j) = electron_n_re(j) + REAL(local_data(i_local, j_local), kind=dp)
    2825         4860 :                   electron_n_im(j) = electron_n_im(j) + AIMAG(local_data(i_local, j_local))
    2826         4860 :                   EXIT
    2827              :                END IF
    2828              :             END DO
    2829              :          END DO
    2830              :          ! reduce the per-rank partial traces over the process grid (the MO diagonal is distributed)
    2831          830 :          CALL rho(j)%matrix_struct%para_env%sum(electron_n_re(j))
    2832          830 :          CALL rho(j)%matrix_struct%para_env%sum(electron_n_im(j))
    2833              :          ! N_e^σ = spin_degeneracy * Tr[ρ^σ] (g=2 closed shell; 1 per channel open shell)
    2834          830 :          electron_n_re(j) = electron_n_re(j)*rtbse_env%spin_degeneracy
    2835         2370 :          electron_n_im(j) = electron_n_im(j)*rtbse_env%spin_degeneracy
    2836              :       END DO
    2837              : 
    2838          710 :       CALL timestop(handle)
    2839          710 :    END SUBROUTINE get_electron_number_MO
    2840              : 
    2841              : END MODULE rt_bse_linearized
        

Generated by: LCOV version 2.0-1