Reorganization Energy¶
One of the ingredients for computing hopping rates in Marcus theory is the reorganization energy \(\lambda\), defined as
where states A and B are two electronic configurations, e.g. neutral and anion (see the example usage below).
In this recipe we build a job class ReorganizationEnergyJob by extending MultiJob. Our job will perform four AMSJob calculations: two geometry optimizations for states A anb B, followed by two single point calculations (state A at the optimal geometry of state B and state B at the optimal geometry of state A).
In ReorganizationEnergyResults, the reorganization energy is computed by fetching and combining the results from the children jobs.
from collections import OrderedDict
from scm.plams.core.basejob import MultiJob
from scm.plams.core.functions import add_to_instance
from scm.plams.core.results import Results
from scm.plams.interfaces.adfsuite.ams import AMSJob
__all__ = ["ReorganizationEnergyJob", "ReorganizationEnergyResults"]
# using this function to pass a molecule inside a MultiJob ensures proper parallel execution
def pass_molecule(source, target):
@add_to_instance(target)
def prerun(self): # noqa F811
self.molecule = source.results.get_main_molecule()
class ReorganizationEnergyResults(Results):
"""Results class for reorganization energy."""
def reorganization_energy(self, unit="au"):
energies = self.get_all_energies(unit)
reorganization_energy = (
energies["state B geo A"]
- energies["state A geo A"]
+ energies["state A geo B"]
- energies["state B geo B"]
)
return reorganization_energy
def get_all_energies(self, unit="au"):
energies = {}
energies["state A geo A"] = self.job.children["go_A"].results.get_energy(unit=unit)
energies["state B geo B"] = self.job.children["go_B"].results.get_energy(unit=unit)
energies["state A geo B"] = self.job.children["sp_A_for_geo_B"].results.get_energy(unit=unit)
energies["state B geo A"] = self.job.children["sp_B_for_geo_A"].results.get_energy(unit=unit)
return energies
class ReorganizationEnergyJob(MultiJob):
"""A class for calculating the reorganization energy using AMS.
Given two states, A and B, the reorganization energy is defined as follows:
reorganization energy =
E(state B at optimal geometry for state A) -
E(state A at optimal geometry for state A) +
E(state A at optimal geometry for state B) -
E(state B at optimal geometry for state B)
This job will run two geometry optimizations and two single point calculations.
"""
_result_type = ReorganizationEnergyResults
def __init__(self, molecule, common_settings, settings_state_A, settings_state_B, **kwargs):
"""
molecule: the molecule
common_settings: a setting object for an AMSJob containing the shared settings for all the calculations
settings_state_A: Setting object for an AMSJob containing exclusivelt the options defining the state A (e.g. charge and spin)
settings_state_B: Setting object for an AMSJob containing exclusivelt the options defining the state B (e.g. charge and spin)
kwargs: other options to be passed to the MultiJob constructor
"""
MultiJob.__init__(self, children=OrderedDict(), **kwargs)
go_settings = common_settings.copy()
go_settings.input.ams.task = "GeometryOptimization"
sp_settings = common_settings.copy()
sp_settings.input.ams.task = "SinglePoint"
# copy the settings so that we wont modify the ones provided as input by the user
settings_state_A = settings_state_A.copy()
settings_state_B = settings_state_B.copy()
# In case the charge key is not specified, excplicitely set the value to 0.
# This is to prevent the charge in molecule.properties.charge (set by get_main_molecule())
# to be used in case of neutral systems
for s in [settings_state_A, settings_state_B]:
if not "charge" in s.input.ams.system:
s.input.ams.system.charge = 0
self.children["go_A"] = AMSJob(molecule=molecule, settings=go_settings + settings_state_A, name="go_A")
self.children["go_B"] = AMSJob(molecule=molecule, settings=go_settings + settings_state_B, name="go_B")
self.children["sp_A_for_geo_B"] = AMSJob(settings=sp_settings + settings_state_A, name="sp_A_geo_B")
self.children["sp_B_for_geo_A"] = AMSJob(settings=sp_settings + settings_state_B, name="sp_B_geo_A")
pass_molecule(self.children["go_A"], self.children["sp_B_for_geo_A"])
pass_molecule(self.children["go_B"], self.children["sp_A_for_geo_B"])
To follow along, either
Download
ReorganizationEnergy.py(run as$AMSBIN/amspython ReorganizationEnergy.py).Download
ReorganizationEnergy.ipynb(see also: how to install Jupyterlab in AMS)
Worked Example¶
Initialization¶
#!/usr/bin/env amspython
from scm.plams import Molecule, Settings, ReorganizationEnergyJob, init, AMSJob
# this line is not required in AMS2025+
init()
PLAMS working folder: /path/plams/ReorganizationEnergy/plams_workdir.002
Define molecule¶
Normally you would read it from an xyz file, but here is for convenience explicit code
# molecule = Molecule("pyrrole.xyz")
def get_molecule(input_string):
job = AMSJob.from_input(input_string)
return job.molecule[""]
molecule = get_molecule(
"""
System
Atoms
C -1.12843000 0.00000000 -0.35463200
C -0.71293000 0.00000000 0.96463800
C 0.71293000 0.00000000 0.96463800
C 1.12843000 0.00000000 -0.35463200
N 0.00000000 0.00000000 -1.14563200
H 0.00000000 0.00000000 -2.15713200
H -2.12074000 0.00000000 -0.79100200
H -1.36515000 0.00000000 1.83237800
H 1.36515000 0.00000000 1.83237800
H 2.12074000 0.00000000 -0.79100200
End
End
"""
)
molecule.properties.name = "pyrrole" # normally the name of the xyz file
Setup and run job¶
# Generic settings of the calculation
# (for quantitatively better results, use better settings)
common_settings = Settings()
common_settings.input.adf.Basis.Type = "DZ"
# Specific settings for the neutral calculation.
# Nothing special needs to be done for the neutral calculation,
# so we just use an empty settings.
neutral_settings = Settings()
# Specific settings for the anion calculation:
anion_settings = Settings()
anion_settings.input.ams.System.Charge = -1
anion_settings.input.adf.Unrestricted = "Yes"
anion_settings.input.adf.SpinPolarization = 1
# Create and run the ReorganizationEnergyJob:
job = ReorganizationEnergyJob(
molecule, common_settings, neutral_settings, anion_settings, name=molecule.properties.name
)
job.run()
[05.03|13:59:19] JOB pyrrole STARTED
[05.03|13:59:19] JOB pyrrole RUNNING
[05.03|13:59:19] JOB pyrrole/go_A STARTED
[05.03|13:59:19] JOB pyrrole/go_A RUNNING
[05.03|13:59:23] JOB pyrrole/go_A FINISHED
[05.03|13:59:23] JOB pyrrole/go_A SUCCESSFUL
[05.03|13:59:23] JOB pyrrole/go_B STARTED
[05.03|13:59:23] JOB pyrrole/go_B RUNNING
[05.03|13:59:30] JOB pyrrole/go_B FINISHED
[05.03|13:59:30] JOB pyrrole/go_B SUCCESSFUL
... (PLAMS log lines truncated) ...
<scm.plams.recipes.reorganization_energy.ReorganizationEnergyResults at 0x7e91b3f3b0d0>
Fetch and print the results:¶
energy_unit = "eV"
energies = job.results.get_all_energies(energy_unit)
reorganization_energy = job.results.reorganization_energy(energy_unit)
print("")
print("== Results ==")
print("")
print(f"Molecule: {molecule.properties.name}")
print("State A: neutral")
print("State B: anion")
print("")
print(f"Reorganization energy: {reorganization_energy:.6f} [{energy_unit}]")
print("")
print(f"| State | Optim Geo | Energy [{energy_unit}]")
print(f'| A | A | {energies["state A geo A"]:.6f}')
print(f'| A | B | {energies["state A geo B"]:.6f}')
print(f'| B | A | {energies["state B geo A"]:.6f}')
print(f'| B | B | {energies["state B geo B"]:.6f}')
print("")
== Results ==
Molecule: pyrrole
State A: neutral
State B: anion
Reorganization energy: 0.473683 [eV]
| State | Optim Geo | Energy [eV]
| A | A | -63.801633
| A | B | -63.487503
| B | A | -61.702138
| B | B | -61.861691
Complete Python code¶
#!/usr/bin/env amspython
# coding: utf-8
# ## Initialization
#!/usr/bin/env amspython
from scm.plams import Molecule, Settings, ReorganizationEnergyJob, init, AMSJob
# this line is not required in AMS2025+
init()
# ## Define molecule
#
# Normally you would read it from an xyz file, but here is for convenience explicit code
# molecule = Molecule("pyrrole.xyz")
def get_molecule(input_string):
job = AMSJob.from_input(input_string)
return job.molecule[""]
molecule = get_molecule(
"""
System
Atoms
C -1.12843000 0.00000000 -0.35463200
C -0.71293000 0.00000000 0.96463800
C 0.71293000 0.00000000 0.96463800
C 1.12843000 0.00000000 -0.35463200
N 0.00000000 0.00000000 -1.14563200
H 0.00000000 0.00000000 -2.15713200
H -2.12074000 0.00000000 -0.79100200
H -1.36515000 0.00000000 1.83237800
H 1.36515000 0.00000000 1.83237800
H 2.12074000 0.00000000 -0.79100200
End
End
"""
)
molecule.properties.name = "pyrrole" # normally the name of the xyz file
# ## Setup and run job
# Generic settings of the calculation
# (for quantitatively better results, use better settings)
common_settings = Settings()
common_settings.input.adf.Basis.Type = "DZ"
# Specific settings for the neutral calculation.
# Nothing special needs to be done for the neutral calculation,
# so we just use an empty settings.
neutral_settings = Settings()
# Specific settings for the anion calculation:
anion_settings = Settings()
anion_settings.input.ams.System.Charge = -1
anion_settings.input.adf.Unrestricted = "Yes"
anion_settings.input.adf.SpinPolarization = 1
# Create and run the ReorganizationEnergyJob:
job = ReorganizationEnergyJob(
molecule, common_settings, neutral_settings, anion_settings, name=molecule.properties.name
)
job.run()
# ## Fetch and print the results:
energy_unit = "eV"
energies = job.results.get_all_energies(energy_unit)
reorganization_energy = job.results.reorganization_energy(energy_unit)
print("")
print("== Results ==")
print("")
print(f"Molecule: {molecule.properties.name}")
print("State A: neutral")
print("State B: anion")
print("")
print(f"Reorganization energy: {reorganization_energy:.6f} [{energy_unit}]")
print("")
print(f"| State | Optim Geo | Energy [{energy_unit}]")
print(f'| A | A | {energies["state A geo A"]:.6f}')
print(f'| A | B | {energies["state A geo B"]:.6f}')
print(f'| B | A | {energies["state B geo A"]:.6f}')
print(f'| B | B | {energies["state B geo B"]:.6f}')
print("")