For this example we’ll build a plugin for the component shown in the figure Conceptual View of a Simple Component (from the User Guide). This component simply computes the value of its single output by adding its two inputs.
Our first step is to create our class. We want to inherit from openmdao.main.api.Component because that provides us with the interface we need to function properly as an OpenMDAO component.
from openmdao.lib.datatypes.api import Float
from openmdao.main.api import Component
class SimpleAdder(Component):
a = Float(0.0, iotype='in')
b = Float(0.0, iotype='in')
c = Float(0.0, iotype='out')
def execute(self):
self.c = self.a + self.b
The code defines the class SimpleAdder, which inherits from the Component class defined in openmdao.main.api, so we have to import it from there. The function in our component that performs a computation is called execute(), and there we define c as the sum of a and b. The self object that is passed as an argument to execute() represents an instance of our SimpleAdder class.
SimpleAdder has three variables of type Float with the names a, b, and c. All three variables have a default value of 0.0. Attributes a and b are inputs, so we specify that they have an iotype of 'in'. Attribute c is an output, so it has an iotype of 'out'.
The Float variable is defined in the package openmdao.lib.datatypes.api, so we have to import it from there before we can use it. This package defines a wide variety of traits, including basic types like Int, Str, and Bool; containers like List and Dict; and others. Variables are actually implemented using Enthought’s Traits and to learn more about traits, see the Traits User Manual.
At this point, our SimpleAdder plugin is usable within OpenMDAO. We could simply import the module containing it and use it in a model; but we want more than that. By packaging our plugin as a Python distribution, we can make it easy to share with others in the OpenMDAO community. We can give our distribution a version identifier and other metadata that will allow the framework to discover our plugin and show users that it’s available.
Creating a distribution out of a Python module is straightforward, but it does require the creation of a simple directory structure because distributions are intended to contain Python packages and not just individual modules.
For example, if our SimpleAdder class is in a file called simple_adder.py, we need a directory structure that looks like this to make it distributable as a package in a distribution:
simple_adder
|
|-- simple_adder
| |
| |-- simple_adder.py
| `-- __init__.py
|
`-- setup.py
The __init__.py file is empty and is there only because that is how Python determines that the directory simple_adder is a Python package. The only other file in the directory structure besides simple_adder.py is the setup.py file, which describes how to build a distribution containing our module. In this case, the setup.py file looks like this:
from setuptools import setup, find_packages
setup(
name='simple_adder',
version='1.0',
packages=find_packages(),
install_requires=['openmdao.lib', 'Traits>=3.1.0'],
entry_points={
'openmdao.component': ['SimpleAdder = simple_adder:SimpleAdder']
}
)
The setup() command has many arguments in addition to those shown above, e.g., author, author_email, maintainer, maintainer_email, url, license, description, long_description, keywords, platforms, fullname, contact, contact_email, classifiers, and download_url. If you supply any of these, their values will be stored as metadata in the distribution. To keep things simple, we won’t describe all of the arguments in detail. If you’re interested, you can go to this reference page for a description of the arguments to setup() or go here for the keyword arguments added or changed by setuptools.
The following options are required for our distribution to function properly within the OpenMDAO framework:
Entry points can be used by OpenMDAO to determine which plugins are available within a distribution. Entry points are divided into groups, and each type of OpenMDAO plugin has a particular group. For example, Component plugins are found in the openmdao.component group. Each entry point is specified by its name, followed by an equals (=) sign, followed by dotted module path (dotted path you would use to import the module in Python), followed by a colon (:) and the name of the plugin class. The value of entry_points should be a string in INI file format or a dictionary.
For example:
"""
[openmdao.component]
SimpleAdder = simple_adder:SimpleAdder
[openmdao.driver]
MyDriver = mydriver:MyDriver
"""
or
{ 'openmdao.component': ['SimpleAdder = simple_adder:SimpleAdder'],
'openmdao.driver': ['MyDriver = mydriver:MyDriver']
}
With the simple_adder directory structure shown above and the setup.py file shown, we can now build our distribution. From the simple_adder directory, typing python setup.py sdist -d . will create the distribution in our current directory. The version of the distribution and the Python version will be included in the filename. For example, since the version we specified in our setup.py file was '1.0', our distribution will be named simple_adder-1.0.tar.gz.
A tool called mod2dist exists for those of us who don’t want to create a package directory structure and a setup.py file manually. It has a number of options that you can see if you run mod2dist -h. The only required options are the desired version of the distribution and the module to use to generate the distribution. For example, the command
mod2dist -v 1.0 simple_adder.py
will generate the same distribution that we built manually earlier in this example.
Sometimes it’s necessary to create a new type of variable that can be passed between OpenMDAO components. This section describes how to do this using a pure Python OpenMDAO plugin.
Let’s assume we want to have a variable that represents a set of Cartesian coordinates, with the value of the variable being a 3-tuple of floating point values representing the x, y, and z position. We’ll start by creating a file called coord.py and placing the following code in it:
from openmdao.lib.datatypes.api import TraitType
class Coordinates(TraitType):
def __init__(self, default_value = (0.,0.,0.), **metadata):
super(Coordinates, self).__init__(default_value=default_value,
**metadata)
def validate(self, object, name, value):
if isinstance(value, tuple) and len(value) == 3 and \
all([isinstance(val,float) or isinstance(val,int) for val in value]):
return value
else:
self._logger.error(object, name, value)
OpenMDAO uses the Traits package from Enthought to implement variables. The base class for custom traits is TraitType, so that’s the base class for our coordinates variable. If a component or a component class contains a TraitType object and that object has a metadata attribute called iotype, then that object is exposed to the framework as a variable whose value can be passed between components. One thing that can be a little confusing to people first using Traits is that the Trait object itself is just a validator and possibly a converter. The object that actually gets passed around between components is the value that the trait corresponds to and not the trait itself. For example, if we had a component named wheel that contained one of our Coordinates traits named center_location, then the value of wheel.center_location would be a 3-tuple, not a Coordinates object.
We override the base class constructor so we can supply a default value of (0.,0.,0.) if the caller doesn’t supply one. After that, the only function we need to supply is the validate function, which will be called with the following arguments:
Our validate function should test that the value we’ve been called with is valid. In this particular case, we just need to verify that the value is a 3-tuple and it has float or int entries. If the value is acceptable, then we just return it. We don’t need to do it in this case, but in other custom traits, we could convert the value before returning it. If the value is not acceptable, then we call the error function, which will raise an exception.
That’s all of the source code required to make our coordinates variable functional. The next step is to turn our module into a package and define an entry point for our new class. This is very similar to what we did in the section earlier where we made a component plugin, except this time we use a different entry point group name.
from setuptools import setup, find_packages
setup(
name='coord',
version='1.0',
packages=find_packages(),
install_requires=['Traits>=3.1.0'],
entry_points={
'openmdao.variable': ['Coordinates = coord:Coordinates']
}
)
We can create this file by hand or generate it using mod2dist, as shown in an earlier section.