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 |
---|---|---|---|---|
allowed_return_codes | [0] | N/A | N/A | List of return codes that are considered successful. |
always_opt | False | [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_type | csc | ['csc', 'dense'] | N/A | Linear solver(s) in this group or implicit component, if using an assembled jacobian, will use this type. |
command_apply | [] | N/A | N/A | command to be executed for apply_nonlinear |
command_solve | [] | N/A | N/A | command to be executed for solve_nonlinear |
derivs_method | N/A | ['jax', 'cs', 'fd', None] | N/A | The method to use for computing derivatives |
distributed | False | [True, False] | ['bool'] | If True, set all variables in this component as distributed across multiple processes |
env_vars | {} | N/A | N/A | Environment variables required by the command. |
external_input_files | [] | N/A | N/A | List of input files that must exist before execution, otherwise an Exception is raised. |
external_output_files | [] | N/A | N/A | List of output files that must exist after execution, otherwise an Exception is raised. |
fail_hard | True | [True, False] | ['bool'] | If True, external code errors raise a 'hard' exception (RuntimeError), otherwise errors raise a 'soft' exception (AnalysisError). |
poll_delay | 0.0 | N/A | N/A | Delay between polling for command completion. A value of zero will use an internally computed default. |
run_root_only | False | [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. |
timeout | 0.0 | N/A | N/A | Maximum time to wait for command completion. A value of zero implies an infinite wait. |
use_jit | True | [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]