Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci_env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ channels:
dependencies:
- libblas =*=*mkl
- adcc >=0.16.0
- respondo >=0.0.5
- respondo >=0.0.6
- numpy >=1.14
- isort
- ruff
Expand Down
127 changes: 127 additions & 0 deletions examples/cme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Compute the Cotton-Mouton constant according to Eq. 2 in 10.1021/acs.jpca.3c04963
"""
import adcc
import numpy as np
from pyscf import gto, scf
from scipy import constants

from responsefun import evaluate_property_isr, TransitionMoment
from responsefun.symbols_and_labels import (O, n, p, m, mu_a, mu_b, m_a, m_b, m_c, m_d, xi_cd,
w_n, w_p, w_o, w_2, w_m, w_3, w, w_1
)
from responsefun.AdccProperties import DiamagneticMagnetizability

# run SCF in PySCF
mol = gto.M(
atom="""
O 0 0 0
H 0 0 1.795239827225189
H 1.693194615993441 0 -0.599043184453037
""",
unit="Bohr",
basis="sto-3g"
)
scfres = scf.RHF(mol)
scfres.kernel()

# run ADC calculation using adcc
state = adcc.adc2(scfres, n_singlets=1)

# define (first term of) symbolic SOS expressions
alpha_sos_expr = (
TransitionMoment(O, mu_a, n) * TransitionMoment(n, mu_b, O) / (w_n - w)
+ TransitionMoment(O, mu_b, n) * TransitionMoment(n, mu_a, O) / (w_n + w)
)
xi_para_sos_expr = (
TransitionMoment(O, m_a, n) * TransitionMoment(n, m_b, O) / (w_n - w)
+ TransitionMoment(O, m_b, n) * TransitionMoment(n, m_a, O) / (w_n + w)
)
eta_dia_sos_expr = (
TransitionMoment(O, mu_a, n) * TransitionMoment(n, mu_b, p, shifted=True)
* TransitionMoment(p, xi_cd, O) / ((w_n - w_o) * (w_p - w_2))
)
eta_para_term_I_sos_expr = (
TransitionMoment(O, mu_a, n) * TransitionMoment(n, mu_b, m, shifted=True)
* TransitionMoment(m, m_c, p, shifted=True) * TransitionMoment(p, m_d, O)
/ ((w_n - w_o) * (w_m - w_2 - w_3) * (w_p - w_3))
)
eta_para_term_II_sos_expr = (
TransitionMoment(O, mu_a, n) * TransitionMoment(n, mu_b, O)
* TransitionMoment(O, m_c, m) * TransitionMoment(m, m_d, O)
/ ((w_n - w_o) * (w_m - w_3) * (w_m + w_2))
)

w_ruby = 0.072
gauge_origin = "mass_center"

# calculate tensors
alpha_tens = evaluate_property_isr(
state, # ExcitedStates object returned by adcc calculation
alpha_sos_expr, # symbolic SOS expression
[n], # indices of summation
excluded_states=O,# states excluded from summation (here: ground state)
freqs_in=(w, w_ruby), # incoming frequencies
freqs_out=(w, w_ruby), # outcoming frequencies
conv_tol=1e-4, # convergence tolerance for response solver
gauge_origin=gauge_origin # gauge origin for operator integrals
)

xi_para_tens = evaluate_property_isr(
state, xi_para_sos_expr, [n], excluded_states=O,
freqs_in=(w, 0), freqs_out=(w, 0),
conv_tol=1e-4,
gauge_origin=gauge_origin
)

xi_dia_tens = DiamagneticMagnetizability(state, gauge_origin=gauge_origin).gs_moment

eta_dia_tens = evaluate_property_isr(
state, eta_dia_sos_expr, [n, p],
perm_pairs=[(mu_a, -w_o), (mu_b, w_1), (xi_cd, w_2)], # pairs to be permuted
freqs_in=[(w_1, w_ruby), (w_2, 0)],
freqs_out=(w_o, w_1+w_2),
excluded_states=O,
conv_tol=1e-4,
gauge_origin=gauge_origin
)

eta_para_tens_I = evaluate_property_isr(
state, eta_para_term_I_sos_expr, [n, m, p],
perm_pairs=[(mu_a, -w_o), (mu_b, w_1), (m_c, w_2), (m_d, w_3)],
excluded_states=O,
freqs_in=[(w_1, w_ruby), (w_2, 0), (w_3, 0)],
freqs_out=(w_o, w_1+w_2+w_3),
conv_tol=1e-4,
gauge_origin=gauge_origin
)

eta_para_tens_II = evaluate_property_isr(
state, eta_para_term_II_sos_expr, [n, m],
perm_pairs=[(mu_a, -w_o), (mu_b, w_1), (m_c, w_2), (m_d, w_3)],
excluded_states=O,
freqs_in=[(w_1, w_ruby), (w_2, 0), (w_3, 0)],
freqs_out=(w_o, w_1+w_2+w_3),
conv_tol=1e-4,
gauge_origin=gauge_origin
)

# add dia- and paramagnetic contributions
xi_tot = xi_dia_tens + xi_para_tens
eta_tot = eta_dia_tens + (eta_para_tens_I - eta_para_tens_II)

# calculate anisotropies
alpha_aniso = alpha_tens[2, 2] - alpha_tens[0, 0]
xi_aniso = xi_tot[2, 2] - xi_tot[0, 0]
eta_aniso = 1/15 * (7 * eta_tot[0, 0, 0, 0] - 5 * eta_tot[0, 0, 1, 1] + 2 * eta_tot[2, 2, 2, 2]
- 2 * eta_tot[0, 0, 2, 2] - 2 * eta_tot[2, 2, 0, 0] + 12 * eta_tot[0, 2, 0, 2])

# compute CME constant in cgs system [cm^3 G^–2 mol^–1] at T=298.15K
pi = constants.pi
N_A = constants.N_A
Hartree = constants.physical_constants['atomic unit of energy'][0]
k_B = constants.k/Hartree
T = 273.15
const_CME = ((2 * np.pi *N_A)/27 * (eta_aniso + 2/ (15 * k_B * T ) * alpha_aniso * xi_aniso))
const_CME_cgs = const_CME * 2.68211e-44 * 1e20
print(f"The Cotton-Mouton constant at 298.15 K is {const_CME_cgs:.2f} (10^-20 cm^3 G^–2 mol^–1).")
101 changes: 101 additions & 0 deletions examples/tpcd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Compute the two-photon circular dichroism rotatory strength in the velocity gauge
according to 10.1021/acs.jpca.5c02108 eqs. 21-27.
"""
import adcc
import numpy as np
from pyscf import gto, scf

from responsefun import evaluate_property_isr, TransitionMoment
from responsefun.misc import epsilon
from responsefun.symbols_and_labels import (O, f, n, m_b, mup_a, mup_b, mup_c,
qp_ab, w_a, w_b, w_f, w_n)

# The calculation of two-photon circular dichroism requires three different two-photon tensors.
# To avoid code duplication, the following two functions are defined.
def compute_tp_tensor(state, sos_expr, n_f, perm_pairs, gauge_origin, conv_tol=1e-4):
tensor = evaluate_property_isr(
state, # ExcitedStates object returned by adcc calculation
sos_expr, # first term of symbolic SOS expression
[n], # indices of summation
excluded_states=O, # states excluded from summation (here: ground state)
excited_state=n_f, # excited state of interest (here: final state)
freqs_in=[(w_a, w_f / 2), (w_b, w_f / 2)], # incoming frequencies
perm_pairs=perm_pairs, # pairs to be permuted
gauge_origin=gauge_origin, # gauge origin for operator integrals
conv_tol=conv_tol # convergence tolerance for response solver
)
return tensor

# b1, b2, and b3 are defined by the experimental setup.
def compute_rotatory_strength(tpcd_data, final_state, b1=6, b2=2, b3=-2):
external_energy = tpcd_data["excitation_energy_uncorrected"][final_state]/2
Spp = tpcd_data[f"state_{final_state}"]["Spp"]
Qpp = tpcd_data[f"state_{final_state}"]["Qpp"]
Mp = tpcd_data[f"state_{final_state}"]["Mp"]
B1_p = 1/(external_energy**3) * np.einsum("ps,ps->", np.conjugate(Mp), Spp)
B2_p = 1/(2 * (external_energy**3)) * np.einsum("ps,ps->", np.conjugate(Qpp), Spp)
B3_p = 1/(external_energy**3) * np.einsum("ss->", np.conjugate(Mp)) * np.einsum("pp->", Spp)
R_TP_p = 1.0 * b1 * B1_p + b2 * B2_p + 1.0 * b3 * B3_p
return R_TP_p

# run SCF in PySCF
mol = gto.M(
atom="""
O 0.000000 0.000000 0.000000
O 1.480000 0.000000 0.000000
H -0.316648 0.000000 0.895675
H 1.796648 0.775678 -0.447838
""",
unit="Angstrom",
basis="sto-3g",
)
scfres = scf.RHF(mol)
scfres.kernel()

# run ADC calculation using adcc
state = adcc.run_adc(scfres, method="adc2", n_singlets=2)
print(state.describe())

# define first SOS term
Mp_sos_expr = TransitionMoment(f, m_b, n, shifted=True) \
* TransitionMoment(n, mup_a, O) / (w_n - w_a)
Spp_sos_expr = TransitionMoment(f, mup_b, n, shifted=True) \
* TransitionMoment(n, mup_a, O) / (w_n - w_a)
Qpp_sos_expr = TransitionMoment(f, mup_c, n, shifted=True) \
* TransitionMoment(n, qp_ab, O) / (w_n - w_a)

# define operator-frequency pairs to be permuted
Mp_perm_pairs = [(m_b, w_b), (mup_a, w_a)]
Spp_perm_pairs = [(mup_b, w_b), (mup_a, w_a)]
Qpp_perm_pairs = [(mup_c, w_b), (qp_ab, w_a)]

tensors = {
"Mp": (Mp_sos_expr, Mp_perm_pairs),
"Spp": (Spp_sos_expr, Spp_perm_pairs),
"Qpp": (Qpp_sos_expr, Qpp_perm_pairs),
}

# define gauge origin for gauge origin dependent operator integrals
gauge_origin = "mass_center"
tpcd_data = {}

# compute two-photon tensors
for n_f in range(2):
data_state = {}
for tensor_name, (sos_expr, perm_pairs) in tensors.items():
tensor = compute_tp_tensor(state, sos_expr, n_f, perm_pairs, gauge_origin)
if len(tensor.shape) == 3:
data_state[tensor_name] = np.einsum("bcd,acd->ab", epsilon, tensor)
else:
data_state[tensor_name] = tensor

tpcd_data[f"state_{n_f}"] = data_state

tpcd_data["excitation_energy_uncorrected"] = state.excitation_energy_uncorrected

for ex_state in range(2):
R_TP_p = compute_rotatory_strength(tpcd_data, ex_state)
print("The two-photon rotatory strength in velocity gauge for excited state "
f"{ex_state} is {R_TP_p:.2f} (a.u.).")

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description = "Fun with response function in the ADC framework."
version = "0.3.0"
readme = "README.md"
authors = [
{ name = "Antonia Papapostolou, Maximilian Scheurer" }
{ name = "Antonia Papapostolou, Maximilian Scheurer, Friederike Schneider" }
]
license = {file = "LICENSE"}
# See https://pypi.org/classifiers/
Expand All @@ -22,7 +22,7 @@ requires-python = ">=3.7"
# Declare any run-time dependencies that should be installed with the package.
dependencies = [
"adcc>=0.16.0",
"respondo>=0.0.5",
"respondo>=0.0.6",
"numpy>=1.14",
"sympy<=1.13",
"tqdm",
Expand Down
49 changes: 47 additions & 2 deletions responsefun/test_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest
from adcc.Excitation import Excitation
from adcc.misc import assert_allclose_signfix
from respondo.mcd import mcd_bterm
from respondo.polarizability import (
complex_polarizability,
real_polarizability,
Expand All @@ -16,14 +17,16 @@
evaluate_property_sos,
evaluate_property_sos_fast,
)
from responsefun.misc import ev2au
from responsefun.misc import epsilon, ev2au
from responsefun.SumOverStates import TransitionMoment
from responsefun.symbols_and_labels import (
O,
f,
gamma,
j,
k,
m,
m_c,
mu_a,
mu_b,
mu_c,
Expand All @@ -35,6 +38,7 @@
w_2,
w_3,
w_f,
w_j,
w_k,
w_m,
w_n,
Expand Down Expand Up @@ -91,6 +95,20 @@ def run_scf(molecule, basis, backend="pyscf"):
),
None,
),
"mcd_term1": (
(
TransitionMoment(O, m_c, k) * TransitionMoment(k, mu_b, j, shifted=True)
* TransitionMoment(j, mu_a, O) / w_k
),
None
),
"mcd_term2": (
(
TransitionMoment(O, mu_b, k) * TransitionMoment(k, m_c, j) * TransitionMoment(j, mu_a, O)
/ (w_k - w_j)
),
None
),
"beta": (
(
TransitionMoment(O, mu_a, n)
Expand Down Expand Up @@ -141,7 +159,6 @@ def run_scf(molecule, basis, backend="pyscf"):
),
}

# TODO: add mcd test as soon as gator-program/respondo#15 is merged
@pytest.mark.parametrize("case", cache.cases)
class TestIsrAgainstRespondo:
def test_static_polarizability(self, case):
Expand Down Expand Up @@ -253,6 +270,34 @@ def test_tpa_resonant(self, case):
tpa, tpa_ref[1], atol=1e-7, err_msg="final_state = {}".format(final_state)
)

def test_mcd(self, case):
molecule, basis, method = case.split("_")
scfres = run_scf(molecule, basis)
refstate = adcc.ReferenceState(scfres)
state = adcc.run_adc(refstate, method=method, n_singlets=5)
mcd_sos_expr1 = SOS_expressions["mcd_term1"][0]
mcd_sos_expr2 = SOS_expressions["mcd_term2"][0]

for ee in state.excitations:
gauge_origin = "origin"
final_state = ee.index
excited_state = Excitation(state, final_state, method)
bterm_ref = mcd_bterm(excited_state, gauge_origin=gauge_origin)
mcd_tens1 = evaluate_property_isr(
state, mcd_sos_expr1, [k],
excluded_states=O, excited_state=final_state, gauge_origin=gauge_origin
)
mcd_tens2 = evaluate_property_isr(
state, mcd_sos_expr2, [k],
excluded_states=[O,j], excited_state=final_state, gauge_origin=gauge_origin
)

bterm = np.einsum("abc,abc->", epsilon, mcd_tens1 + mcd_tens2)

np.testing.assert_allclose(
bterm, bterm_ref, atol=1e-5, err_msg="final_state = {}".format(final_state)
)


@pytest.mark.parametrize("case", [case for case in cache.cases if case in cache.data_fulldiag])
class TestIsrAgainstSos:
Expand Down
Loading