Evaluator Hierarchy
This notebook introduces the core abstraction in the Standard Evaluator library: the Evaluator class hierarchy. You will learn how the abstract Evaluator base class defines a common API for wrapping analysis codes, explore the available concrete subclasses, and see how decorators add caching and logging capabilities. By the end, you will be able to create evaluators, call them with DataFrames or NumPy arrays, and leverage built-in caching and logging.
The Evaluator Class Hierarchy
The Evaluator abstract base class defines the standard interface that all evaluators share:
__call__(sites: DataFrame)— Evaluate sites provided as a pandas DataFrame. The DataFrame is modified in-place to include response columns.eval_np(sites: ndarray)— Evaluate sites provided as a NumPy array with unrolled scalar columns (excludes fixed variables and unrolls ArrayVariables).eval_list(sites: List)— Same aseval_npbut accepts and returns Python lists.
Concrete Subclasses
Class |
Purpose |
|---|---|
|
Wraps a user-defined function that operates on NumPy arrays |
|
Wraps an external executable (command-line tool) |
|
Applies shift/scale transformations before delegating to another evaluator |
|
Wraps an OpenMDAO component as an evaluator |
|
Wraps an Excel workbook as an evaluator |
|
Wraps a MATLAB function via the MATLAB Engine API |
Decorators
The evaluator constructor supports two decorator parameters:
cache— Pass a path to a SQLite database file to enable caching of evaluated sites. Previously computed sites are looked up rather than re-evaluated.logging— Set toTrueto record every evaluation call. The logged history is accessible viaget_log().
Setup
We begin by importing the library using the standard alias convention.
import standard_evaluator as se
import pandas as pd
import numpy as np
matlabengine not available.
Creating and Calling an Evaluator
We create a Sphere benchmark evaluator with 3 design variables. This is a NumpyEvaluator subclass representing the sphere function \(f(\mathbf{x}) = \sum x_i^2\). We then build an input DataFrame with 3 rows and call the evaluator using __call__.
# Create a Sphere benchmark evaluator with 3 design variables
evaluator = se.evaluators.Sphere(num_independent=3)
print("Inputs:", evaluator.inputs)
print("Outputs:", evaluator.outputs)
print("Variable bounds:")
for var in evaluator.opt_problem.variables:
print(f" {var.name}: {var.bounds}")
Inputs: ['x0', 'x1', 'x2']
Outputs: ['f']
Variable bounds:
x0: (-10.0, 10.0)
x1: (-10.0, 10.0)
x2: (-10.0, 10.0)
Now we build an input DataFrame with 3 sample sites and call the evaluator. The __call__ method modifies the DataFrame in-place, appending the response column f.
# Build an input DataFrame with 3 rows of sample sites
sites = pd.DataFrame({
"x0": [1.0, 2.0, -3.0],
"x1": [0.5, -1.0, 4.0],
"x2": [2.0, 3.0, -1.0],
})
# Call the evaluator — modifies sites in-place
evaluator(sites)
# Display the result with both input and response columns
sites
| x0 | x1 | x2 | f | |
|---|---|---|---|---|
| 0 | 1.0 | 0.5 | 2.0 | 5.25 |
| 1 | 2.0 | -1.0 | 3.0 | 14.00 |
| 2 | -3.0 | 4.0 | -1.0 | 26.00 |
The DataFrame now contains the original input columns (x0, x1, x2) along with the computed response column f, where each row’s value equals \(x_0^2 + x_1^2 + x_2^2\).
Using eval_np and eval_list
The eval_np and eval_list methods provide a convenience interface that works with raw NumPy arrays or Python lists instead of DataFrames. Key differences from __call__:
Fixed variables are excluded from the input — you only provide varying design variables.
ArrayVariables are unrolled into individual scalar columns (e.g., a shape
(3,)array becomes 3 separate values).Only the
__call__method preserves array variables as arrays within DataFrame cells.
These methods are useful when integrating with optimization libraries that operate on flat numeric arrays.
# eval_np: pass a 2D NumPy array (rows=sites, cols=unrolled inputs)
sites_np = np.array([
[1.0, 0.5, 2.0],
[2.0, -1.0, 3.0],
[-3.0, 4.0, -1.0],
])
results_np = evaluator.eval_np(sites_np)
print("eval_np results (NumPy array):")
print(results_np)
eval_np results (NumPy array):
[[ 5.25]
[14. ]
[26. ]]
The eval_list method works identically but accepts and returns Python lists of lists.
# eval_list: pass a list of lists
sites_list = [
[1.0, 0.5, 2.0],
[2.0, -1.0, 3.0],
]
results_list = evaluator.eval_list(sites_list)
print("eval_list results (Python lists):")
print(results_list)
eval_list results (Python lists):
[[5.25], [14.0]]
Both eval_np and eval_list return only the response values (here, the f column as a single-column array or nested list).
Caching Decorator
By passing a cache parameter (a path to a SQLite database file) when constructing an evaluator, the caching decorator is applied. Previously evaluated sites are retrieved from the cache instead of being re-computed. After evaluation, you can inspect the cache using get_cache().
import tempfile
import os
# Create a temporary database file for caching
cache_file = os.path.join(tempfile.gettempdir(), "sphere_cache_demo.db")
# Create evaluator with caching enabled
cached_evaluator = se.evaluators.Sphere(num_independent=2, cache=cache_file)
# Evaluate two sites
sites_cached = pd.DataFrame({"x0": [1.0, 2.0], "x1": [3.0, 4.0]})
cached_evaluator(sites_cached)
print("Evaluation result:")
print(sites_cached)
# Inspect the cache
print("\nCached sites:")
print(cached_evaluator.get_cache())
Evaluation result:
x0 x1 f
0 1.0 3.0 10.0
1 2.0 4.0 20.0
Cached sites:
x0 x1 f
0 1.0 3.0 10.0
1 2.0 4.0 20.0
The cache stores all evaluated sites. On subsequent calls, any site matching a cached entry (within a configurable tolerance) will retrieve its response from the database rather than re-evaluating.
Logging Decorator
Setting logging=True when constructing an evaluator enables the logging decorator. Every evaluation call is recorded, and the full history can be retrieved using get_log(). Each logged site includes a call_num column indicating which evaluation call it belongs to.
# Create evaluator with logging enabled
logged_evaluator = se.evaluators.Sphere(num_independent=2, logging=True)
# First evaluation call
sites_a = pd.DataFrame({"x0": [1.0, 2.0], "x1": [3.0, 4.0]})
logged_evaluator(sites_a)
# Second evaluation call
sites_b = pd.DataFrame({"x0": [5.0], "x1": [6.0]})
logged_evaluator(sites_b)
# Retrieve the full evaluation log
log = logged_evaluator.get_log()
print("Evaluation log:")
log
Evaluation log:
| x0 | x1 | f | call_num | |
|---|---|---|---|---|
| 0 | 1.0 | 3.0 | 10.0 | 1.0 |
| 1 | 2.0 | 4.0 | 20.0 | 1.0 |
| 2 | 5.0 | 6.0 | 61.0 | 2.0 |
The log shows all 3 evaluated sites across both calls. The call_num column distinguishes which invocation produced each row — sites from the first call have call_num=1 and the site from the second call has call_num=2.