Skip to content

Commit

Permalink
Merge pull request #430 from Kenneth-T-Moore/external
Browse files Browse the repository at this point in the history
ExternalCode ported from OpenMDAO 1.x
  • Loading branch information
naylor-b committed Nov 3, 2017
2 parents 17524c4 + c077fc6 commit d653cef
Show file tree
Hide file tree
Showing 10 changed files with 1,094 additions and 1 deletion.
1 change: 1 addition & 0 deletions openmdao/api.py
Expand Up @@ -12,6 +12,7 @@
# Components
from openmdao.components.balance_comp import BalanceComp
from openmdao.components.deprecated_component import Component
from openmdao.components.external_code import ExternalCode
from openmdao.components.exec_comp import ExecComp
from openmdao.components.linear_system_comp import LinearSystemComp
from openmdao.components.meta_model import MetaModel
Expand Down
225 changes: 225 additions & 0 deletions openmdao/components/external_code.py
@@ -0,0 +1,225 @@
"""Define the ExternalCode class."""
from __future__ import print_function

import os
import sys

from six import iteritems, itervalues

import numpy.distutils
from numpy.distutils.exec_command import find_executable

from openmdao.core.analysis_error import AnalysisError
from openmdao.core.explicitcomponent import ExplicitComponent
from openmdao.utils.options_dictionary import OptionsDictionary
from openmdao.utils.shell_proc import STDOUT, DEV_NULL, ShellProc


class ExternalCode(ExplicitComponent):
"""
Run an external code as a component.
Default stdin is the 'null' device, default stdout is the console, and
default stderr is ``error.out``.
Options
-------
options['command'] : list([])
Command to be executed. Command must be a list of command line args.
options['env_vars'] : dict({})
Environment variables required by the command
options['external_input_files'] : list([])
(optional) list of input file names to check the existence of before solve_nonlinear
options['external_output_files'] : list([])
(optional) list of input file names to check the existence of after solve_nonlinear
options['poll_delay'] : float(0.0)
Delay between polling for command completion. A value of zero will use
an internally computed default.
options['timeout'] : float(0.0)
Maximum time in seconds to wait for command completion. A value of zero
implies an infinite wait. If the timeout interval is exceeded, an
AnalysisError will be raised.
options['fail_hard'] : bool(True)
Behavior on error returned from code, either raise a 'hard' error (RuntimeError) if True
or a 'soft' error (AnalysisError) if False.
"""

def __init__(self):
"""
Intialize the ExternalCode component.
"""
super(ExternalCode, self).__init__()

self.STDOUT = STDOUT
self.DEV_NULL = DEV_NULL

# Input options for this Component
self.options.declare('command', [], desc='command to be executed')
self.options.declare('env_vars', {},
desc='Environment variables required by the command')
self.options.declare('poll_delay', 0.0, lower=0.0,
desc='Delay between polling for command completion. A value of zero '
'will use an internally computed default')
self.options.declare('timeout', 0.0, lower=0.0,
desc='Maximum time to wait for command completion. A value of zero '
'implies an infinite wait')
self.options.declare('external_input_files', [],
desc='(optional) list of input file names to check the existence '
'of before solve_nonlinear')
self.options.declare('external_output_files', [],
desc='(optional) list of input file names to check the existence of '
'after solve_nonlinear')
self.options.declare('fail_hard', True,
desc="If True, external code errors raise a 'hard' exception "
"(RuntimeError). Otherwise raise a 'soft' exception "
"(AnalysisError).")

# Outputs of the run of the component or items that will not work with the OptionsDictionary
self.return_code = 0 # Return code from the command
self.stdin = self.DEV_NULL
self.stdout = None
self.stderr = "error.out"

def check_config(self, logger):
"""
Perform optional error checks.
Parameters
----------
logger : object
The object that manages logging output.
"""
# check for the command
cmd = [c for c in self.options['command'] if c.strip()]
if not cmd:
logger.error("The command cannot be empty")
else:
program_to_execute = self.options['command'][0]
command_full_path = find_executable(program_to_execute)

if not command_full_path:
logger.error("The command to be executed, '%s', "
"cannot be found" % program_to_execute)

# Check for missing input files
missing = self._check_for_files(self.options['external_input_files'])
if missing:
logger.error("The following input files are missing at setup "
" time: %s" % missing)

def compute(self, inputs, outputs):
"""
Run this component.
User should call this method from their overriden compute method.
Parameters
----------
inputs : Vector
Unscaled, dimensional input variables read via inputs[key].
outputs : Vector
Unscaled, dimensional output variables read via outputs[key].
"""
self.return_code = -12345678

if not self.options['command']:
raise ValueError('Empty command list')

if self.options['fail_hard']:
err_class = RuntimeError
else:
err_class = AnalysisError

return_code = None

try:
missing = self._check_for_files(self.options['external_input_files'])
if missing:
raise err_class("The following input files are missing: %s"
% sorted(missing))
return_code, error_msg = self._execute_local()

if return_code is None:
raise AnalysisError('Timed out after %s sec.' %
self.options['timeout'])

elif return_code:
if isinstance(self.stderr, str):
if os.path.exists(self.stderr):
stderrfile = open(self.stderr, 'r')
error_desc = stderrfile.read()
stderrfile.close()
err_fragment = "\nError Output:\n%s" % error_desc
else:
err_fragment = "\n[stderr %r missing]" % self.stderr
else:
err_fragment = error_msg

raise err_class('return_code = %d%s' % (return_code,
err_fragment))

missing = self._check_for_files(self.options['external_output_files'])
if missing:
raise err_class("The following output files are missing: %s"
% sorted(missing))

finally:
self.return_code = -999999 if return_code is None else return_code

def _check_for_files(self, files):
"""
Check that specified files exist.
Parameters
----------
files : iterable
Contains files to check.
Returns
-------
list
List of files that do not exist.
"""
return [path for path in files if not os.path.exists(path)]

def _execute_local(self):
"""
Run the command.
Returns
-------
int
Return Code
str
Error Message
"""
# Check to make sure command exists
if isinstance(self.options['command'], str):
program_to_execute = self.options['command']
else:
program_to_execute = self.options['command'][0]

# Suppress message from find_executable function, we'll handle it
numpy.distutils.log.set_verbosity(-1)

command_full_path = find_executable(program_to_execute)
if not command_full_path:
msg = "The command to be executed, '%s', cannot be found" % program_to_execute
raise ValueError(msg)

command_for_shell_proc = self.options['command']
if sys.platform == 'win32':
command_for_shell_proc = ['cmd.exe', '/c'] + command_for_shell_proc

self._process = \
ShellProc(command_for_shell_proc, self.stdin,
self.stdout, self.stderr, self.options['env_vars'])

try:
return_code, error_msg = \
self._process.wait(self.options['poll_delay'], self.options['timeout'])
finally:
self._process.close_files()
self._process = None

return (return_code, error_msg)
18 changes: 18 additions & 0 deletions openmdao/components/tests/external_code_feature_sample.py
@@ -0,0 +1,18 @@
import sys

def paraboloid(input_filename, output_filename):
with open(input_filename, 'r') as input_file:
file_contents = input_file.readlines()
x, y = [ float(f) for f in file_contents ]

f_xy = (x-3.0)**2 + x*y + (y+4.0)**2 - 3.0

with open( output_filename, 'w') as out:
out.write('%f\n' % f_xy )

if __name__ == "__main__":

input_filename = sys.argv[1]
output_filename = sys.argv[2]

paraboloid(input_filename, output_filename)
31 changes: 31 additions & 0 deletions openmdao/components/tests/external_code_sample.py
@@ -0,0 +1,31 @@
import os
import time
import argparse

def main():
""" Just an external program for testing ExternalCode. """

parser = argparse.ArgumentParser()
parser.add_argument("output_filename")
parser.add_argument("-e", "--write_test_env_var", help="Write the value of TEST_ENV_VAR to the file",
action="store_true", default=False)
parser.add_argument("-d", "--delay", type=float,
help="time in seconds to delay")

args = parser.parse_args()

if args.delay:
if args.delay < 0:
raise ValueError('delay must be >= 0')
time.sleep(args.delay)

with open(args.output_filename, 'w') as out:
out.write("test data\n")
if args.write_test_env_var:
out.write("%s\n" % os.environ['TEST_ENV_VAR'])

return 0

if __name__ == '__main__':
main()

0 comments on commit d653cef

Please sign in to comment.