ExternalCodeImplicitComp

ExternalCodeImplicitComp is very similar to ExternalCodeComp in that it runs an external program in a subprocess on your operating system. But it treats the Component as an ImplicitComponent rather than an ExplicitComponent. See ExternalCodeComp for basic information about how ExternalCodeComp works.

ExternalCodeImplicitComp has most of the same options as ExternalCodeComp, but there is one major difference.

Option Default Acceptable Values Acceptable Types Description Deprecation
allowed_return_codes [0] N/A N/A List of return codes that are considered successful. N/A
assembled_jac_type csc ['csc', 'dense'] N/A Linear solver(s) in this group or implicit component, if using an assembled jacobian, will use this type. N/A
command_apply [] N/A N/A command to be executed for apply_nonlinear N/A
command_solve [] N/A N/A command to be executed for solve_nonlinear N/A
distributed False [True, False] ['bool'] True if ALL variables in this component are distributed across multiple processes. The 'distributed' option has been deprecated. Individual inputs and outputs should be set as distributed instead, using calls to add_input() or add_output().
env_vars {} N/A N/A Environment variables required by the command. N/A
external_input_files [] N/A N/A List of input files that must exist before execution, otherwise an Exception is raised. N/A
external_output_files[] N/A N/A List of output files that must exist after execution, otherwise an Exception is raised. N/A
fail_hard True [True, False] ['bool'] If True, external code errors raise a 'hard' exception (RuntimeError), otherwise errors raise a 'soft' exception (AnalysisError). N/A
poll_delay 0.0 N/A N/A Delay between polling for command completion. A value of zero will use an internally computed default. N/A
run_root_only False [True, False] ['bool'] If True, call compute/compute_partials/linearize/apply_linear/apply_nonlinear/compute_jacvec_product only on rank 0 and broadcast the results to the other ranks.N/A
timeout 0.0 N/A N/A Maximum time to wait for command completion. A value of zero implies an infinite wait. N/A

When using an ExternalCodeImplicitComp, you have the option to define two external programs rather than one. The first of these is “command_apply”, which is the command that you want to run to evaluate the residuals. You should always specify a value for this option. The second is “command_solve”, which is the command that you want to run to let the external program solve its own states. This is optional, but you should specify it if your code can solve itself, and if you want it to do so (for example, while using a Newton solver with “solve_subsystems” turned on in a higher-level Group.)

ExternalCodeImplicitComp Constructor

The call signature for the ExternalCodeImplicitComp constructor is:

ExternalCodeImplicitComp.__init__(**kwargs)[source]

Intialize the ExternalCodeComp component.

ExternalCodeImplicitComp Example

Here is a simple example of the use of an ExternalCodeImplicitComp Component. The external code in the example is a Python script that evaluates the output and residual for the implicit relationship between the area ratio and mach number in an isentropic flow. We use the same external code for both “command_apply” and “command_solve”, but in each case we pass it different flags.

#!/usr/bin/env python
#
# usage: extcode_mach.py input_filename output_filename
#
# Evaluates the output and residual for the implicit relationship
#     between the area ratio and mach number.
#
# Read the value of `area_ratio` from input file
# and writes the values or residuals of `mach` to output file depending on what is requested.
# What is requested is given by the first line in the file read. It can be either 'residuals' or
# 'outputs'.

def area_ratio_explicit(mach):
    """Explicit isentropic relationship between area ratio and Mach number"""
    gamma = 1.4
    gamma_p_1 = gamma + 1
    gamma_m_1 = gamma - 1
    exponent = gamma_p_1 / (2 * gamma_m_1)
    return (gamma_p_1 / 2.) ** -exponent * (
            (1 + gamma_m_1 / 2. * mach ** 2) ** exponent) / mach

def mach_residual(mach, area_ratio_target):
    """If area_ratio is known, then finding Mach is an implicit relationship"""
    return area_ratio_target - area_ratio_explicit(mach)

def mach_solve(area_ratio, super_sonic=False):
    """Solve for mach, given area ratio"""
    if super_sonic:
        initial_guess = 4
    else:
        initial_guess = .1
    mach = fsolve(func=mach_residual, x0=initial_guess, args=(area_ratio,))[0]
    return mach

if __name__ == '__main__':
    import sys
    from scipy.optimize import fsolve

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

    with open(input_filename, 'r') as input_file:
        output_or_resids = input_file.readline().strip()
        area_ratio = float(input_file.readline())
        if output_or_resids == 'residuals':
            mach = float(input_file.readline())
        else: # outputs
            super_sonic = (input_file.readline().strip() == "True")

    if output_or_resids == 'outputs':
        mach_output = mach_solve(area_ratio, super_sonic=super_sonic)
        with open(output_filename, 'w') as output_file:
            output_file.write('%.16f\n' % mach_output)

    elif output_or_resids == 'residuals':
        mach_resid = mach_residual(mach, area_ratio)
        with open(output_filename, 'w') as output_file:
            output_file.write('%.16f\n' % mach_resid)

import sys
import openmdao.api as om


class MachExternalCodeComp(om.ExternalCodeImplicitComp):

    def initialize(self):
        self.options.declare('super_sonic', types=bool)

    def setup(self):
        self.add_input('area_ratio', val=1.0, units=None)
        self.add_output('mach', val=1., units=None)

        self.input_file = 'mach_input.dat'
        self.output_file = 'mach_output.dat'

        # providing these are optional; the component will verify that any input
        # files exist before execution and that the output files exist after.
        self.options['external_input_files'] = [self.input_file]
        self.options['external_output_files'] = [self.output_file]


        self.options['command_apply'] = [
            sys.executable, 'extcode_mach.py', self.input_file, self.output_file,
        ]
        self.options['command_solve'] = [
            sys.executable, 'extcode_mach.py', self.input_file, self.output_file,
        ]

        # If you want to write your own string command, the code below will also work.
        # self.options['command_apply'] = ('python extcode_mach.py {} {}').format(self.input_file, self.output_file)

    def setup_partials(self):
        self.declare_partials(of='mach', wrt='area_ratio', method='fd')

    def apply_nonlinear(self, inputs, outputs, residuals):
        with open(self.input_file, 'w') as input_file:
            input_file.write('residuals\n')
            input_file.write('{}\n'.format(inputs['area_ratio'][0]))
            input_file.write('{}\n'.format(outputs['mach'][0]))

        # the parent apply_nonlinear function actually runs the external code
        super().apply_nonlinear(inputs, outputs, residuals)

        # parse the output file from the external code and set the value of mach
        with open(self.output_file, 'r') as output_file:
            mach = float(output_file.read())
        residuals['mach'] = mach

    def solve_nonlinear(self, inputs, outputs):
        with open(self.input_file, 'w') as input_file:
            input_file.write('outputs\n')
            input_file.write('{}\n'.format(inputs['area_ratio'][0]))
            input_file.write('{}\n'.format(self.options['super_sonic']))
        # the parent apply_nonlinear function actually runs the external code
        super().solve_nonlinear(inputs, outputs)

        # parse the output file from the external code and set the value of mach
        with open(self.output_file, 'r') as output_file:
            mach = float(output_file.read())
        outputs['mach'] = mach

group = om.Group()
mach_comp = group.add_subsystem('comp', MachExternalCodeComp(), promotes=['*'])
prob = om.Problem(model=group)
group.nonlinear_solver = om.NewtonSolver()
group.nonlinear_solver.options['solve_subsystems'] = True
group.nonlinear_solver.options['iprint'] = 0
group.nonlinear_solver.options['maxiter'] = 20
group.linear_solver = om.DirectSolver()

prob.setup()

area_ratio = 1.3
super_sonic = False
prob.set_val('area_ratio', area_ratio)
mach_comp.options['super_sonic'] = super_sonic
prob.run_model()
print(prob.get_val('mach'))
[0.52196203]
area_ratio = 1.3
super_sonic = True
prob.set_val('area_ratio', area_ratio)
mach_comp.options['super_sonic'] = super_sonic
prob.run_model()
print(prob.get_val('mach'))
[1.65884779]