OpenMDAO Component Integration

This notebook demonstrates how to wrap any Standard Evaluator as an OpenMDAO ExplicitComponent using EvaluatorOpenMdaoComponent. You will learn how OptProblem variables map to OpenMDAO inputs, how responses map to outputs (with bounds and scaling), and how to serialize an evaluator for later reconstruction inside an OpenMDAO model.

How EvaluatorOpenMdaoComponent Works

The EvaluatorOpenMdaoComponent class bridges the Standard Evaluator API and OpenMDAO:

  • Variables → Inputs: Each variable in the evaluator’s OptProblem becomes an OpenMDAO input. The variable’s default value sets the input default, and bounds define the feasible range.

  • Responses → Outputs: Each response becomes an OpenMDAO output. Response bounds map to lower/upper, and shift/scale map to OpenMDAO’s ref0/ref scaling parameters.

  • Compute: When OpenMDAO calls compute(), the component builds a pandas DataFrame from the current inputs, calls the evaluator, and extracts the response values back into the OpenMDAO output vector.

This means any evaluator — whether backed by a NumPy function, a surrogate model, or an external executable — can participate in an OpenMDAO model with a single line of wrapping code.

Setup

We start by importing the Standard Evaluator library and OpenMDAO.

import standard_evaluator as se
from standard_evaluator.evaluators.numpy_evaluator import NumpyEvaluator
from standard_evaluator.components import EvaluatorOpenMdaoComponent
import openmdao.api as om
import numpy as np
import pandas as pd
matlabengine not available.

Wrapping an Evaluator as an OpenMDAO Component

We define a simple evaluator that computes \(f(x, y) = x^2 + y^2\). First we create an OptProblem with two variables and one response, then implement the NumpyEvaluator subclass.

# Define the optimization problem interface
opt_problem = se.utilities.create_opt_problem(num_independent=2, num_dependent=1)
opt_problem.variables[0].name = "x"
opt_problem.variables[0].bounds = (-5.0, 5.0)
opt_problem.variables[0].default = 1.0
opt_problem.variables[1].name = "y"
opt_problem.variables[1].bounds = (-5.0, 5.0)
opt_problem.variables[1].default = 2.0
opt_problem.responses[0].name = "f"
opt_problem.responses[0].bounds = (0.0, 50.0)
opt_problem.responses[0].scale = 0.02
opt_problem.responses[0].shift = -25.0
opt_problem.objectives = ["f"]
opt_problem.constraints = []


# Implement the evaluator
class SumOfSquares(NumpyEvaluator):
    """Computes f = x^2 + y^2."""

    def eval_np(self, sites, **kwargs):
        x = sites[:, 0]
        y = sites[:, 1]
        return (x**2 + y**2).reshape(-1, 1)


evaluator = SumOfSquares(opt_problem=opt_problem)
print("Inputs:", evaluator.inputs)
print("Outputs:", evaluator.outputs)
Inputs: ['x', 'y']
Outputs: ['f']

Now we wrap the evaluator in an EvaluatorOpenMdaoComponent, add it to an OpenMDAO Problem, and run the model.

# Create an OpenMDAO Problem with the wrapped evaluator
prob = om.Problem()
prob.model.add_subsystem(
    "comp", EvaluatorOpenMdaoComponent(evaluator), promotes=["*"]
)
prob.setup()

# Set input values and run
prob.set_val("x", 3.0)
prob.set_val("y", 4.0)
prob.run_model()

print(f"x = {prob.get_val('x')[0]}")
print(f"y = {prob.get_val('y')[0]}")
print(f"f = x^2 + y^2 = {prob.get_val('f')[0]}")
x = 3.0
y = 4.0
f = x^2 + y^2 = 25.0

The component correctly computes \(f = 3^2 + 4^2 = 25\). The evaluator’s OptProblem defined the variable names and bounds, and the component automatically registered them as OpenMDAO inputs and outputs.

Inspecting OpenMDAO Input and Output Metadata

The component maps OptProblem variable/response properties to OpenMDAO metadata. We can retrieve this metadata to verify how bounds, defaults, and scaling are configured.

comp = prob.model.comp

# Input metadata: default values and shape
input_meta = comp.get_io_metadata(
    iotypes="input", metadata_keys=["val", "shape", "units"]
)
print("Input metadata:")
print("-" * 50)
for abs_name, meta in input_meta.items():
    print(f"  {meta['prom_name']:>4s}: default = {meta['val']}, shape = {meta['shape']}")

print()

# Output metadata: bounds and scaling
output_meta = comp.get_io_metadata(
    iotypes="output", metadata_keys=["val", "shape", "lower", "upper", "ref0", "ref"]
)
print("Output metadata:")
print("-" * 50)
for abs_name, meta in output_meta.items():
    print(f"  {meta['prom_name']:>4s}: lower = {meta['lower']}, upper = {meta['upper']}")
    print(f"        ref0 = {meta['ref0']}, ref = {meta['ref']}")
Input metadata:
--------------------------------------------------
     x: default = [1.], shape = (1,)
     y: default = [2.], shape = (1,)

Output metadata:
--------------------------------------------------
     f: lower = [0.], upper = [50.]
        ref0 = 25.0, ref = 75.0

The metadata shows:

  • Inputs receive their default values from the OptProblem variables (x defaults to 1.0, y to 2.0).

  • Outputs receive lower/upper bounds from the response bounds (0.0 to 50.0), and ref0/ref are computed from the response’s shift and scale parameters. OpenMDAO uses ref0 and ref to normalize the output for optimizers: normalized = (physical - ref0) / (ref - ref0).

Serialization and Reconstruction via evaluator_options

Surrogate models (which inherit from NumpyEvaluator) support serialization via to_dict(). The resulting dictionary can be passed as the evaluator_options keyword argument to construct a new EvaluatorOpenMdaoComponent — without needing the original Python class definition. This is useful for saving trained models and loading them into OpenMDAO workflows later.

We demonstrate this by training a PolynomialModel on data from our evaluator, wrapping it in a component, then reconstructing an equivalent component from the serialized dictionary.

from standard_evaluator.surrogate_models import PolynomialModel, PolynomialModelOptions

# Generate training data from the evaluator
np.random.seed(42)
train_df = pd.DataFrame({
    "x": np.random.uniform(-5, 5, 20),
    "y": np.random.uniform(-5, 5, 20),
})
evaluator(train_df)

# Train a degree-2 polynomial model (exact fit for x^2 + y^2)
poly_model = PolynomialModel(
    sites=train_df,
    options=PolynomialModelOptions(degree=2),
    opt_problem=opt_problem,
)

# Serialize the trained model
model_dict = poly_model.to_dict()
print(f"Serialized model type: {model_dict['type']}")
print(f"Dictionary keys: {list(model_dict.keys())}")
Serialized model type: PolynomialModel
Dictionary keys: ['type', 'info', 'opt_problem', 'version', 'name']

Now we use the original polynomial model in an OpenMDAO component and compare its output to a reconstructed component built from the serialized dictionary.

# Original component using the trained polynomial model
prob_original = om.Problem()
prob_original.model.add_subsystem(
    "comp", EvaluatorOpenMdaoComponent(poly_model), promotes=["*"]
)
prob_original.setup()
prob_original.set_val("x", 3.0)
prob_original.set_val("y", 4.0)
prob_original.run_model()
original_f = prob_original.get_val("f")[0]
print(f"Original component output:      f = {original_f:.6f}")

# Reconstructed component from evaluator_options
prob_reconstructed = om.Problem()
prob_reconstructed.model.add_subsystem(
    "comp", EvaluatorOpenMdaoComponent(evaluator_options=model_dict), promotes=["*"]
)
prob_reconstructed.setup()
prob_reconstructed.set_val("x", 3.0)
prob_reconstructed.set_val("y", 4.0)
prob_reconstructed.run_model()
reconstructed_f = prob_reconstructed.get_val("f")[0]
print(f"Reconstructed component output: f = {reconstructed_f:.6f}")

print(f"\nOutputs match: {np.isclose(original_f, reconstructed_f)}")
Original component output:      f = 25.000000
Reconstructed component output: f = 25.000000

Outputs match: True

The reconstructed component produces identical output. The evaluator_options keyword internally calls SurrogateModel.from_dict() to rebuild the trained model, so the full prediction capability is preserved without needing the original training data or class definition in scope.