Solution¶
Here is the solution to the second exercise regarding the polarizability tensor.
Define your worflow¶
As already mentioned, a workflow is mainly made of a queue of BigDFT jobs. This means you need to define a list of Job
instances, to be run sequentially afterwards.
We want to compute the polarizability tensor of the N\(_2\) molecule using the Workflow
class, just like in the previous exercise. The main input parameters being given, you only have to add Job
instances to the pt_queue
list. You can do so by re-using the same jobs as in the previous notebook (especially the same electric fields and run directories): first, a ground state job (without electric field) and then three calculations with an electric field, along the \(x\),
\(y\) and \(z\) axis respectively. Try to use a for loop to add the last three jobs to the queue.
Only the base classes of the MyBigDFT
package are required to do that, and might also require the help of extra packages such as numpy
(you should not need it yet, though).
[1]:
import os
from copy import deepcopy
from mybigdft import Workflow, InputParams, Posinp, Atom, Job
import numpy as np
# Main input variables:
# - use the optimized structure for the N2 molecule (found using the
# LDA exchange-correlation functional)
atoms = [Atom('N', [0, 0, 0]), Atom('N', [0, 0, 1.0935])]
pos = Posinp(atoms, units="angstroem", boundary_conditions="free")
# - use some non-default input parameters to get high quality results
inp = InputParams({"dft": {"rmult": [7, 9], "hgrids": 0.35}})
# Electric field amplitude to be applied in each direction
ef_amplitude = 1.e-4
# Initialize the queue of jobs in order to be able to compute the
# polarizability tensor:
# - Add the ground state job (without any electric field)
base_run_dir = os.path.relpath("N2/pol_tensor/")
gs = Job(inputparams=inp, posinp=pos, name="N2",
run_dir=base_run_dir)
pt_queue = [gs]
# - Add the jobs with an electric field
for i, coord in enumerate(["x", "y", "z"]):
# Add the electric field to the base input parameters
elecfield = [0.0] * 3
elecfield[i] = ef_amplitude
new_inp = deepcopy(inp)
new_inp["dft"]["elecfield"] = elecfield
# Define the correct run directory
new_run_dir = os.path.join(gs.run_dir,
"EF_along_{}+/".format(coord))
# Append the new job to the end of the queue
pt_queue.append(Job(inputparams=new_inp, posinp=pos,
name="N2", run_dir=new_run_dir))
pt_wf = Workflow(queue=pt_queue)
[2]:
# Let us ensure that you followed the instructions:
# - there are four jobs in the queue
assert len(pt_wf.queue) == 4
# - correct electric fields are applied
assert "elecfield" not in pt_wf.queue[0].inputparams["dft"]
assert pt_wf.queue[1].inputparams["dft"]["elecfield"] == [1.e-4, 0.0, 0.0]
assert pt_wf.queue[2].inputparams["dft"]["elecfield"] == [0.0, 1.e-4, 0.0]
assert pt_wf.queue[3].inputparams["dft"]["elecfield"] == [0.0, 0.0, 1.e-4]
# - correct run directories
assert pt_wf.queue[0].run_dir.endswith("N2/pol_tensor")
assert pt_wf.queue[1].run_dir.endswith("N2/pol_tensor/EF_along_x+")
assert pt_wf.queue[2].run_dir.endswith("N2/pol_tensor/EF_along_y+")
assert pt_wf.queue[3].run_dir.endswith("N2/pol_tensor/EF_along_z+")
Run the jobs of your workflow¶
Nothing important to code here, as you might only want to change the value of the attributes to suit your needs.
Note that you do not have to use the context manager: no need for a with
statement before using the run
method of a Workflow
instance as it is actually taken care of internally. This is one of the advantage of using a workflow.
[3]:
pt_wf.run(nmpi=6, nomp=3)
/Users/maximemoriniere/Documents/Python/MyBigDFT/doc/source/notebooks/N2/pol_tensor
Logfile log-N2.yaml already exists!
/Users/maximemoriniere/Documents/Python/MyBigDFT/doc/source/notebooks/N2/pol_tensor/EF_along_x+
Logfile log-N2.yaml already exists!
/Users/maximemoriniere/Documents/Python/MyBigDFT/doc/source/notebooks/N2/pol_tensor/EF_along_y+
Logfile log-N2.yaml already exists!
/Users/maximemoriniere/Documents/Python/MyBigDFT/doc/source/notebooks/N2/pol_tensor/EF_along_z+
Logfile log-N2.yaml already exists!
Note that, if you succeeded in performing the previous notebook, the calculations were not run again.
[4]:
# Let us ensure that you followed the instructions:
assert pt_wf.is_completed
Post-process your results¶
Now that you ran the workflow, you can use the results to extract meaningful quantities. Let us define a post_proc
function to do just that. Its only argument must be a workflow instance. You should be able to do the post-processing without having to add another argument to the post_proc
function. Try to loop over the three calculations with an electric field to get their dipoles.
[5]:
def post_proc(workflow):
"""
Use the logfile of the calculations run by the workflow to
extract the polarizability tensor.
Parameters
----------
workflow: Workflow
Workflow of run calculations allowing to compute the
polarizability tensor of a given system.
Returns
-------
numpy array
Polarizability tensor of the system.
"""
pol_tensor = np.zeros((3, 3))
delta_ef = 1.e-4
d0 = np.array(workflow.queue[0].logfile.dipole)
for i, job in enumerate(workflow.queue[1:4]):
d1 = np.array(job.logfile.dipole)
pol_tensor[:, i] = (d1 - d0) / delta_ef
return pol_tensor
# Run the post-processing of the workflow to compute
pol_tensor = post_proc(pt_wf)
print(pol_tensor)
[[ 1.075753e+01 5.400000e-04 -9.900000e-04]
[ 5.400000e-04 1.075753e+01 -9.900000e-04]
[-1.740000e-03 -1.740000e-03 1.525630e+01]]
[6]:
# Let us ensure that you obtained the expected result:
expected_pol_tensor = [[ 1.075753e+01, 5.400000e-04, -9.900000e-04],
[ 5.400000e-04, 1.075753e+01, -9.900000e-04],
[-1.740000e-03, -1.740000e-03, 1.525630e+01]]
assert np.allclose(pol_tensor, expected_pol_tensor)