Refactoring TDDFT and LCAOTDDFT
This issue is for planning a refactorization of TDDFT and LCAOTDDFT classes.
Goals
- Merge TDDFT and LCAOTDDFT classes to a single RTTDDFT class
- Reduce code duplication and ease maintenance and addition of new features
- Remove troublesome inheritance from GPAW class
- Enable initialization of a dummy RTTDDFT object without a gpw file (for testing etc)
- Simplify interface: Separate constructors for starting and restarting a calculation
- Not possible to change "permanent" settings (propagator etc) during restart -> simpler and more robust code
- Same for observers
- Make observer interaction more explicit and versatile
RTTDDFT class
- All code that differentiates fd/LCAO mode is hidden in propagator implementations etc
class RTTDDFT:
def __init__(self, wfs, hamiltonian, propagator, ...):
# This object can also be initialized with some dummy wfs etc
...
# No extra tddft_init() or initialize()
@classmethod
def from_dft_file(cls, filepath, *,
propagator=None,
td_potential=None,
...,
parallel=None,
communicator=None,
txt='-'):
# This function is mostly like the present __init__()
...
return cls(wfs, ...)
@classmethod
def from_tddft_file(cls, filepath, *,
parallel=None,
communicator=None,
txt='-'):
# Read also propagator, td_potential, etc
...
return cls(wfs, ...)
def write(self, filepath):
...
def attach(self, observer):
...
observer.update_after_attached(self)
def absorption_kick(self, kick_strength):
"""Kick with a weak electric field.
Parameters
----------
kick_strength
Strength of the kick in atomic units
"""
ext = ...
self.kick(ext)
def kick(self, ext):
"""Kick with any external potential.
Parameters
----------
ext
External potential
"""
self._update_observers_before_kick()
...
self._update_observers_after_kick()
def propagate(self, time_step: float = 10.0, iterations: int = 2000):
"""Propagate the electronic system.
Parameters
----------
time_step
Time step in attoseconds
iterations
Number of propagation steps
"""
...
while self.niter < self.maxiter:
self._update_observers_before_propagation_step()
...
self._update_observers_after_propagation_step()
Observers
Example writer-observer (many observers share the same structure -> PlainTextWriter):
class MagneticMomentWriter(PlainTextWriter):
def __init__(self, filepath, mode, *,
origin,
...):
...
@classmethod
def new_file(cls, filepath: str, *,
origin='COM',
...):
# Write a header defining origin and other "permanent"
# settings to the file
...
cls(filepath, mode='w', ...)
@classmethod
def existing_file(cls, filepath: str):
# Read origin and other "permanent" settings from file
...
cls(filepath, mode='a', ...)
def update_after_attached(self, td_calc):
# Instead of a single update() function, allow more versatile hooks
# that the observer may implement if it so wishes
...
def update_before_kick(self, td_calc):
...
def update_after_kick(self, td_calc):
...
def update_before_propagation_step(self, td_calc):
...
def update_after_propagation_step(self, td_calc):
...
Input scripts
For users, the code could look something like this:
- Starting a RTTDDFT calculation from ground state gpw file:
- User can provide propagator, td_potential, etc "permanent" settings
- Explicit notation for attaching observers and for the new files they create
from gpaw.rttddft import RTTDDFT, DipoleMomentWriter, MagneticMomentWriter
td_calc = RTTDDFT.from_dft_file('gs.gpw', propagator=..., td_potential=...,
parallel=..., txt=...)
td_calc.attach(DipoleMomentWriter.new_file('dm.dat'))
td_calc.attach(MagneticMomentWriter.new_file('mm.dat', origin=...))
td_calc.absorption_kick([1e-5, 0, 0])
td_calc.propagate(10, 3000)
td_calc.write('td.gpw')
- Restarting a RTTDDFT calculation from a checkpoint file:
- User can only provide parallel, txt, etc runtime settings; "permanent" settings cannot be changed
- Explicit notation for observers using the existing files
from gpaw.rttddft import RTTDDFT, DipoleMomentWriter, MagneticMomentWriter
td_calc = RTTDDFT.from_tddft_file('td.gpw', parallel=..., txt=...)
td_calc.attach(DipoleMomentWriter.existing_file('dm.dat'))
td_calc.attach(MagneticMomentWriter.existing_file('mm.dat'))
td_calc.attach(DipoleMomentWriter.new_file('dm2.dat')) # New observers can be added any time if the user so wishes
td_calc.propagate(10, 3000)
td_calc.write('td.gpw')
Issues
- Not all the features are supported by both fd and LCAO modes at the moment -> some assertions of the mode needed
Feedback and alternative suggestions are welcome.