Algorithms#
We need to compute
where \(\delta\) is simulated with a Lorentzian of half width at half maximum \(\Gamma\).
Direct solution#
EDRIXS hosts a pure Python implementation of the cross-section that is suitable for performance insensitive cases and testing. This was what we used in the atomic model section. See the diagonalization step in edrixs.solvers.ed_1v1c_py and spectrum construction in edrixs.solvers.rixs_1v1c_py. The following is a crude illustration of what happens
Build
ematandumatin \(Y_l^m\) single-particle basis
import edrixs
import scipy
import numpy as np
ten_dq = 1.0
emat_i = np.zeros((14, 14), dtype=complex)
emat_i[:10, :10] = edrixs.cf_cubic_d(ten_dq) # plus SOC and hopping
F0, F2, F4 = 4.0, 1.0, 0.3
umat_i = np.zeros((14, 14, 14, 14), dtype=complex)
umat_i = edrixs.get_umat_slater('d', F0, F2, F4) # plus core hole terms
# For L3-edge RIXS of d-shell material
print(f"emat is 14x14")
print(f"umat is 14x14x14x14")
emat is 14x14
umat is 14x14x14x14
Construct Fock basis for \(\ket{i}\) and \(\ket{n}\).
v_norb = 10
v_noccu = 8
c_norb = 4
basis_i = edrixs.get_fock_bin_by_N(v_norb, v_noccu, c_norb, c_norb)
basis_n = edrixs.get_fock_bin_by_N(v_norb, v_noccu+1, c_norb, c_norb - 1)
print(np.array(basis_i)[:3, :10])
[[1 1 1 1 1 1 1 1 0 0]
[1 1 1 1 1 1 1 0 1 0]
[1 1 1 1 1 1 1 0 0 1]]
Build Hamiltonian for initial and intermediate state using edrixs.two_fermion and edrixs.four_fermion.
ncfg_i, ncfg_n = len(basis_i), len(basis_n)
print(f"{ncfg_i} ground state vectors")
print(f"{ncfg_n} intermediate state vectors")
hmat_i = np.zeros((ncfg_i, ncfg_i), dtype=complex)
hmat_n = np.zeros((ncfg_n, ncfg_n), dtype=complex)
hmat_i[:, :] += edrixs.two_fermion(emat_i, basis_i, basis_i)
hmat_i[:, :] += edrixs.four_fermion(umat_i, basis_i)
# hmat_n left out for brevity.
45 ground state vectors
40 intermediate state vectors
Diagonalize Hamiltonian for initial and intermediate state using scipy, which calls LAPACK under the hood.
eval_i, evec_i = scipy.linalg.eigh(hmat_i)
eval_n, evec_n = scipy.linalg.eigh(hmat_n)
Load relevant dipole matrix operators
trans_op_Ylm = edrixs.get_trans_oper('dp32')
Build the many-body transition operators for each polarization and express them in the eigenbasis
npol = 3
trans_op = np.zeros((npol, ncfg_n, ncfg_i), dtype=complex)
for i in range(npol):
trans_op[i] = edrixs.cb_op2(edrixs.two_fermion(trans_op_Ylm[i], basis_n, basis_i),
evec_n, evec_i)
print(trans_op.shape)
(3, 40, 45)
RIXS spectra can be built from
trans_op,eval_i, andeval_n.
Current EDRIXS solver#
Currently, EDRIXS calls into its Fortran layer. This is sparsely documented and suffers from serious limitations in portability, maintainability, and flexibility. See edrixs.ed_siam_fort and edrixs.rixs_siam_fort, which calls ed_driver.f90 and rixs_driver.f90. We used this in the Anderson impurity model section.
Mathematics#
To do the calculation, it is useful to reformulate the cross-section compared to the most common Kramers-Heisenberg form. We first note that one can re-express \(M_{fi}\) as
where \({\cal \widetilde{H}}\) is the intermediate state Hamiltonian with \(\widetilde{\mathcal H}\ket{n} = E_n\ket{n}\) and \(\sum_n \ket{n}\bra{n} = \mathbb{I}.\) To see this, note that using a spectral decomposition we can write
Inserting this operator between the dipole operators, we get
By rearranging, we can show that this is equivalent to the original equation with
The next step is to define an effective scattering operator
so that we have
Since the set of \(|f\rangle\) is mutually orthonormal, they have the property that
such that
The Dirac \(\delta\) function can be represented as a limiting case of a Lorentzian with width \(\Gamma\)
If we define the prepared state using \(|F_i\rangle \equiv \mathcal O_i|i\rangle\), and leave out a factor \(\pi\), we can assemable the full equation as
In this form, it is possible to effciently compute the spectrum using Lanczos methods. Let’s define
and
The key thing to compute is
where \(\ket{F_i} = |F_i| \ket{f_i}\) splits the vectors into a nomalized component and a normalization factor.
Krylov’s methods give us a procedure for constructing a new basis \(V_m = [\ket{q_0}, \ket{q_1}, ... \ket{q_m}]\), whose first element is \(\ket{q_0} = \ket{F_i}\), which allows us to effciently represent the Hamiltonanian in a tridiagonal form
Once this form is constructed, the components of the spectrum can be built via.
The recursive continued fraction arises because the inverse of a tridiagonal matrix can be constructed by repeatedly eliminating deeper blocks, and Lanczos produces exactly the tridiagonal structure required for this elimination to proceed step by step.
Computational methods#
The general process is:
Fock basis is generated in integer representation
for binary_string in ["011", "101", "110"]:
print(f"{binary_string} is {int(binary_string, 2)}")
011 is 3
101 is 5
110 is 6
Hamiltonian is constructed in CSR format. For example, the matrix
is encoded as
V = [10, 20, 30, 40, 50, 60, 70, 80]
COL_INDEX = [0, 1, 1, 3, 2, 3, 4, 5]
ROW_INDEX = [0, 2, 4, 7, 8]
A handful of the eigenvectors of the ground state Hamiltonian \({\cal H}\) are obtained via the Lanczos method within the parallel ARPACK library.
Apply the absorption transition operator to the ground state
Solve the following linear equation, involving the intermediate state Hamiltonian \({\cal \widetilde{H}}\) via sparse MINRES methods
Apply emission operator
The spectrum can then be represented as
The continued fraction technique is then used to construct the spectrum
Plans#
We are still finalizing the planned approach.
Algorithm changes#
Infrastructure changes#
The current plan is to call into more modern platform-agnostic CPU/GPU packages PETSc+SLEPc. These, which are primarily written in C, have Python interfaces.