How to create AddOns for QuantumATK¶
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:
1 2 3 4 5 6 7 8 9 10 11 | import datetime
import XYZLabFloor
__addon_description__ = "Plugins for importing and exporting XYZ configurations."
__addon_version__ = "1.0"
__addon_date__ = datetime.date(year=2015, month=8, day=6)
__plugins__ = [XYZLabFloor.XYZLabFloor]
def reloadPlugins():
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:
1 2 3 4 5 6 7 8 9 10 | import os
from API import LabFloorImporterPlugin
from API import LabFloorItem
from NL.CommonConcepts.Configurations.MoleculeConfiguration import MoleculeConfiguration
from NL.CommonConcepts.PeriodicTable import SYMBOL_TO_ELEMENT
from NL.CommonConcepts.PhysicalQuantity import Angstrom
from NL.ComputerScienceUtilities import Exceptions
from 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
:
13 | class 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 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | def scan(self, filename):
"""
Scans a file to check if it is supported by the plugin
@param filename : The path to be scanned.
@type : string
@return A list of LabFloorItems
"""
# Setup a resulting vector.
result = []
# Determine extension
basename = os.path.basename(filename)
no_extension_name, extension = os.path.splitext(basename)
# Return empty string if extension isn't ".xyz"
if extension != '.xyz':
return result
# Try to load configuration
try:
reader = XYZFileRawReader(filename)
except Exception:
return result
for molecule_idx in xrange(reader.numOfMolecules()):
# Read the comment for this molecule.
comment = reader.comment(molecule=molecule_idx)
# Create and add LabFloorItem to list
if reader.numOfMolecules() == 1:
title = no_extension_name
else:
title = no_extension_name + " (" + str(molecule_idx) + ")"
# Create labfloor item.
item = LabFloorItem(MoleculeConfiguration,
title=title,
tool_tip=comment,
molecule_idx=molecule_idx)
# Add to result list.
result.append(item)
# Return the result list.
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | def load(self, filename, molecule_idx=0):
"""
Load the desired object in memory.
@param filename : The path of the XYZ-file.
@type : string
@return Desired object (MoleculeConfiguration)
"""
# Read the file
reader = XYZFileRawReader(filename)
# Lists of elements and positions.
elements = []
positions = []
# Loop over atoms.
for atom in reader.atomList(molecule=molecule_idx):
elements.append(SYMBOL_TO_ELEMENT[atom["element"]])
positions.append(atom["coords"]*Angstrom)
# Create configuration.
configuration = MoleculeConfiguration(
elements=elements,
cartesian_coordinates=positions)
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:
1 2 3 4 5 6 7 8 9 10 11 | import datetime
import XYZExporterPlugin
__addon_description__ = "Plugins for exporting XYZ files."
__addon_version__ = "1.0"
__addon_date__ = datetime.date(year=2015, month=8, day=6)
__plugins__ = [XYZExporterPlugin.XYZExporterPlugin]
def reloadPlugins():
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | from NL.CommonConcepts.Configurations.MoleculeConfiguration import MoleculeConfiguration
from NL.CommonConcepts.PhysicalQuantity import Angstrom
from API import ExportConfigurationPlugin, showMessage
class XYZExporterPlugin(ExportConfigurationPlugin):
""" Class for handling the export of the XYZ input files. """
def title(self):
""" Return the title file selection dialog. """
return 'XYZ'
def extension(self):
""" The default extension of XYZ. """
return 'xyz'
def export(self, configuration, path):
"""
Export the configuration.
@param configuration : The configuration to export.
@param path : The path to save the configuration to.
@return None
"""
# XYZ files only supports molecules.
if not isinstance(configuration, MoleculeConfiguration):
showMessage('XYZExporter can only export MoleculeConfigurations')
return
# Open the file with write permission.
with open(path, 'w') as f:
# Get the total number of atoms.
number_of_atoms = len(configuration)
# Write out the header to the file.
f.write('%i\n' % number_of_atoms)
f.write('Generated by XYZExporter\n')
# Get the list of atomic symbols.
symbols = [ element.symbol() for element in configuration.elements() ]
# Get the cartesian coordinates in units of Angstrom.
coordinates = configuration.cartesianCoordinates().inUnitsOf(Angstrom)
# Loop over each atom and write out its symbol and coordinates.
for i in xrange(number_of_atoms):
x, y, z = coordinates[i]
f.write('%3s %16.8f %16.8f %16.8f\n' % (symbols[i], x, y, z))
def canExport(self, configuration):
"""
Method to determine if an exporter class can export a given configuration.
@param configuration : The configuration to test.
@return A bool, True if the plugin can export, False if it cannot.
"""
supported_configurations = [MoleculeConfiguration]
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import numpy
# Define a orthogonal cell.
cell = numpy.array( [ [ 5.0, 0.0, 0.0 ],
[ 0.0, 5.0, 0.0 ],
[ 0.0, 0.0, 5.0 ] ] )
# Create a 3D grid of points from 0 to 5 in x, y, and z.
x = numpy.linspace(0.0, 5.0)
y = numpy.linspace(0.0, 5.0)
z = numpy.linspace(0.0, 5.0)
xx, yy, zz = numpy.meshgrid(x, y, z, indexing='ij')
# Define an electron density as a Gaussian centered at (2.5, 2.5, 2.5) times a
# sine wave in the x direction.
density = numpy.exp(-(xx-2.5)**2 - (yy-2.5)**2 - (zz-2.5)**2) * numpy.sin(yy-2.5)
# Save the cell and density to a .npz file.
numpy.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:
1 2 3 4 5 6 7 8 9 10 11 | import datetime
import NPZLabFloor
__addon_description__ = "Plugin for reading a NPZ formatted electron density."
__addon_version__ = "1.0"
__addon_date__ = datetime.date(2014, 9, 1)
__plugins__ = [NPZLabFloor.NPZLabFloor]
def reloadPlugins():
reload(NPZLabFloor)
|
There is nothing new here. Now we need to define the actual plugin class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | import numpy
import os
import NLEngine
from API import LabFloorImporterPlugin
from API import LabFloorItem
from NL.Analysis.ElectronDensity import ElectronDensity
from NL.Analysis.GridValues import GridValues
from NL.ComputerScienceUtilities.NLFlag import Spin
from NL.CommonConcepts.PhysicalQuantity import Angstrom
class NPZLabFloor(LabFloorImporterPlugin):
"""
Class for handling the importing of NPZ-files as LabFloor items.
"""
def scan(self, filename):
"""
Scans a file to check if it is supported by the plugin
@param filename : The path to be scanned.
@type : string
@return A list of LabFloorItems
"""
# Setup a vector for the LabFloorItems that will be returned.
lab_floor_items = []
# Determine extension
basename = os.path.basename(filename)
no_extension_name, extension = os.path.splitext(basename)
# Return an empty list if the extension isn't ".npz"
if extension != '.npz':
return []
item = LabFloorItem(ElectronDensity,
title='NPZ Electron Density',
tool_tip='NPZ Electron Density')
# Add to the list of items.
lab_floor_items.append(item)
# Return the list of items.
return lab_floor_items
def load(self, filename):
"""
Load the desired object in memory.
@param filename : The path of the NPZ-file.
@type : string
@return Desired object (MoleculeConfiguration)
"""
# Read the file
npz = numpy.load(filename)
# Create an "empty" ElectronDensity object.
electron_density = ElectronDensity.__new__(ElectronDensity)
# We will now fill out a dictionary that contains the information
# needed by the ElectronDensity class.
data = {}
# The "data" key is the electron density. The units must be given in the
# "data_unit" key. The array should have have the x-axis as the first
# dimension, the y-axis as the second, and the z-axis as the third.
data['data'] = npz['density']
# The data in "data" has no units so they are assigned here.
data['data_unit'] = 1.0 * Angstrom**-3
# Set the origin to be at zero.
data['origo'] = numpy.zeros(3)
# The cell must be given in Bohr units.
data['cell'] = npz['cell']
# The boundary conditions are expressed as a list of 6 numbers that should
# map to:
# { Dirichlet, Neumann, Periodic, Multipole };
# A value of 2 corresponds to "Periodic".
data['boundary_conditions'] = [2, 2, 2, 2, 2, 2]
# Construct the GridValues specific part of the object.
GridValues._populateFromDict(electron_density, data)
# Set the spin_type to unpolarized.
spin_type = NLEngine.UNPOLARIZED
electron_density._AnalysisSpin__spin_type = spin_type
sp = Spin.All
electron_density._AnalysisSpin__spin = sp
electron_density._setSupportedSpins(sp)
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.
