Example: Compressor Efficiency Map
This walkthrough demonstrates fitting a 2D compressor efficiency map using the
class-based EBF API. The full script is at examples/comp_map_ebf.py.
The Problem
A compressor map relates operating conditions — corrected mass flow and total pressure ratio — to compressor efficiency. We have scattered test data and want to produce a smooth efficiency surface for use in cycle simulations.
The Data
data/GenericMap.xlsx holds 56 operating points on a single sheet
named data, with one column per variable:
| Column | Meaning | Units | Role |
|---|---|---|---|
mdot |
Corrected mass flow | kg/s | input 1 |
PR |
Total-to-total pressure ratio | — | input 2 |
eta |
Total-to-total efficiency | — | output |
import numpy as np
import ebf
import pandas as pd
from pathlib import Path
data_path = Path("data/GenericMap.xlsx")
df = pd.read_excel(data_path, sheet_name="data")
mdot = df['mdot'].values # corrected mass flow (input 1)
PR = df['PR'].values # pressure ratio (input 2)
eta = df['eta'].values # efficiency (output)
X = np.column_stack([mdot, PR])
The input array X has shape (n_points, 2) and the output eta has shape
(n_points,).
Creating and Training the Model
model = ebf.EBF(
n_nodes=9, # number of interpolation nodes
basis='multiquadric', # basis function — see ebf.BASIS_FUNCTIONS for all options
eps=1e-8, # numerical stability offset
)
model.fit(
X, eta, # inputs and output
steps=80000, # optimizer iterations
lr=0.01, # initial learning rate (Adam with exponential decay)
var_weight=0.01, # node spread regularization strength
ellipsoid_weight=0.001, # ellipsoid shape penalty — explicit smoothness knob (ADR-011)
loss_type='huber', # 'rmse' (default), 'huber', or 'tukey'
huber_delta='auto', # 'auto' (default) tracks the residual noise floor
tukey_c='auto', # Tukey rejection point — 'auto' (default) recommended
val_fraction=0.0, # held-out fraction for early stopping (0 = off)
patience=10, # val evaluations without improvement before stopping
verbose=True, # print training progress every 100 steps
loss_threshold=0.05, # early stopping when training loss <= this value
seed=42, # reproducible weight initialization
)
Parameter guidance:
n_nodes— Start with roughly 1/3 to 1/2 the number of data points. More nodes give more flexibility but take longer to train.basis—'multiquadric'is a good default. Try'gaussian'or'matern52'if you want localized node influence. See the Basis Functions page.var_weight— Controls how strongly nodes are prevented from collapsing together, and is also the primary smoothing knob. Forcing nodes apart causes the optimizer to broaden each node's ellipsoidal influence zone (lower effectivervalues), producing a smoother surface. Increasevar_weightto smooth the fit; decrease it if the fit is too flat or misses sharp features. If nodes visibly cluster together,var_weightis too low.ellipsoid_weight— The explicit smoothness knob (ADR-011). It penalizes the mean squared Frobenius norm of each node's ellipsoid factorL, which bounds how sharp a node's influence zone can get. Unlikevar_weightit acts on the sharpness mechanism directly rather than through node spacing, so reach for it first when a fit is too wiggly.0.0(default) disables it; small values like0.001are usually enough.loss_type—'rmse'(default) gives standard least-squares fitting. Use'huber'for noisy data with outliers (outliers get reduced, linear weight), or'tukey'when some points are outright erroneous — residuals beyond the Tukey rejection point exert zero pull and are effectively discarded.huber_delta— Controls the threshold where Huber loss switches from quadratic to linear. The default'auto'recalibrates it every 100 steps from the current residual spread, so roughly the largest ~18% of residuals get linear (outlier-resistant) treatment as the fit tightens. Pass a float (in scaled data space) to fix the threshold; lower values make the model more aggressive at ignoring outliers.tukey_c— The Tukey biweight rejection point (only used withloss_type='tukey'). Keep the default'auto': it tracks the residual noise floor at4.685·σand anneals from an effectively quadratic start, which protects against rejecting good data early in training.steps— More steps gives the optimizer more time to converge. Watch the loss printout — if it's still decreasing at the end, increasesteps.loss_threshold— Stops training early once the training loss is low enough. Set toNoneto always run the full number of steps.val_fraction— Holds out a fraction of the points (e.g.0.15) and stops training when the held-out loss stops improving forpatienceevaluations, then restores the best weights. This is the most reliable way to avoid fitting measurement noise, and it replaces guessingsteps— but it needs roughly 50+ data points to give a stable signal.0.0(default) disables it.
Prediction and Inspection
Out = model.predict(X) # predictions at training points
Nodes = model.get_nodes() # node positions in original space
print("Node positions:\n", Nodes)
# Per-step training history, stored by fit() — columns are (step, loss)
print(f"Trained for {len(model.history_)} steps, "
f"final loss {model.history_[-1, 1]:.4f}")
predict() accepts any (n_points, 2) array — it doesn't have to be the
training data. get_nodes() returns node positions in the original (unscaled)
coordinate space.
Saving and Loading
ckpt_path = model.save("checkpoints", filename='compressor-map')
print("Saved to:", ckpt_path)
loaded_model = ebf.EBF.load(ckpt_path)
Out_loaded = loaded_model.predict(X)
The checkpoint includes all model weights, the basis function configuration, and the Scale/Offset values needed to convert between scaled and unscaled space.
Evaluation and Plotting
The example uses the built-in visualization utilities to generate three plots with a single function call each:
# Convergence plot — training loss curve with the early-stopping target
ebf.convergence_plot(model, loss_threshold=0.05)
# Correlation plot — data vs prediction with R²
ebf.correlation_plot(eta, Out)
# Contour plot — filled contour of the fitted surface
ebf.contour_plot_2d(
model, X, eta,
xlabel='Corrected Mass Flow, mdot',
ylabel='Pressure Ratio, PR',
zlabel='Efficiency, eta',
show_data=True,
show_nodes=True,
)
The contour plot automatically masks predictions outside the convex hull of the training data so the plot doesn't show misleading extrapolation.
A well-fitted model should achieve R² > 0.99 on this dataset.
The Summary Figure
summary_plot_3d() composes all of the above into one figure — the fitted
surface alongside the three diagnostics:
ebf.summary_plot_3d(
model, X, eta,
xlabel='Corrected Mass Flow, mdot',
ylabel='Pressure Ratio, PR',
zlabel='Efficiency, eta',
loss_threshold=0.05,
show_nodes=True,
error_color=True, # the default — shade points by |error|
)

56 test points, 9 nodes. The three right-hand panels answer the questions worth asking of any fit:
- Correlation (R² = 0.9964) — is it accurate overall?
- Residuals (RMSE = 0.0083, ~1.3% of the 0.184–0.843 efficiency range) — is it wrong anywhere in particular? Note the single point near prediction 0.5 sitting well below the others; the correlation plot hides it against the 1:1 line.
- Convergence — training stopped at 11,783 of the 80,000 requested
steps when the loss reached
loss_threshold=0.05.
Every data point is shaded by its absolute error, on one scale shared by
all three data panels (colorbar on the far right). That is what ties the
panels together: the outlier the residual plot isolates is also the
darkest marker on the map, in the low-flow, low-pressure-ratio corner.
Pass error_color=False for flat white markers instead.
Two of the nine nodes sit outside the plotted region. contour_plot_2d
deliberately clamps the axes to the data bounds, so a node that drifts far
outside during training doesn't stretch the view and shrink the map.
Exporting a Lookup Table
After fitting, you can evaluate the model on a regular grid and export the results as a CSV lookup table for use in other programs (Excel, MATLAB, cycle-deck codes, etc.):
bounds = list(zip(X.min(axis=0), X.max(axis=0)))
grid = ebf.eval_grid(model, bounds, n_points=100)
ebf.export_grid(
"checkpoints/compressor_map_lookup.csv",
grid,
dim_names=['mdot', 'PR'],
)
This produces a 10 000-row CSV with columns Corrected Mass Flow,
Total Pressure Ratio, and prediction. See the
Visualization page for
NPZ export and other options.