How to create AddOns for QuantumATK¶
Version: 2015 An AddOn is a Python module that contains one or more plugins for QuantumATK. They are used to add new functionality to the software. There are several types of plugins available. This tutorial will focus on the types of plugins that allows QuantumATK to read and write new data formats. This tutorial contains three examples. The first is a plugin to read molecular configurations from XYZ files. The second is a plugin to read electron densities. |
Basic Structure of a AddOn Module¶
Because plugins are contained in Python modules, they should exist in their own directory. As an example we will take a look at an plugin that reads XYZ files. Its directory structure looks like:
XYZFilters/
__init__.py
XYZLabFloor.py
XYZFileRawReader.py
The __init__.py
file is a special file that tells Python that this folder is
a module. It also contains code that is executed when the module is imported.
For a plugin, this file also needs to contain some information to inform QuantumATK
about itself.
Example 1: A Plugin to Read XYZ Files¶
The __init__.py
file in the XYZFilters
AddOn contains the following code:
1import datetime
2import XYZLabFloor
3
4__addon_description__ = "Plugins for importing and exporting XYZ configurations."
5__addon_version__ = "1.0"
6__addon_date__ = datetime.date(year=2015, month=8, day=6)
7
8__plugins__ = [XYZLabFloor.XYZLabFloor]
9
10def reloadPlugins():
11 reload(XYZLabFloor)
This code gives a description of the AddOn, assigns a version number, and a
date that the AddOn was last updated. It also defines a list of plugins that
this AddOns provides. In this case, there is a XYZLabFloor.py
file that
contains a plugin class named XYZLabFloor
. Additionally there is a function
named reloadPlugins
that is provided so that QuantumATK may reload the plugin if the
source code files change.
We will now look at the structure of the XYZLabFloor.py
file that contains
the plugin class. At the top of the file we import the modules and classes we
need to write the plugin:
1import os
2
3from API import LabFloorImporterPlugin
4from API import LabFloorItem
5from NL.CommonConcepts.Configurations.MoleculeConfiguration import MoleculeConfiguration
6from NL.CommonConcepts.PeriodicTable import SYMBOL_TO_ELEMENT
7from NL.CommonConcepts.PhysicalQuantity import Angstrom
8from NL.ComputerScienceUtilities import Exceptions
9
10from XYZFileRawReader import XYZFileRawReader
Next we need to define the plugin class. Plugins must inherit from a particular
class defined in QuantumATK. In this case, we will define a class that inherits from
LabFloorImporterPlugin
:
13class XYZLabFloor(LabFloorImporterPlugin):
This type of plugin must define two methods. The first method is scan
. The
role of this method is to determine if a particular file is handled by this
plugin and if so what type of LabFloor object(s) the file contains. It will
return a list of items to the LabFloor. The second method is load
, which is
responsible for parsing the file and loading the object into memory.
For our XYZLabFloor
plugin the scan method is defined as:
18 def scan(self, filename):
19 """
20 Scans a file to check if it is supported by the plugin
21
22 @param filename : The path to be scanned.
23 @type : string
24
25 @return A list of LabFloorItems
26 """
27 # Setup a resulting vector.
28 result = []
29
30 # Determine extension
31 basename = os.path.basename(filename)
32 no_extension_name, extension = os.path.splitext(basename)
33
34 # Return empty string if extension isn't ".xyz"
35 if extension != '.xyz':
36 return result
37
38 # Try to load configuration
39 try:
40 reader = XYZFileRawReader(filename)
41 except Exception:
42 return result
43
44 for molecule_idx in xrange(reader.numOfMolecules()):
45
46 # Read the comment for this molecule.
47 comment = reader.comment(molecule=molecule_idx)
48
49 # Create and add LabFloorItem to list
50 if reader.numOfMolecules() == 1:
51 title = no_extension_name
52 else:
53 title = no_extension_name + " (" + str(molecule_idx) + ")"
54
55 # Create labfloor item.
56 item = LabFloorItem(MoleculeConfiguration,
57 title=title,
58 tool_tip=comment,
59 molecule_idx=molecule_idx)
60
61 # Add to result list.
62 result.append(item)
63
64 # Return the result list.
65 return result
This code detects if the file is a valid XYZ file by first testing if the
filename has an “xyz” extension and then trying to actually parse the file. If
the file is not a valid XYZ file, then an empty list is returned. If it is a
valid file, then each of the molecules contained in the file are read in and a
LabFloorItem
is created for each molecule.
A LabFloorItem
is a class that represents the item on the LabFloor. It
contains the type of object, in this case it is a MoleculeConfiguration
as
well as a title and tool tip (text that is visible when the mouse cursor hovers
on the item). Extra information about the item can also be passed as keyword
argument. As we will see next, these arguments get passed to the load method.
The load method is called by QuantumATK when the user interacts with the item. For example, this occurs when visualizing the structure with the viewer or importing it to the builder. The load method is defined as:
67 def load(self, filename, molecule_idx=0):
68 """
69 Load the desired object in memory.
70
71 @param filename : The path of the XYZ-file.
72 @type : string
73
74 @return Desired object (MoleculeConfiguration)
75 """
76 # Read the file
77 reader = XYZFileRawReader(filename)
78
79 # Lists of elements and positions.
80 elements = []
81 positions = []
82
83 # Loop over atoms.
84 for atom in reader.atomList(molecule=molecule_idx):
85 elements.append(SYMBOL_TO_ELEMENT[atom["element"]])
86 positions.append(atom["coords"]*Angstrom)
87
88 # Create configuration.
89 configuration = MoleculeConfiguration(
90 elements=elements,
91 cartesian_coordinates=positions)
92
93 return configuration
The method reads in the file contents and extracts the elements and positions
of the requested molecule. The method is passed the index of the molecule to
read (this was stored in the LabFloorItem
that was created in the scan
method) and creates a MoleculeConfiguration
. The MoleculeConfiguration
class is how QuantumATK represents molecules. For periodic systems there is a
corresponding BulkConfiguration
class that stores the lattice vectors along
with the coordinates and elements.
The full source code
for this AddOn can be
downloaded.
Example 2: Plugin to export configurations¶
In this example, we will write a plugin to allow QuantumATK to export configurations to XYZ files. The directory structure for the module will look like:
XYZExporter/
__init__.py
XYZExporter.py
The first step is to write the __init__.py
file:
1import datetime
2import XYZExporterPlugin
3
4__addon_description__ = "Plugins for exporting XYZ files."
5__addon_version__ = "1.0"
6__addon_date__ = datetime.date(year=2015, month=8, day=6)
7
8__plugins__ = [XYZExporterPlugin.XYZExporterPlugin]
9
10def reloadPlugins():
11 reload(XYZExporterPlugin)
The next step will be to write the actual plugin class. In the previous
example, the plugin class was derived from the LabFloorImporterPlugin
class
to indicate that it is a plugin for importing data. This plugin, however, will
derive from ExportConfigurationPlugin
. These plugins are used to extend the
number of export formats supported by the builder. With the builder open, you
can choose File->Export to see a list of the file formats that are supported.
Classes that inherit from ExportConfigurationPlugin
must implement four
methods: title
, extension
, canExport
, and export
. The title
method needs to return the name of this type of file (“XYZ”). The extension
method should return the file extension (“xyz”). The canExport
method
determines if the type of configuration (MoleculeConfiguration
,
BulkConfiguration
, DeviceConfiguration
, or NudgedElasticBand
) are
supported by this plugin (XYZ files only support MoleculeConfiguration
objects). Finally, the export
method is where the configuration is passed
in and written out to disk.
Let’s look at the code for our XYZExporterPlugin
in the
XYZExporterPlugin.py
file:
1from NL.CommonConcepts.Configurations.MoleculeConfiguration import MoleculeConfiguration
2from NL.CommonConcepts.PhysicalQuantity import Angstrom
3
4from API import ExportConfigurationPlugin, showMessage
5
6class XYZExporterPlugin(ExportConfigurationPlugin):
7 """ Class for handling the export of the XYZ input files. """
8
9 def title(self):
10 """ Return the title file selection dialog. """
11
12 return 'XYZ'
13
14 def extension(self):
15 """ The default extension of XYZ. """
16
17 return 'xyz'
18
19 def export(self, configuration, path):
20 """
21 Export the configuration.
22
23 @param configuration : The configuration to export.
24 @param path : The path to save the configuration to.
25
26 @return None
27 """
28
29 # XYZ files only supports molecules.
30 if not isinstance(configuration, MoleculeConfiguration):
31 showMessage('XYZExporter can only export MoleculeConfigurations')
32 return
33
34 # Open the file with write permission.
35 with open(path, 'w') as f:
36
37 # Get the total number of atoms.
38 number_of_atoms = len(configuration)
39
40 # Write out the header to the file.
41 f.write('%i\n' % number_of_atoms)
42 f.write('Generated by XYZExporter\n')
43
44 # Get the list of atomic symbols.
45 symbols = [ element.symbol() for element in configuration.elements() ]
46 # Get the cartesian coordinates in units of Angstrom.
47 coordinates = configuration.cartesianCoordinates().inUnitsOf(Angstrom)
48
49 # Loop over each atom and write out its symbol and coordinates.
50 for i in xrange(number_of_atoms):
51 x, y, z = coordinates[i]
52 f.write('%3s %16.8f %16.8f %16.8f\n' % (symbols[i], x, y, z))
53
54
55 def canExport(self, configuration):
56 """
57 Method to determine if an exporter class can export a given configuration.
58
59 @param configuration : The configuration to test.
60
61 @return A bool, True if the plugin can export, False if it cannot.
62 """
63
64 supported_configurations = [MoleculeConfiguration]
65
66 return isinstance(configuration, supported_configurations)
The full source code
for this AddOn can be downloaded.
Example 3: Plugin to read electron densities¶
For this example we will make a new plugin that reads electron density data.
For this tutorial we will define a new format for 3D grid data that is
stored in a NumPy’s binary .npz
files.
Construct a density¶
This code will define a electron density and save it to the file
electron_density.npz
. This will be the density file
that our plugin will read.
1import numpy
2
3# Define a orthogonal cell.
4cell = numpy.array( [ [ 5.0, 0.0, 0.0 ],
5 [ 0.0, 5.0, 0.0 ],
6 [ 0.0, 0.0, 5.0 ] ] )
7# Create a 3D grid of points from 0 to 5 in x, y, and z.
8x = numpy.linspace(0.0, 5.0)
9y = numpy.linspace(0.0, 5.0)
10z = numpy.linspace(0.0, 5.0)
11xx, yy, zz = numpy.meshgrid(x, y, z, indexing='ij')
12
13# Define an electron density as a Gaussian centered at (2.5, 2.5, 2.5) times a
14# sine wave in the x direction.
15density = numpy.exp(-(xx-2.5)**2 - (yy-2.5)**2 - (zz-2.5)**2) * numpy.sin(yy-2.5)
16
17# Save the cell and density to a .npz file.
18numpy.savez('electron_density.npz', cell=cell, density=density)
This electron density is not physical since it will be negative in some places,
but it will allow us to easily double check that the data is read in correctly.
The source code
to this script can
downloaded.
Write the NPZFilters AddOn¶
The directory structure for this AddOn will look like:
NPZFilters/
__init__.py
NPZLabFloor.py
Like the previous example, we will first focus on the __init__.py
file:
1import datetime
2import NPZLabFloor
3
4__addon_description__ = "Plugin for reading a NPZ formatted electron density."
5__addon_version__ = "1.0"
6__addon_date__ = datetime.date(2014, 9, 1)
7
8__plugins__ = [NPZLabFloor.NPZLabFloor]
9
10def reloadPlugins():
11 reload(NPZLabFloor)
There is nothing new here. Now we need to define the actual plugin class:
1import numpy
2import os
3
4import NLEngine
5
6from API import LabFloorImporterPlugin
7from API import LabFloorItem
8from NL.Analysis.ElectronDensity import ElectronDensity
9from NL.Analysis.GridValues import GridValues
10from NL.ComputerScienceUtilities.NLFlag import Spin
11from NL.CommonConcepts.PhysicalQuantity import Angstrom
12
13class NPZLabFloor(LabFloorImporterPlugin):
14 """
15 Class for handling the importing of NPZ-files as LabFloor items.
16 """
17
18 def scan(self, filename):
19 """
20 Scans a file to check if it is supported by the plugin
21
22 @param filename : The path to be scanned.
23 @type : string
24
25 @return A list of LabFloorItems
26 """
27 # Setup a vector for the LabFloorItems that will be returned.
28 lab_floor_items = []
29
30 # Determine extension
31 basename = os.path.basename(filename)
32 no_extension_name, extension = os.path.splitext(basename)
33
34 # Return an empty list if the extension isn't ".npz"
35 if extension != '.npz':
36 return []
37
38 item = LabFloorItem(ElectronDensity,
39 title='NPZ Electron Density',
40 tool_tip='NPZ Electron Density')
41
42 # Add to the list of items.
43 lab_floor_items.append(item)
44
45 # Return the list of items.
46 return lab_floor_items
47
48 def load(self, filename):
49 """
50 Load the desired object in memory.
51
52 @param filename : The path of the NPZ-file.
53 @type : string
54
55 @return Desired object (MoleculeConfiguration)
56 """
57
58 # Read the file
59 npz = numpy.load(filename)
60
61 # Create an "empty" ElectronDensity object.
62 electron_density = ElectronDensity.__new__(ElectronDensity)
63
64 # We will now fill out a dictionary that contains the information
65 # needed by the ElectronDensity class.
66 data = {}
67
68 # The "data" key is the electron density. The units must be given in the
69 # "data_unit" key. The array should have have the x-axis as the first
70 # dimension, the y-axis as the second, and the z-axis as the third.
71 data['data'] = npz['density']
72
73 # The data in "data" has no units so they are assigned here.
74 data['data_unit'] = 1.0 * Angstrom**-3
75
76 # Set the origin to be at zero.
77 data['origo'] = numpy.zeros(3)
78
79 # The cell must be given in Bohr units.
80 data['cell'] = npz['cell']
81
82 # The boundary conditions are expressed as a list of 6 numbers that should
83 # map to:
84 # { Dirichlet, Neumann, Periodic, Multipole };
85 # A value of 2 corresponds to "Periodic".
86 data['boundary_conditions'] = [2, 2, 2, 2, 2, 2]
87
88 # Construct the GridValues specific part of the object.
89 GridValues._populateFromDict(electron_density, data)
90
91 # Set the spin_type to unpolarized.
92 spin_type = NLEngine.UNPOLARIZED
93 electron_density._AnalysisSpin__spin_type = spin_type
94
95 sp = Spin.All
96 electron_density._AnalysisSpin__spin = sp
97 electron_density._setSupportedSpins(sp)
98
99 return electron_density
The full source code
for this AddOn can be downloaded.
How to install AddOns¶
There are two different ways to install AddOns. The first way is to set the
environment variable QUANTUM_ADDONS_PATH
to a directory where the AddOn
modules are located. For example if the path to the NPZFilters AddOn from the
previous section is $HOME/AddOns/NPZFilters
then setting the environment
variable QUANTUM_ADDONS_PATH=$HOME/AddOns
, would be correct.
Another way is to zip the Python module and install it through the graphical
interface. The first step is to create a zip file containing the module.
Following the NPZFilter example in the last paragraph, this can be done by
running zip -r NPZFilters.zip $HOME/AddOns/NPZFilters
. Then, in QuantumATK, under
the Help menu is an item named AddOn Manager. Opening the AddOn manager
will present a window that should look like the following:
By clicking Local Install, you will be presented with a file dialog. After
selecting NPZFilters.zip
QuantumATK will install the AddOn to the
QUANTUM_ADDONS_PATH
.
Test the NPZFilters AddOn¶
After following the steps in the previous section, the NPZFilters AddOn should
now be installed. You can double check this by pulling up the AddOn Manager
and seeing that NPZFilters is listed. If we create a new project in QuantumATK and
the folder contains the electron_density.npz
file we created then an
ElectronDensity object should show up on the lab floor.
Click on the Viewer… button in the panel on the right to visualize the electron density. This will present a dialog to choose between an isosurface and a cut plane. Choose isosurface. The default isosurface value is the average charge density, which is zero for our charge density (since half is negative and half is positive). Click on the Properties… button in the panel on the right and drag the Isovalue… slider to a value near 1.
The resulting isosurface should now look like dumbbell, with the two different colors representing the areas of negative and positive density and a plane of zero density through the x-z axis. This confirms that we correctly read in our model density function.