.. index:: simple example
.. _`Getting-Started-with-OpenMDAO`:
Getting Started: A Simple Tutorial Problem
==========================================
In this section, you are going to learn how to execute a simple optimization
problem using the OpenMDAO script interface. To get the most out of this
tutorial, you should be familiar (though you don't have to be proficient) with
the Python language and the concepts presented in
:ref:`Introduction-to-the-OpenMDAO-Framework`. You should understand the terms
:term:`Component`, :term:`Assembly`, and :term:`Driver`. If you don't have
much experience with Python, we recommend trying `Dive into Python
`_. It is an excellent introduction to Python that
is licensed under the GNU Free Documentation License, so you can download and
use it as you wish.
The problem we present here is a paraboloid that is a function of two input
variables. Our goal is to find the minimum value of this function
over a particular range of interest. First, we will solve this problem with no constraints. After
this, we will add constraints and solve the problem again. We will not be providing
analytical gradients. The optimizer will calculate numerical gradients internally.
If we express the problem as a block diagram, we can see how to set it up in OpenMDAO:
.. _`OpenMDAO-overview`:
.. figure:: ../../examples/openmdao.examples.simple/openmdao/examples/simple/Simple1.png
:align: center
A Simple Optimization Problem
The optimizer is the :term:`Driver`. Its job is to manipulate the two design
variables (*x* and *y*) to minimize the output of the paraboloid function
(*f*). The Paraboloid equation fits into the OpenMDAO process as a
:term:`Component`. This Paraboloid component contains a method that operates
on the inputs (*x* and *y*) and returns the value of the function (*f*)
evaluated at those inputs. Both the driver and the component are contained in
an :term:`Assembly`, which maintains the connections between the driver and
the component, and knows how to run the system.
The following instructions will help you locate the directory containing
the pieces needed for the model.
If you have downloaded the latest release version from the website, the files you need should be
here:
``openmdao-X.X.X/lib/python2.6/site-packages/openmdao.examples.simple-X.X.X-######.egg/openmdao/examples/simple``
X.X.X is the current OpenMDAO version, and ###### is a string that
contains the Python version and the operating system description. This path may
vary depending on your system and version, but there will only be one
*simple* egg.
If you are a developer, and have a branch from the source repository, the files you need will
be here:
``examples/openmdao.examples.simple/openmdao/examples/simple``
Getting Started
---------------
The first thing you must do before running OpenMDAO is to activate the enviroment. If
you have not done this, then please refer to the instructions in the section on
:ref:`installation `.
With the environment activated, you can run OpenMDAO from anywhere by typing "python" and
the name of a Python script. We recommend creating a clean directory somewhere and starting
your work there. You will need some kind of editor to create the Python files, so use
whichever editor you are familiar with using.
.. index:: Component
Building a Component - Paraboloid
---------------------------------
A component takes a set of inputs and operates on them to produce a set of
outputs. In the OpenMDAO architecture, a class called *Component*
provides this behavior. Any Component has inputs and outputs and
contains a function called *execute* that calculates the outputs based on the
values of the inputs. Let's take a look at how we would implement the
paraboloid as an OpenMDAO component:
.. testcode:: simple_component_Paraboloid
from openmdao.main.api import Component
from openmdao.lib.api import Float
class Paraboloid(Component):
""" Evaluates the equation f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3 """
# set up interface to the framework
x = Float(0.0, iotype='in', desc='The variable x')
y = Float(0.0, iotype='in', desc='The variable y')
f_xy = Float(0.0, iotype='out', desc='F(x,y)')
def execute(self):
"""f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3
Minimum: x = 6.6667; y = -7.3333
"""
x = self.x
y = self.y
self.f_xy = (x-3.0)**2 + x*y + (y+4.0)**2 - 3.0
Your component should look pretty close to this when it is complete.
To implement a component in the OpenMDAO framework, you write some Python
code and place it in a file. This file is called a *module* in Python.
Typically, a module will contain one component, although you can include more
than one component in a single file. The file ``paraboloid.py`` contains the
code shown above. Later in this tutorial we will discuss how to execute a
model containing this component.
In Python, a class or function must be imported before it can be used. Most of
what you need in OpenMDAO can be imported from: ``openmdao.main.api`` and
``openmdao.lib.api``.
The first two lines in the ``paraboloid.py`` module import the definitions
of the Component class and the Float class. We will use these in the definition
of our Paraboloid class. Open an editor and create a file called ``paraboloid.py``.
Copy these two lines into that file by typing:
.. testcode:: simple_component_Paraboloid_pieces
from openmdao.main.api import Component
from openmdao.lib.api import Float
We could import many other objects from ``openmdao.main.api`` and ``openmdao.lib.api``, but we are
importing only the two classes that we need. This is a good idea because it helps to prevent any
namespace collisions in our module. In other words:
.. testcode:: package
# BAD
from openmdao.main.api import *
# INCONVENIENT
import openmdao.main.api
# GOOD
from openmdao.main.api import Component
The next line defines a class called *Paraboloid:*
.. testcode:: simple_component_Paraboloid_pieces
class Paraboloid(Component):
""" Evaluates the equation f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3 """
.. index:: classes, functions
We define the Paraboloid class by deriving it from
the Component class. A Paraboloid is a Component, so it
contains all of the data and members that a Component contains. This includes
a lot of helper functions that are used by the framework infrastructure to
manage things. You don't have to worry about any of the framework back-end.
Typically there are just two functions that you provide -- one for
initialization (anything that needs to be set up once), and one to execute the
component (calculate the outputs from the inputs.)
Please edit the ``paraboloid.py`` that you created and define the class
Paraboloid as we do above.
If we stop here, we have a Paraboloid component with no inputs, no
outputs, and an execute function that does nothing. The next thing we need
to do is to define the inputs and outputs in the class definition
by adding these lines:
.. testcode:: simple_component_Paraboloid_pieces
# set up interface to the framework
x = Float(0.0, iotype='in', desc='The variable x')
y = Float(0.0, iotype='in', desc='The variable y')
f_xy = Float(iotype='out', desc='F(x,y)')
.. index:: Traits
OpenMDAO has two kinds of variables: *internal* variables and *public* variables. *Internal* variables
are used internally to a component but are ignored by the framework. *Public* variables are publicly
visible (and manipulable if they are inputs) in the framework. Public variables are declared in the
class definition of a component.
All of our inputs and outputs are floating point numbers, so we use a type of
public variable called *Float*. The Float constructor contains a default
value and some arguments. The default value has been set to zero for the x
and y.
The argument *iotype* declares this variable as an input or an output. This
argument is required. If it is omitted (or misspelled) then the variable
won't be visible in the framework.
The argument *desc* contains a description, or a string of text that describes this
variable. This argument, while not required, is encouraged.
The variable is given a name by which it will be known internally and externally.
Please edit the ``paraboloid.py`` that you created and add three variables to
class Paraboloid. You will need to have *x* and *y* as inputs and ``f_xy`` as an output. Use
the example above to check your work.
For the Paraboloid component, we've created two inputs and one output. Later
in this example, an optimizer will set these inputs. In later examples, we
will see how they can be set by connecting them to an output of another
component.
Finally, we need a function to execute this component:
.. testcode:: simple_component_Paraboloid_pieces
def execute(self):
"""f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3
Optimal solution (minimum): x = 6.6667; y = -7.3333
"""
x = self.x
y = self.y
self.f_xy = (x-3.0)**2 + x*y + (y+4.0)**2 - 3.0
The execute function is where you define what a component does when it runs.
For our Paraboloid component, the equation is evaluated here. The input and
output public variables are members of the Paraboloid class, which means that
they must be accessed using *self*. For example, ``self.x`` gives you the value
stored in x. This ``self.`` can be cumbersome in a big equation, so a pair of
internal variables, *x* and *y*, are used in the calculation.
Often, you will already have the code for evaluating your component outputs,
but it will be in some other language, such as Fortran or C/C++. The :ref:`Plugin-Developer-Guide`
gives some examples of how to incorporate these kinds of components into OpenMDAO.
Please edit the ``paraboloid.py`` that you created and add an execute function
that solves the equation given above. Don't forget that indentation is important
in Python and that your execute function must be indented so that Python knows
it is part of the Paraboloid class. The finished result should look like the code
from the beginning of this tutorial.
To make sure this component works, let's try running it. Please enter the Python
shell by typing
::
python
at the command prompt. Now, let's create an instance of our Paraboloid component,
set a new value for each of the inputs, run the component, and look at the output.
::
>>> from paraboloid import Paraboloid
>>> my_comp = Paraboloid()
>>> my_comp.x = 3
>>> my_comp.y = -5
>>> my_comp.run()
>>> my_comp.f_xy
-17.0
If you have done everything correctly, you should also get -17.0 as the solution.
The Paraboloid component is now built and ready for inclusion in a larger model.
.. index:: CONMIN
. _`using-CONMIN`:
Building a Model - Unconstrained Optimization using CONMIN
-----------------------------------------------------------
Our next task is to build a model that finds the minimum value for the
Paraboloid component described above. This model contains the Paraboloid as
well as a public domain gradient optimizer called :term:`CONMIN`, for which a
Python-wrapped driver has been included in OpenMDAO. As the name implies,
:ref:`CONMIN ` finds the minimum of a function. The model can be found in the Python
file ``optimization_unconstrained.py``:
.. testcode:: simple_model_Unconstrained
from openmdao.main.api import Assembly
from openmdao.lib.api import CONMINdriver
from openmdao.examples.simple.paraboloid import Paraboloid
class OptimizationUnconstrained(Assembly):
"""Unconstrained optimization of the Paraboloid with CONMIN."""
def __init__(self):
""" Creates a new Assembly containing a Paraboloid and an optimizer"""
super(OptimizationUnconstrained, self).__init__()
# Create CONMIN Optimizer instance
self.add('driver', CONMINdriver())
# Create Paraboloid component instances
self.add('paraboloid', Paraboloid())
# CONMIN Flags
self.driver.iprint = 0
self.driver.itmax = 30
self.driver.fdch = .000001
self.driver.fdchm = .000001
# CONMIN Objective
self.driver.objective = 'paraboloid.f_xy'
# CONMIN Design Variables
self.driver.design_vars = ['paraboloid.x',
'paraboloid.y' ]
self.driver.lower_bounds = [-50, -50]
self.driver.upper_bounds = [50, 50]
Please create a file called ``optimization_unconstrained.py`` and copy this
block of code into it. We will discuss this code next.
.. index:: top level Assembly
An :term:`Assembly` is a container that can hold any number of components,
drivers, and other assemblies. An Assembly also manages the connections
between the components and assemblies that it owns, and it executes all
components and drivers in the correct order. In OpenMDAO terminology, we call
the top assembly in a model the *top level assembly.* In our problem, the top
level assembly includes a Paraboloid component and a CONMIN driver. It will
tell the CONMIN driver when to run and what to run.
We derive the class from Assembly instead of Component.
.. testsetup:: simple_model_Unconstrained_pieces
from openmdao.main.api import Assembly
from openmdao.lib.api import CONMINdriver
from openmdao.examples.simple.paraboloid import Paraboloid
from openmdao.examples.simple.optimization_unconstrained import OptimizationUnconstrained
self = OptimizationUnconstrained()
.. testcode:: simple_model_Unconstrained_pieces
class OptimizationUnconstrained(Assembly):
"""Unconstrained optimization of the Paraboloid with CONMIN."""
In the Paraboloid component, we created an execute function to tell it what to
do when the component is run. The OptimizationUnconstrained assembly does
not need an execute function, because the Assembly class already has one that
is sufficient for most cases. However, this assembly does need an initialize
function to set parameters for the optimization. This is done using the
``__init__`` function:
.. testcode:: simple_model_Unconstrained_pieces
def __init__(self):
""" Creates a new Assembly containing a Paraboloid and an optimizer"""
super(OptimizationUnconstrained, self).__init__()
.. index:: Expression
The ``__init__`` function is called by the class constructor on a new
uninitialized instance of the class, so it's a good spot to set up any
parameters that CONMIN needs. The *super* command calls the
``__init__`` function of the parent (Assembly). This is required, and forgetting it
can lead to unexpected behavior.
Next, the Paraboloid and the CONMIN driver have to be instantiated and added
to OptimizationUnconstrained. The function *add* is used to add them
to the assembly:
.. testcode:: simple_model_Unconstrained_pieces
# Create CONMIN Optimizer instance
self.add('driver', CONMINdriver())
# Create Paraboloid component instances
self.add('paraboloid', Paraboloid())
Here we make an instance of the *Paraboloid* component we created above and
give it the name *paraboloid.* Similarly we create an instance of the CONMIN
driver and give it the name *driver.* As with other class members, these are
now accessible in the OptimizationUnconstrained assembly via ``self.paraboloid``
and ``self.driver``.
For this problem, we want to minimize ``f_xy``. In optimization, this is called
the *objective function*. In OpenMDAO, we define the objective function using an
*Expression* variable:
.. testcode:: simple_model_Unconstrained_pieces
# CONMIN Objective
self.driver.objective = 'paraboloid.f_xy'
An *Expression* is a special kind of public variable that contains a string
expression that combines public variables with Python mathematical syntax.
Every public variable has a unique name in the OpenMDAO data hierarchy. This
name combines the public variable name with its parents' names. You can think
of it as something similar to the path name in a file system, but using a "."
as a separator. This allows for two components to have the same variable name
while still assuring that you can refer to each of them uniquely. Here, the
``f_xy`` output of the Paraboloid component is selected as the objective for
minimization.
Expressions are also used to define the design variables (decision variables)
for the optimization problem. While CONMIN operates only on a single objective,
it allows multiple design variables. These are assigned in a Python list:
.. testcode:: simple_model_Unconstrained_pieces
# CONMIN Design Variables
self.driver.design_vars = ['paraboloid.x',
'paraboloid.y' ]
Here, both x and y are chosen as the design variables. We can also add a range
of validity for these variables, which allows an unconstrained optimization to be
performed on what is essentially a bounded region. For this problem, we have
created a lower and an upper bound, constraining x and y to lie on [-50, 50].
.. testcode:: simple_model_Unconstrained_pieces
self.driver.lower_bounds = [-50, -50]
self.driver.upper_bounds = [50, 50]
The problem is now essentially ready to execute. CONMIN contains quite a few
additional control parameters, though the default values for many of them are
adequate. These parameters are detailed in the section on :ref:`CONMIN-driver`.
.. testcode:: simple_model_Unconstrained_pieces
# CONMIN Flags
self.driver.iprint = 1
self.driver.itmax = 30
self.driver.fdch = .000001
self.driver.fdchm = .000001
The parameters specified here include the debug verbosity (*iprint*) and the number of
iterations (*itmax*). The relative and absolute step sizes for the
numerical gradient calculation are adjusted to reduce the step size for this
problem (*fdch* and *fdchm*). If the default values are used, only two places of
accuracy can be obtained in the calculated minimum because CONMIN's default step
size is too large for this problem.
This model is now finished and ready to be run. The next section will show how this is done.
Executing the Simple Optimization Problem
------------------------------------------
To run our model, we need to create an instance of OptimizationUnconstrained and tell it to run. We
did this above using an interactive Python session. Try doing this for ``optimization_unconstrained.py``.
We can execute this model another way. We can add some code to the end of
the ``optimization_unconstrained.py`` so that it can be executed in Python,
either at the command line or in the Python shell. Using the conditional
::
``if __name__ == "__main__":``
we can include some Python code at the bottom of ``optimization_unconstrained.py``. It will execute only
when we call it at the command line or the shell, and not when another module imports it. So, the final
lines in this file are:
.. testsetup:: simple_model_Unconstrained_run
from openmdao.examples.simple.optimization_unconstrained import OptimizationUnconstrained
__name__ = "__main__"
.. testcode:: simple_model_Unconstrained_run
if __name__ == "__main__":
from openmdao.main.api import set_as_top
opt_problem = OptimizationUnconstrained()
set_as_top(opt_problem)
import time
tt = time.time()
opt_problem.run()
print "\n"
print "CONMIN Iterations: ", opt_problem.driver.iter_count
print "Minimum found at (%f, %f)" % (opt_problem.paraboloid.x, \
opt_problem.paraboloid.y)
print "Elapsed time: ", time.time()-tt, "seconds"
.. testoutput:: simple_model_Unconstrained_run
:hide:
...
CONMIN Iterations: 5
Minimum found at (6.666309, -7.333026)
Elapsed time: ... seconds
This block of code does four things. In the first statement, we create an
instance of the class OptimizationUnconstrained with the name
``opt_problem``. In the second statement, we set ``opt_problem`` as the top
Assembly in the model hierarchy. This will be explained in a later tutorial.
In the fifth statement, we tell ``opt_problem`` to run. The model will execute
until the optimizer's termination criteria are reached. The rest of the
statements print the results and report the elapsed time.
Please edit your copy of ``optimization_unconstrained.py`` and add the
block of code into it. Now, save the file and type the following at the command
prompt:
::
python optimization_unconstrained.py
This should produce the output:
::
[ CONMIN output not shown ]
CONMIN Iterations: 5
Minimum found at (6.666309, -7.333026)
Elapsed time: 0.0558300018311 seconds
Now we are ready to solve a more advanced optimization problem with constraints.
.. index:: constraints, CONMIN
.. _`constrained-optimization`:
Building a Model - Constrained Optimization using CONMIN
---------------------------------------------------------
Usually, an optimization problem also contains constraints that reduce the
design space. *Constraints* are equations or inequalities that are expressed as functions
of the design variables. We would like to add a constraint to our model in
``optimization_unconstrained.py``. First, copy the file and give the new file the
name ``optimization_constrained.py``. Inside of this file, change the name of the
assembly from OptimizationUnconstrained to OptimizationConstrained. Don't forget to
also change it in the bottom section where it is instantiated and run.
In OpenMDAO, you can construct a constraint with an Expression using any available public
variables to build an expression with Python mathematical syntax. For CONMIN,
the constraints parameter is a list of inequalities that are defined to be
satisfied when they return a negative value or zero and violated when they
return a positive value.
We want to add the constraint ``(y-x+15)<0`` to the problem. The unconstrained
minimum violates this constraint, so a new minimum must be found by
the optimizer. We can add a constraint to our existing OptimizationUnconstrained
model by adding one line to the init function:
.. testcode:: simple_model_Unconstrained_pieces
# CONMIN Constraints
self.driver.constraints = ['paraboloid.y-paraboloid.x+15.0']
So, please add this line to the ``__init__`` function in
``optimization_constrained.py`` and save it. Execute it by typing:
::
python optimization_constrained.py
When it is executed, it should produce this output:
::
[ CONMIN output not shown ]
CONMIN Iterations: 6
Minimum found at (7.175775, -7.824225)
Elapsed time: 0.0295481681824 seconds
Notice that the minimum of the constrained problem is different from the minimum of
the unconstrained problem.
This concludes an introduction to a simple problem of component creation and execution in
OpenMDAO. The next tutorial section introduces a problem with more complexity and
presents additional features of the framework.