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.

OptionDefaultAcceptable ValuesAcceptable TypesDescription
allowed_return_codes[0]N/AN/AList of return codes that are considered successful.
always_optFalse[True, False]['bool']If True, force nonlinear operations on this component to be included in the optimization loop even if this component is not relevant to the design variables and responses.
assembled_jac_typecsc['csc', 'dense']N/ALinear solver(s) in this group or implicit component, if using an assembled jacobian, will use this type.
command_apply[]N/AN/Acommand to be executed for apply_nonlinear
command_solve[]N/AN/Acommand to be executed for solve_nonlinear
derivs_methodN/A['jax', 'cs', 'fd', None]N/AThe method to use for computing derivatives
distributedFalse[True, False]['bool']If True, set all variables in this component as distributed across multiple processes
env_vars{}N/AN/AEnvironment variables required by the command.
external_input_files[]N/AN/AList of input files that must exist before execution, otherwise an Exception is raised.
external_output_files[]N/AN/AList of output files that must exist after execution, otherwise an Exception is raised.
fail_hardTrue[True, False]['bool']If True, external code errors raise a 'hard' exception (RuntimeError), otherwise errors raise a 'soft' exception (AnalysisError).
poll_delay0.0N/AN/ADelay between polling for command completion. A value of zero will use an internally computed default.
run_root_onlyFalse[True, False]['bool']If True, call compute, compute_partials, linearize, apply_linear, apply_nonlinear, and compute_jacvec_product only on rank 0 and broadcast the results to the other ranks.
timeout0.0N/AN/AMaximum time to wait for command completion. A value of zero implies an infinite wait.
use_jitTrue[True, False]['bool']If True, attempt to use jit on compute_primal, assuming jax or some other AD package is active.

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]