Building ODE Models#

This page explains how to train NeurEco ODE models using the GUI, Python API, and CLI.

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

  1. Launch NeurEco

  2. Select Dynamic > ODE

  3. Click New Project

Loading Data

Training data:

  1. In the Build panel, click + next to Training files

  2. Select your input (excitation) file

  3. Select the corresponding output file

  4. 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

  1. Click BUILD

  2. Monitor training progress in the Training panel

  3. Checkpoints are saved automatically

  4. Training completes when no further improvement is found

Parameter Reference#

Training Data#

Parameter

Required

Description

train_exc_list

Yes

List of 2-D arrays; column 0 is time, remaining columns are input signals

train_out_list

Yes

List of 2-D arrays; column 0 is time, remaining columns are target outputs

Validation/Test Data#

Parameter

Default

Description

valid_exc_list

None

Validation input arrays (time in the first column)

valid_out_list

None

Validation output arrays (time in the first column)

test_exc_list

None

Test input arrays (time in the first column)

test_out_list

None

Test output arrays (time in the first column)

valid_percentage

30

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

write_model_to

"model"

Path for final trained model

checkpoint_address

"ckpt"

Path for intermediate checkpoints

Normalization#

Parameter

Default

Description

inputs_scaling

"l2"

Input scaling: "none", "l2", "max"

inputs_shifting

"mean"

Input shifting: "none", "mean"

outputs_scaling

"l2"

Output scaling: "none", "l2", "max"

outputs_shifting

"mean"

Output shifting: "none", "mean"

inputs_normalize_per_feature

True

Normalize each input feature independently

outputs_normalize_per_feature

True

Normalize each output feature independently

Steady-State Constraints#

Parameter

Default

Description

steady_state_exc

None

1-D array of input values at steady state, one per input (see Using Steady-State Constraints)

steady_state_out

None

1-D array of output values at steady state, one per output; give both arrays or neither

Model Architecture#

Parameter

Default

Description

min_hidden_states

1

Starting number of hidden states

max_hidden_states

0

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_exc keep the model at rest with outputs steady_state_out, and get_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 equal steady_state_exc is it the given steady state, with first predicted outputs equal to steady_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:

\[k(t) = k \quad \text{for } t_{\text{start}} \leq t \leq t_{\text{end}}\]

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]

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()}")

Next Steps#