Example 16 Plot S2 absorption spectrum of pyrazine

Model Hamiltonian

The parameters in the Hamiltonian are given in electron volts (eV) while, in the calculation, these values were converted to atomic units (a.u.) . This model contains two excited states (S1 and S2).

\[\begin{split}\begin{align} &\text{H}=\text{H}_{\text{el}}+\text{H}_{\text{vib}}+\text{H}_{G_{1}}+\text{H}_{G_{2}}+\text{H}_{G_{3}}+\text{H}_{G_{4}} , \\ &\text{H}_{\text{el}}=\begin{pmatrix} -0.4230 & 0 \\ 0 & 0.4230 \end{pmatrix} , \\ &\text{H}_{\text{vib}}=\sum_{i\in \{6a,1,9a...,16b,11\}}\frac{\omega_{i}}{2}(P_{i}^{2}+Q_{i}^{2}) , \\ &\text{H}_{G_{1}}= \sum_{i \in G_{1}}\underbrace{\begin{pmatrix} a_{i} & 0 \\ 0 & b_{i}\end{pmatrix}Q_{i}}_{\text{5 terms}} , \\ &\text{H}_{G_{2}}= \sum_{(i,j) \in G_{2}}\underbrace{\begin{pmatrix}a_{ij} & 0 \\ 0 & b_{ij}\end{pmatrix}Q_{i}Q_{j}}_{\text{40 terms}} , \\ &\text{H}_{G_{3}}= \sum_{i \in G_{3}}\underbrace{\begin{pmatrix} 0 & c_{i} \\ c_{i} & 0\end{pmatrix}Q_{i}}_{\text{1 term}} , \\ &\text{H}_{G_{4}}= \sum_{(i,j) \in G_{4}}\underbrace{\begin{pmatrix}0 & c_{ij} \\ c_{ij} & 0\end{pmatrix}Q_{i}Q_{j}}_{\text{29 terms}} \end{align}\end{split}\]

1.Import modules

[1]:
import platform
import sys

import pytdscf

print(sys.version)
print(f"pytdscf version = {pytdscf.__version__}")
print(platform.platform())
3.10.19 (main, Oct  9 2025, 15:25:03) [Clang 16.0.0 (clang-1600.0.26.6)]
pytdscf version = 1.3.0
macOS-26.2-arm64-arm-64bit
[2]:
import matplotlib.pyplot as plt
import netCDF4 as nc
import numpy as np
import sympy
from pympo import AssignManager, OpSite, SumOfProducts

from pytdscf import (
    BasInfo,
    Boson,
    Exciton,
    Model,
    Simulator,
    TensorHamiltonian,
    TensorOperator,
    units,
)

2.Set model parameters

[3]:
backend = "numpy"

# (eV)
# delta, omega coeff data : taken from https://pubs.aip.org/aip/jcp/article-abstract/110/2/936/475747
delta = 0.4230

omega = [
    0.0739, 0.1258, 0.1525, 0.1961, 0.3788, # Ag (6a, 1, 9a, 8a, 2)
    0.1139,                                 # B1g (10a)
    0.0937, 0.1219,                         # B2g (4, 5)
    0.0873, 0.1669, 0.1891, 0.3769,         # B3g (6b, 3, 8b, 7b)
    0.0423, 0.1190,                         # Au (16a, 17a)
    0.1266, 0.1408, 0.1840, 0.3734,         # B1u (12, 18a, 19a, 13)
    0.1318, 0.1425, 0.1756, 0.3798,         # B2u (18b, 14, 19b, 20b)
    0.0521, 0.0973                          # B3u (16b, 11)
]

# data of G1 (eV)
# k_mode : [a_i, b_i]
g1_data = {
    0: [-0.0981,  0.1355], # 6a
    1: [-0.0503, -0.1710], # 1
    2: [ 0.1452,  0.0375], # 9a
    3: [-0.0445,  0.0168], # 8a
    4: [ 0.0247,  0.0162]  # 2
}


# data of G2 (eV)
# B1g x Ag
# except Ag x Ag
# (k_mode, l_mode) : [aij, bij]
g2_data = {
    # Au modes (v16a=omega13, v17a=omega17)
    (13, 13): [ 0.01145, -0.01459], # 16a * 16a
    (17, 17): [-0.02040, -0.00618], # 17a * 17a
    (13, 17): [ 0.00100, -0.00091], # 16a * 17a

    # B1g mode (v10a:omega5)
    (5, 5): [-0.01159, -0.01159],   # 10a * 10a

    # B2g modes (v4:omega6, v5:omega11)
    (6, 6): [-0.02252, -0.03445],   # 4 * 4
    (11, 11): [-0.01825, -0.00265], # 5 * 5
    (6, 11): [-0.00049,  0.00911],  # 4 * 5

    # B3g modes (v6b:omega7, v3:omega8, v8b:omega9, v7b:omega10)
    (7, 7): [-0.00741, -0.00385],   # 6b * 6b
    (8, 8): [ 0.05183,  0.04842],   # 3 * 3
    (9, 9): [-0.05733, -0.06332],   # 8b * 8b
    (10, 10): [-0.00333, -0.00040], # 7b * 7b
    (7, 8): [ 0.01321, -0.00661],   # 6b * 3
    (7, 9): [-0.00717,  0.00429],   # 6b * 8b
    (7, 10): [ 0.00515, -0.00246],  # 6b * 7b
    (8, 9): [-0.03942, -0.03034],   # 3 * 8b
    (8, 10): [ 0.00170, -0.00185],  # 3 * 7b
    (9, 10): [-0.00204, -0.00388],  # 8b * 7b

    # B1u modes (v12:omega12, v18a:omega14, v19a:omega15, v13:omega16)
    (12, 12): [-0.04819, -0.00840], # 12 * 12
    (14, 14): [-0.00792,  0.00429], # 18a * 18a
    (15, 15): [-0.02429, -0.00734], # 19a * 19a
    (16, 16): [-0.00492,  0.00346], # 13 * 13
    (12, 14): [ 0.00525,  0.00536], # 12 * 18a
    (12, 15): [-0.00485, -0.00097], # 12 * 19a
    (12, 16): [-0.00326,  0.00034], # 12 * 13
    (14, 15): [ 0.00852,  0.00209], # 18a * 19a
    (14, 16): [ 0.00888, -0.00049], # 18a * 13
    (15, 16): [-0.00443,  0.00346], # 19a * 13

    # B2u modes (v18b:omega18, v14:omega20, v19b:omega21, v20b:omega22)
    (18, 18): [-0.00277, -0.01179], # 18b * 18b
    (20, 20): [ 0.03924,  0.04000], # 14 * 14
    (21, 21): [ 0.00992,  0.01246], # 19b * 19b
    (22, 22): [-0.00110,  0.00069], # 20b * 20b
    (18, 20): [ 0.00016, -0.00844], # 18b * 14
    (18, 21): [-0.00250,  0.07000], # 18b * 19b
    (18, 22): [ 0.00357, -0.01249], # 18b * 20b
    (20, 21): [-0.00197, -0.05000], # 14 * 19b
    (20, 22): [-0.00355,  0.00265], # 14 * 20b
    (21, 22): [ 0.00623, -0.00422], # 19b * 20b

    # B3u modes (v16b:omega19, v11:omega23)
    (19, 19): [-0.02176, -0.02214], # 16b * 16b
    (23, 23): [ 0.00315, -0.00496], # 11 * 11
    (19, 23): [-0.00624, -0.00261], # 16b * 11
}

# data of G3 (eV)
# mode 10a(omega5) : c_i
g3_val = 0.2080

# data of G4 (eV)
# (k_mode, l_mode) : c_ij
# v6a:omega0, v1:omega1, v9a:omega2, v8a:omega3, v2:omega4, v10a:omega5,
# v4:omega6, v6b:omega7, v3:omega8, v8b:omega9, v7b:omega10, v5:omega11,
# v12:omega12, v16a:omega13, v18a:omega14, v19a:omega15, v13:omega16, v17a:omega17,
# v18b:omega18, v16b:omega19, v14:omega20, v19b:omega21, v20b:omega22, v11:omega23

g4_data = {
    # B1g x Ag
    (5, 0): -0.01000,  # 10a * 6a
    (5, 1): -0.00551,  # 10a * 1
    (5, 2):  0.00127,  # 10a * 9a
    (5, 3):  0.00799,  # 10a * 8a
    (5, 4): -0.00512,  # 10a * 2

    # B2g x B3g
    (6, 7): -0.01372,  # 4 * 6b
    (6, 8): -0.00466,  # 4 * 3
    (6, 9):  0.00329,  # 4 * 8b
    (6, 10):-0.00031,  # 4 * 7b
    (11, 7): 0.00598,  # 5 * 6b
    (11, 8):-0.00914,  # 5 * 3
    (11, 9): 0.00961,  # 5 * 8b
    (11, 10): 0.00500, # 5 * 7b

    # Au x B1u
    (13, 12): -0.01056, # 16a * 12
    (13, 14):  0.00559, # 16a * 18a
    (13, 15):  0.00401, # 16a * 19a
    (13, 16): -0.00226, # 16a * 13
    (17, 12): -0.01200, # 17a * 12
    (17, 14): -0.00213, # 17a * 18a
    (17, 15):  0.00328, # 17a * 19a
    (17, 16): -0.00396, # 17a * 13

    # B3u x B2u
    (19, 18):  0.00118, # 16b * 18b
    (19, 20): -0.00009, # 16b * 14
    (19, 21): -0.00285, # 16b * 19b
    (19, 22): -0.00095, # 16b * 20b
    (23, 18):  0.01281, # 11 * 18b
    (23, 20): -0.01780, # 11 * 14
    (23, 21):  0.00134, # 11 * 19b
    (23, 22): -0.00481  # 11 * 20b
}

[4]:
delta_sym = delta/ units.au_in_eV

omega_syms = [sympy.Symbol(f"omega_{i}") for i in range(len(omega))]

subs = {}
for omega_val, omega_sym in zip(
    omega, omega_syms, strict=True
):
    subs[omega_sym] = omega_val / units.au_in_eV

3.Setup basis for wavefunction

[5]:
nmode = len(omega)
nsite = (nmode + 1)
basis = []
for isite in range(nsite):
    if isite == 0:
        basis.append(Exciton(nstate=2))
    else:
        basis.append(Boson(nstate=10))
basinfo = BasInfo([basis])

4.Setup operator

[6]:
p = basis[1].get_p_matrix()
q = basis[1].get_q_matrix()
pp = basis[1].get_p2_matrix()
qq = basis[1].get_q2_matrix()
[7]:
q_ops = []
p_ops = []
qq_ops = []
pp_ops = []

for isite in range(nsite):
    if isite == 0:
        q_ops.append(None)
        p_ops.append(None)
        qq_ops.append(None)
        pp_ops.append(None)

    else:
        q_ops.append(OpSite("Q_{" + f"{isite}" + "}", isite, value=q))
        p_ops.append(OpSite("P_{" + f"{isite}" + "}", isite, value=p))
        qq_ops.append(OpSite("Q2_{" + f"{isite}" + "}", isite, value=qq))
        pp_ops.append(OpSite("P2_{" + f"{isite}" + "}", isite, value=pp))

[8]:
sop = SumOfProducts()

# H_el
el_mat = np.zeros((2, 2), dtype=np.float64)
el_mat[0,0]= -delta_sym
el_mat[1,1]= delta_sym
sop += OpSite("H_el", 0, value=el_mat)

# (eV) -> (au)
ev_to_au = 1.0 / units.au_in_eV

for k_mode in range(nmode):
    jsite = 1 + k_mode

    # H_vib
    sop += 0.5*omega_syms[k_mode] * (pp_ops[jsite] + qq_ops[jsite])

    # H_G1
    if k_mode in g1_data:
        coeffs = g1_data[k_mode]
        mat_g1 = np.diag(coeffs) * ev_to_au
        sop += OpSite(f"V_G1_{k_mode}", 0, value=mat_g1) * q_ops[jsite]

    # H_G3
    elif k_mode == 5: # index 5 = 10a
        mat_g3 = np.array([[0.0,    g3_val],[g3_val, 0.0]]) * ev_to_au
        sop += OpSite(f"V_G3_{k_mode}", 0, value=mat_g3) * q_ops[jsite]


# H_G2
for (k_mode, l_mode), coeff in g2_data.items():
    j_1site = k_mode + 1
    j_2site = l_mode + 1
    mat_g2_current = np.diag(coeff) * ev_to_au
    sop += OpSite(f"V_G2_{k_mode}_{l_mode}", 0, value=mat_g2_current) * q_ops[j_1site] * q_ops[j_2site]


# H_G4
for (k_mode, l_mode), coeff in g4_data.items():
    j_1site = k_mode + 1
    j_2site = l_mode + 1
    mat_g4 = np.array([[0.0, 1.0], [1.0, 0.0]]) * (coeff * ev_to_au)
    sop += OpSite(f"V_G4_{k_mode}_{l_mode}", 0, value=mat_g4) * q_ops[j_1site] * q_ops[j_2site]


sop = sop.simplify()

5.Setup MPO

[9]:
am_pot = AssignManager(sop)
_ = am_pot.assign()
pot_mpo = am_pot.numerical_mpo(subs=subs)

6.Setup Hamiltonian

[10]:
potential = [
    [{tuple((k, k) for k in range(nsite)): TensorOperator(mpo=pot_mpo)}]
]
H = TensorHamiltonian(
    ndof=len(basis), potential=potential, kinetic=None, backend=backend
)
operators = {"hamiltonian": H}

7.Setup initial states

[11]:
model = Model(basinfo=basinfo, operators=operators)
model.m_aux_max = 20


S2_state = [0.0, 1.0]


nprim_boson = basis[1].nprim if len(basis) > 1 else 10
boson_site = [1.0] + [0.0] * (nprim_boson - 1)


initial_weights = []
for isite in range(nsite):
    if isite == 0:
        initial_weights.append(S2_state)
    else:
        initial_weights.append(boson_site)

model.init_HartreeProduct = [initial_weights]

8.Execution

[12]:
jobname = "pyrazine"
simulator = Simulator(jobname=jobname, model=model, backend=backend, verbose=2)
simulator.propagate(
    maxstep=1500,
    stepsize=0.1,
    reduced_density=(
        [(0, 0)],
        10,
    ),
    energy=True,
    autocorr=True,
    observables=False,
    observables_per_step=10
)
00:08:05 | INFO | Log file is ./pyrazine_prop/main.log
00:08:05 | INFO | Wave function is saved in wf_pyrazine.pkl
00:08:05 | INFO | The time unit in the netCDF file is fs
00:08:05 | INFO | Start initial step    0.000 [fs]
00:08:07 | INFO | End     0 step; propagated    0.100 [fs]; AVG Krylov iteration: 6.76
00:10:27 | INFO | End   100 step; propagated   10.100 [fs]; AVG Krylov iteration: 6.76
00:12:48 | INFO | End   200 step; propagated   20.100 [fs]; AVG Krylov iteration: 6.76
00:15:07 | INFO | End   300 step; propagated   30.100 [fs]; AVG Krylov iteration: 6.96
00:17:34 | INFO | End   400 step; propagated   40.100 [fs]; AVG Krylov iteration: 6.96
00:19:52 | INFO | End   500 step; propagated   50.100 [fs]; AVG Krylov iteration: 6.96
00:22:11 | INFO | End   600 step; propagated   60.100 [fs]; AVG Krylov iteration: 6.96
00:24:29 | INFO | End   700 step; propagated   70.100 [fs]; AVG Krylov iteration: 6.96
00:26:51 | INFO | End   800 step; propagated   80.100 [fs]; AVG Krylov iteration: 6.96
00:29:13 | INFO | End   900 step; propagated   90.100 [fs]; AVG Krylov iteration: 6.96
00:31:33 | INFO | Saved wavefunction   99.900 [fs]
00:31:36 | INFO | End  1000 step; propagated  100.100 [fs]; AVG Krylov iteration: 6.96
00:33:53 | INFO | End  1100 step; propagated  110.100 [fs]; AVG Krylov iteration: 6.96
00:36:17 | INFO | End  1200 step; propagated  120.100 [fs]; AVG Krylov iteration: 6.96
00:38:35 | INFO | End  1300 step; propagated  130.100 [fs]; AVG Krylov iteration: 6.96
00:40:56 | INFO | End  1400 step; propagated  140.100 [fs]; AVG Krylov iteration: 6.96
00:43:15 | INFO | End  1499 step; propagated  149.900 [fs]; AVG Krylov iteration: 6.96
00:43:15 | INFO | End simulation and save wavefunction
00:43:15 | INFO | Wave function is saved in wf_pyrazine.pkl
[12]:
(0.08830243567021843, <pytdscf.wavefunction.WFunc at 0x119813160>)

9.Plot (population, absorption spectrum)

[13]:
with nc.Dataset("pyrazine_prop/reduced_density.nc", "r") as file:
    density_data_real = file.variables[f"rho_({0}, {0})_0"][:][
        "real"
    ]
    density_data_imag = file.variables[f"rho_({0}, {0})_0"][:][
        "imag"
    ]
    time_data = file.variables["time"][:]
[14]:
plt.ylabel("population")
plt.xlabel("time / fs")
plt.plot(time_data, density_data_real[:, 0, 0], label="S1")
plt.plot(time_data, density_data_real[:, 1, 1 ], label="S2")
plt.grid()
plt.legend()
plt.show()
../_images/notebook_pyrazine-qvc_24_0.png
[15]:
# omega : taken from https://pubs.aip.org/aip/jcp/article-abstract/110/2/936/475747
omega = [
    0.0739, 0.1258, 0.1525, 0.1961, 0.3788, # Ag (6a, 1, 9a, 8a, 2)
    0.1139,                                 # B1g (10a)
    0.0937, 0.1219,                         # B2g (4, 5)
    0.0873, 0.1669, 0.1891, 0.3769,         # B3g (6b, 3, 8b, 7b)
    0.0423, 0.1190,                         # Au (16a, 17a)
    0.1266, 0.1408, 0.1840, 0.3734,         # B1u (12, 18a, 19a, 13)
    0.1318, 0.1425, 0.1756, 0.3798,         # B2u (18b, 14, 19b, 20b)
    0.0521, 0.0973                          # B3u (16b, 11)
]

E_0 = 0.5*sum(omega)-(3.94+4.89)/2 #experimental data (eV) : taken from https://doi.org/10.1063/1.466618
time, autocorr = pytdscf.spectra.load_autocorr(
    "pyrazine_prop/autocorr.dat"
)

def h(t,tau = 150):
    return np.exp(-np.abs(t)/tau)

freq, intensity = pytdscf.spectra.ifft_autocorr(time, autocorr * h(time), E_shift=E_0,window="cos")

# (cm^-1) -> (nm)
mask = freq > 0
wavelength_nm = 1e7 / freq[mask]
intensity_masked = intensity[mask]

plt.figure(figsize=(6, 4))
plt.plot(wavelength_nm, intensity_masked/ np.max(intensity_masked), color='red', lw=2)
plt.xlabel('wavelength / nm')
plt.ylabel('intensity')
plt.title('absorption spectrum')
plt.xlim(220, 280)
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
delta_t:  0.100[fs], max_freq: 333564.1[cm-1]
../_images/notebook_pyrazine-qvc_25_1.png