Mechanical TestBed#

This example demonstrates building a Convolutional Dynamic model using the Mechanical TestBed test case, which is included in the NeurEco installation package.

Note

The case ships as the MechanicalTestBed example (MechanicalTestBed.zip); its name in the example gallery is “Mechanical TestBed”. Its inputs are three forces (Primary, Control and Disturbance, in N) and a bench temperature (in °C) — i.e. different physical quantities.

Problem Description#

This test case models a mechanical test bench driven by three forces while its temperature varies. The goal is to predict the bench displacement from these inputs.

Inputs: Primary, Control and Disturbance forces (N), and the bench temperature (°C)

Outputs: Displacement (mm)

The dataset captures the nonlinear dynamic response of the bench under various operating conditions.

Test Case Data#

The test case includes:

  • Training: 4 trajectories

  • Validation: 2 trajectories

  • Test: 9 trajectories

Files are organized as:

Data/
|-- train/
|   |-- excitations_train0.csv
|   |-- outputs_train0.csv
|   |-- excitations_train1.csv
|   |-- outputs_train1.csv
|   `-- ...
|-- valid/
|   |-- excitations_valid0.csv
|   |-- outputs_valid0.csv
|   `-- ...
`-- test/
    |-- excitations_test0.csv
    |-- outputs_test0.csv
    `-- ...

Setup#

  1. Create a working directory (e.g., MechanicalTestBedExample)

  2. Extract the test case data into this directory

  3. Create a Python script (e.g., mechanical_testbed.py)

Complete Example#

"""
NeurEco Convolutional Dynamic Example: Mechanical TestBed

This example uses the Mechanical TestBed test case included
with the NeurEco installation.
"""

from NeurEco import NeurEcoDynamic as Dynamic
import numpy as np
import matplotlib.pyplot as plt
import os


def load_dataset(data_path, set_type):
    """
    Load excitation and output arrays (time in column 0) for a given dataset.

    Args:
        data_path: Path to the Data folder
        set_type: One of 'train', 'valid', or 'test'

    Returns:
        Tuple of (excitation_list, output_list), each array time-first
    """
    file_list = os.listdir(os.path.join(data_path, set_type))
    n_experiences = len(file_list) // 2  # Each experience has 2 files

    excs = []
    outs = []

    for exp in range(n_experiences):
        # Load excitations (column 0 is time, remaining columns are features)
        inputs_full = np.genfromtxt(
            f"{os.path.join(data_path, set_type)}/excitations_{set_type}{exp}.csv",
            skip_header=True,
            delimiter=","
        )
        excs.append(inputs_full)   # column 0 = time, remaining columns = excitations

        # Load outputs (column 0 is time, remaining columns are features)
        outputs_full = np.genfromtxt(
            f"{os.path.join(data_path, set_type)}/outputs_{set_type}{exp}.csv",
            skip_header=True,
            delimiter=","
        )
        outs.append(outputs_full)  # column 0 = time, remaining columns = outputs

    return excs, outs


# ===================
# Step 1: Load Data
# ===================

data_path = "Data"

excs_train, outs_train = load_dataset(data_path, "train")
excs_valid, outs_valid = load_dataset(data_path, "valid")
excs_test, outs_test = load_dataset(data_path, "test")

print(f"Training trajectories: {len(excs_train)}")
print(f"Validation trajectories: {len(excs_valid)}")
print(f"Test trajectories: {len(excs_test)}")


# ===================
# Step 2: Build Model
# ===================

builder = Dynamic.ConvolutiveDynamic()

# Each array: column 0 = time, remaining columns = excitations / outputs.
config = Dynamic.ConvolutiveDynamicBuildConfig(
    # Training data
    train_exc_list=excs_train,
    train_out_list=outs_train,
    # Validation data
    valid_exc_list=excs_valid,
    valid_out_list=outs_valid,
    # Test data
    test_exc_list=excs_test,
    test_out_list=outs_test,
    # Normalization: True — the inputs are different physical quantities
    # (three forces in N and a bench temperature in degC), so each feature
    # is normalized on its own range.
    inputs_normalize_per_feature=True,
    # Convolution window size
    window_size=32,
    # Output paths
    write_model_to="VibrationSensorModel/model",
    checkpoint_address="VibrationSensorModel/model",
)

status = builder.build(config)

if status == 0:
    print("Build completed successfully!")


# =====================
# Step 3: Load and Inspect Model
# =====================

evaluator = Dynamic.ConvolutiveDynamic()
evaluator.load("./VibrationSensorModel/model")

print(f"\nModel Information:")
print(f"  Inputs: {evaluator.get_input_count()}")
print(f"  Outputs: {evaluator.get_output_count()}")
print(f"  Window size: {evaluator.get_window_size()}")


# =====================
# Step 4: Evaluate Model
# =====================

# Each excitation array has time in its first column; predictions are returned time-first.
pred_train = evaluator.evaluate(excs_train)
pred_valid = evaluator.evaluate(excs_valid)
pred_test = evaluator.evaluate(excs_test)

# Get window size for alignment
w_size = evaluator.get_window_size()

# Compute errors (align reference with predictions)
print("\n=== Training Set Errors ===")
for i in range(len(pred_train)):
    ref_aligned = outs_train[i][w_size - 1:]
    error = evaluator.compute_error(pred_train[i][:, 1:], ref_aligned[:, 1:])
    print(f"Trajectory {i}: L2 error = {100*error:.4f}%")

print("\n=== Validation Set Errors ===")
for i in range(len(pred_valid)):
    ref_aligned = outs_valid[i][w_size - 1:]
    error = evaluator.compute_error(pred_valid[i][:, 1:], ref_aligned[:, 1:])
    print(f"Trajectory {i}: L2 error = {100*error:.4f}%")

print("\n=== Test Set Errors ===")
for i in range(len(pred_test)):
    ref_aligned = outs_test[i][w_size - 1:]
    error = evaluator.compute_error(pred_test[i][:, 1:], ref_aligned[:, 1:])
    print(f"Trajectory {i}: L2 error = {100*error:.4f}%")


# =====================
# Step 5: Visualize Results
# =====================

# Plot a selection of test trajectories
n_plots = min(4, len(excs_test))
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes = axes.flatten()

for i in range(n_plots):
    ax = axes[i]
    # Time is column 0 of the excitation array; the displacement output is column 1
    time_array = excs_test[i][:, 0]

    # Plot reference (full time range)
    ax.plot(time_array, outs_test[i][:, 1], 'r-', label="Reference")

    # Plot prediction (starts from window_size - 1)
    ax.plot(time_array[w_size - 1:], pred_test[i][:, 1], 'k--', label="Prediction")

    ax.set_xlabel("Time (s)")
    ax.set_ylabel("Displacement (mm)")
    ax.set_title(f"Test Trajectory {i}")
    ax.legend()
    ax.grid(True)

plt.suptitle("Mechanical TestBed: Convolutional Dynamic Model Predictions")
plt.tight_layout()
plt.show()


# =====================
# Cleanup
# =====================

builder.delete()
evaluator.delete()

Key Concepts Demonstrated#

Window Size

The Convolutional Dynamic model uses a sliding window to capture temporal dependencies. The window_size parameter controls how many past time steps influence each prediction:

window_size=32,

Larger windows can capture longer-range dependencies but increase model complexity.

Output Alignment

Because the model uses a sliding window, predictions start at time step window_size - 1. When comparing to reference data, you must align them:

w_size = evaluator.get_window_size()
ref_aligned = reference[w_size - 1:]
error = evaluator.compute_error(prediction[:, 1:], ref_aligned[:, 1:])

Per-Feature Normalization

The inputs are different physical quantities — three forces (Primary, Control and Disturbance, in N) and a bench temperature (in degC). Following the Data Preparation guidance, we enable per-feature normalization so each feature is scaled on its own range:

inputs_normalize_per_feature=True,

Multiple Trajectories

The model is trained on multiple trajectories simultaneously. This helps it generalize across different operating conditions:

train_exc_list=excs_train, # List of 4 excitation arrays (time in column 0)
train_out_list=outs_train, # List of 4 output arrays (time in column 0)

Exploring Checkpoints#

During training, NeurEco saves intermediate models. You can inspect these to find smaller models that may provide acceptable accuracy:

model = Dynamic.ConvolutiveDynamic()

n = model.get_number_of_networks_from_checkpoint("./VibrationSensorModel/model")
print(f"Found {n} checkpointed models")

for i in range(n):
    model.load_model_from_checkpoint("./VibrationSensorModel/model", i)
    print(f"Checkpoint {i + 1}: {model.vec.size} trainable parameters")

Next Steps#

  • Experiment with different window_size values to balance accuracy and complexity

  • Export the model to FMU format (see Exporting Dynamic Models)