Building ODE Models
Contents
Building ODE Models#
This page explains how to train NeurEco ODE models using the GUI, Python API, and CLI.
On this page
Prerequisites#
Before building, ensure your data is properly formatted:
Time-series data — uniform or non-uniform time steps are both supported, as long as each time array is sorted in strictly increasing order
Excitation (input) and output arrays
See Data Preparation for details
Building an ODE Model#
Starting a New Project
Launch NeurEco
Select Dynamic > ODE
Click New Project
Loading Data
Training data:
In the Build panel, click + next to Training files
Select your input (excitation) file
Select the corresponding output file
Repeat for additional sequences/experiments
Validation data (optional):
Add files under Validation files
If not provided, NeurEco uses a percentage of training data
Test data (optional):
Add files under Test files
Used only for final evaluation, not during training
Build Parameters
Validation split:
Set Valid percentage (default: 30%)
Only used if validation data not provided separately
Normalization:
Inputs scaling/shifting: How to normalize input features
Outputs scaling/shifting: How to normalize output features
Per-feature normalization: Normalize each feature independently
Model size control:
Min hidden states: Starting number for topology search
Max hidden states: Upper limit (0 = no limit)
Steady state (optional):
An equilibrium of the system: under constant excitations equal to the steady input, the system rests and its outputs equal the steady output
The model is built with exactly this equilibrium. An evaluation without initialization data whose excitations start at the steady input starts from it, its first predicted outputs equal to the steady output; with other first excitations, it starts from the equilibrium the model learned for them (see Using Steady-State Constraints)
The two lists steady input and steady output are under Advanced settings. To fill them:
Once the training files are chosen, steady input has one row per input variable and steady output one row per output variable, in the column order of the files (time column excluded). Each row is labelled with the name of its variable, taken from the header of the training CSV files
Give a value to every variable of both lists, or leave both lists empty. An empty cell means no value
Select a row and type its value. Enter or Tab goes on to the next variable, and a double-click edits a value in place
Or copy the values and click Paste. With no row selected, the pasted values replace all the values; with a row selected, they replace the values from that row on. A paste with more values than variables is refused
Clear empties all the values, or only those of the selected rows. The Delete key empties the selected rows
Paste accepts cells copied from Excel (a row or a column), or numbers separated by spaces, tabs, new lines, commas or semicolons, with or without brackets, for example 1.0, 2.5, 3 or [1.0 2.5 3.0]. The decimal point is a dot (.). The only exception is a copy from Excel (values separated by tabs or new lines, with no semicolon): there a cell such as 2,5 is read as 2.5. Anywhere else a comma separates two values, so 1,5 2 gives the three values 1, 5 and 2. If any value is not a number, nothing is pasted and a short message says so.
Training files without a header (or .npy / .mat files) give no variable names: the lists then have no rows until values are pasted, and the rows are numbered.
A message under a list points out what the build would refuse: a variable with no value, a value past the last variable (for example a list from a project whose training files had more columns), or a list left empty while the other one is filled. When a model is loaded with only one of the two lists filled, the status bar shows a warning too.
Building
Click BUILD
Monitor training progress in the Training panel
Checkpoints are saved automatically
Training completes when no further improvement is found
Setup
from NeurEco import NeurEcoDynamic as Dynamic
import numpy as np
# Initialize model
model = Dynamic.ODE()
Build Configuration
Create an ODEBuildConfig with your parameters:
# Each array: column 0 = time, remaining columns = excitations / outputs.
config = Dynamic.ODEBuildConfig(
# Required: Training data
train_exc_list=train_exc_list,
train_out_list=train_out_list,
# Optional: Validation data
valid_exc_list=None,
valid_out_list=None,
# Optional: Test data
test_exc_list=None,
test_out_list=None,
# Validation split (if validation data not provided)
valid_percentage=30,
# Output paths
write_model_to="./model/ode_model.ernn",
checkpoint_address="./model/ode_checkpoint",
# Normalization
inputs_scaling="l2",
inputs_shifting="mean",
outputs_scaling="l2",
outputs_shifting="mean",
inputs_normalize_per_feature=True,
outputs_normalize_per_feature=True,
# Steady-state constraints (optional)
steady_state_exc=None,
steady_state_out=None,
# Model architecture
min_hidden_states=1,
max_hidden_states=0, # 0 = no limit
)
Building the Model
status = model.build(config)
if status == 0:
print("Build completed successfully!")
else:
print(f"Build failed with status code: {status}")
Configuration File
Create a JSON configuration file:
{
"neurecoODE_build": {
"exc_filenames": [
"data/train/train_exc_1.csv",
"data/train/train_exc_2.csv"
],
"output_filenames": [
"data/train/train_out_1.csv",
"data/train/train_out_2.csv"
],
"validation_exc_filenames": [
"data/valid/valid_exc_1.csv"
],
"validation_output_filenames": [
"data/valid/valid_out_1.csv"
],
"settings": {
"valid_percentage": 30,
"min_hidden_states": 1,
"max_hidden_states": 3,
"input_normalization": {
"shift_type": "mean",
"scale_type": "l2",
"normalize_per_feature": true
},
"output_normalization": {
"shift_type": "mean",
"scale_type": "l2",
"normalize_per_feature": true
}
},
"write_model_to": "./model/ode_model.ernn",
"checkpoint_address": "./model/ode_checkpoint.checkpoint",
"write_model_output_to_directory": "./model/BuildingResults"
}
}
The build parameters, including the normalization and the optional
steady_state_exc / steady_state_out lists, sit under settings.
max_hidden_states is 0 for no limit. The file names follow the layout of
TutorialCases/NonLinearOscillator. See CLI Reference for all the
fields.
Running the Build
neureco ODE build config.json
Parameter Reference#
Training Data#
Parameter |
Required |
Description |
|---|---|---|
|
Yes |
List of 2-D arrays; column 0 is time, remaining columns are input signals |
|
Yes |
List of 2-D arrays; column 0 is time, remaining columns are target outputs |
Validation/Test Data#
Parameter |
Default |
Description |
|---|---|---|
|
|
Validation input arrays (time in the first column) |
|
|
Validation output arrays (time in the first column) |
|
|
Test input arrays (time in the first column) |
|
|
Test output arrays (time in the first column) |
|
|
Percentage of training data for validation |
Note
Deprecated separate-time parameters. train_time_list,
valid_time_list and test_time_list (lists of 1-D time arrays passed
alongside pure excitation/output arrays) are still accepted for backward
compatibility but emit a DeprecatedParameterWarning. Prefer placing the
time in the first column of each excitation and output array.
Output Paths#
Parameter |
Default |
Description |
|---|---|---|
|
|
Path for final trained model |
|
|
Path for intermediate checkpoints |
Normalization#
Parameter |
Default |
Description |
|---|---|---|
|
|
Input scaling: |
|
|
Input shifting: |
|
|
Output scaling: |
|
|
Output shifting: |
|
|
Normalize each input feature independently |
|
|
Normalize each output feature independently |
Steady-State Constraints#
Parameter |
Default |
Description |
|---|---|---|
|
|
1-D array of input values at steady state, one per input (see Using Steady-State Constraints) |
|
|
1-D array of output values at steady state, one per output; give both arrays or neither |
Model Architecture#
Parameter |
Default |
Description |
|---|---|---|
|
|
Starting number of hidden states |
|
|
Maximum hidden states (0 = no limit) |
Using Steady-State Constraints#
A steady state is an equilibrium of the system: under the constant excitations steady_state_exc, the system
stays at rest and its outputs equal steady_state_out. Give it when you know one, for example the system at
rest:
# At rest under the excitations [0, 0], the outputs are [1, 0]
config = Dynamic.ODEBuildConfig(
# ... other parameters ...
steady_state_exc=np.array([0.0, 0.0]),
steady_state_out=np.array([1.0, 0.0]),
)
NeurEco then builds a model that has exactly this equilibrium, and the steady state plays three roles:
The model rests there. Excitations held at
steady_state_exckeep the model at rest with outputssteady_state_out, andget_steady_input()/get_steady_output()return this pair.Evaluations without initialization data start at rest. An evaluation without
init_outputs, a command-line evaluation without initialization files, Steady State Initialization in the GUI Metrics panel and an FMU run without initialization data all start from the model’s equilibrium for the excitation values at their first time (see Evaluating ODE Models). Only when these values equalsteady_state_excis it the given steady state, with first predicted outputs equal tosteady_state_out. For other values it is the equilibrium the model learned for them, which the build does not constrain.Training trajectories that start at rest start from it. A training trajectory whose first sample equals the steady state starts from the model’s equilibrium. Every other trajectory gets its own initial state, computed from its data.
Give both lists, with one value per variable in the column order of the data files (time column excluded), or neither: the build refuses a single list or a list of the wrong length.
Choosing the steady state#
Give a true equilibrium of the system, for example the system at rest: zero excitation and the outputs it then settles to. A pair the system never rests at forces a false equilibrium into the model and degrades it.
Only one equilibrium can be given. If the system rests at several operating points, give the one around which the model will be used: evaluations without initialization data and FMU runs start at the model’s equilibrium for their first excitation values, which is the given one only when they start at
steady_state_exc.The training trajectories do not have to start from it. Only those that start at it start from the model’s equilibrium; NeurEco computes the initial state of the others from their data.
If you know no equilibrium, or the system has none (for example a self-sustained oscillation), leave both lists empty. NeurEco then takes the input normalization shift as the steady input and learns the matching output;
get_steady_input()/get_steady_output()return that equilibrium of the model.If the real system does not start at rest, give initialization data at evaluation, so the evaluation starts from the right state instead of the equilibrium.
Controlling Model Size#
For deployment on embedded systems or when you need faster inference:
config = Dynamic.ODEBuildConfig(
# ... other parameters ...
min_hidden_states=2, # Start with 2 hidden states
max_hidden_states=10, # Don't exceed 10 hidden states
)
This creates a trade-off between model accuracy and size.
Parametric Models#
Many physical systems have inputs that vary between experiments but remain constant within each trajectory. NeurEco ODE handles these parametric inputs by treating them as time-dependent signals with replicated values.
Understanding Parametric Inputs#
Consider a mass-spring-damper system with two inputs:
External force \(F(t)\) — varies over time
Stiffness parameter \(k\) — constant for each experiment, varies between experiments
While \(F(t)\) is naturally time-dependent, \(k\) does not change during a trajectory. To use \(k\) as an input to NeurEco, replicate its value across all time steps:
Common Use Cases#
Parametric inputs are useful for modeling systems where certain parameters:
Material properties: stiffness, damping coefficients, thermal conductivity
Geometric parameters: length, area, volume, thickness
Operating conditions: temperature setpoints, pressure levels, flow rates
Design variables: component sizes, configuration parameters
Preparing Parametric Data#
For each trajectory, create the parametric input column by replicating the constant value:
import numpy as np
# Time steps for the trajectory
time = np.arange(0, 10, 0.01) # 1000 time steps
n_steps = len(time)
# Time-varying input: external force
force = np.sin(2 * np.pi * 0.5 * time)
# Parametric input: stiffness (constant for this trajectory)
k_value = 2.152
stiffness = np.full(n_steps, k_value) # Replicate across all time steps
# Combine into excitation array
# Column 0: time, Column 1: stiffness (parametric), Column 2: force (time-varying)
excitation = np.column_stack([time, stiffness, force])
print(f"Excitation shape: {excitation.shape}") # (1000, 3)
Multiple Trajectories with Different Parameter Values
# Trajectory 1: k = 1.5
time_1 = np.arange(0, 10, 0.01)
k_1 = np.full(len(time_1), 1.5)
force_1 = np.sin(2 * np.pi * 0.5 * time_1)
exc_1 = np.column_stack([time_1, k_1, force_1])
out_1 = ... # System response (time in column 0)
# Trajectory 2: k = 2.0
time_2 = np.arange(0, 8, 0.01)
k_2 = np.full(len(time_2), 2.0)
force_2 = np.sin(2 * np.pi * 0.3 * time_2)
exc_2 = np.column_stack([time_2, k_2, force_2])
out_2 = ... # System response (time in column 0)
# Trajectory 3: k = 2.5
time_3 = np.arange(0, 12, 0.01)
k_3 = np.full(len(time_3), 2.5)
force_3 = np.sin(2 * np.pi * 0.7 * time_3)
exc_3 = np.column_stack([time_3, k_3, force_3])
out_3 = ... # System response (time in column 0)
# Each array: column 0 = time, remaining columns = excitations / outputs.
train_exc_list = [exc_1, exc_2, exc_3]
train_out_list = [out_1, out_2, out_3]
In CSV files, the parametric column contains the same value repeated for every row:
time;stiffness;force
0.00;2.152;0.000
0.01;2.152;0.031
0.02;2.152;0.063
0.03;2.152;0.094
0.04;2.152;0.125
...
Each experiment file has the same structure, but with different constant values for the parametric input.
Building with Parametric Data#
Once the data is prepared with replicated parametric values, building proceeds normally:
from NeurEco import NeurEcoDynamic as Dynamic
model = Dynamic.ODE()
# Each array: column 0 = time, remaining columns = excitations / outputs.
config = Dynamic.ODEBuildConfig(
train_exc_list=train_exc_list,
train_out_list=train_out_list,
valid_exc_list=valid_exc_list,
valid_out_list=valid_out_list,
write_model_to="./model/parametric_ode.ernn",
)
status = model.build(config)
Evaluating Parametric Models#
During evaluation, provide the parametric values in the same replicated format:
# New experiment with k = 1.8
eval_time = np.arange(0, 5, 0.01)
k_new = np.full(len(eval_time), 1.8) # New parameter value
force_new = np.sin(2 * np.pi * 0.4 * eval_time)
eval_exc = np.column_stack([eval_time, k_new, force_new])
# Evaluate
# eval_exc has time in its first column; predictions are returned time-first.
predictions = model.evaluate([eval_exc])
Tip
The model learns how the system behavior depends on both time-varying inputs and parametric values. After training, you can evaluate the model with any parameter values within the training range.
Complete Example#
import numpy as np
from NeurEco import NeurEcoDynamic as Dynamic
# Prepare training data (time in column 0)
dt = 0.01
t_train = np.arange(0, 10, dt)
train_exc_list = [np.column_stack([t_train, np.random.randn(len(t_train), 3)])]
train_out_list = [np.column_stack([t_train, np.random.randn(len(t_train), 2)])]
# Prepare validation data (time in column 0)
t_valid = np.arange(0, 5, dt)
valid_exc_list = [np.column_stack([t_valid, np.random.randn(len(t_valid), 3)])]
valid_out_list = [np.column_stack([t_valid, np.random.randn(len(t_valid), 2)])]
# Initialize model
model = Dynamic.ODE()
# Configure build
# Each array: column 0 = time, remaining columns = excitations / outputs.
config = Dynamic.ODEBuildConfig(
train_exc_list=train_exc_list,
train_out_list=train_out_list,
valid_exc_list=valid_exc_list,
valid_out_list=valid_out_list,
write_model_to="./model/my_ode_model.ernn",
checkpoint_address="./model/my_ode_checkpoint",
max_hidden_states=5,
)
# Build the model
status = model.build(config)
if status == 0:
print("Model trained successfully!")
print(f"Number of hidden states: {model.get_state_count()}")