.. _ode-building: =================== Building ODE Models =================== This page explains how to train NeurEco ODE models using the GUI, Python API, and CLI. .. contents:: On this page :local: :depth: 2 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 :ref:`dynamic-data-preparation` for details Building an ODE Model ===================== .. tabs:: .. tab:: GUI **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 :ref:`ode-steady-state`) 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 .. tab:: Python API **Setup** .. code-block:: python from NeurEco import NeurEcoDynamic as Dynamic import numpy as np # Initialize model model = Dynamic.ODE() **Build Configuration** Create an ``ODEBuildConfig`` with your parameters: .. code-block:: python # 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** .. code-block:: python status = model.build(config) if status == 0: print("Build completed successfully!") else: print(f"Build failed with status code: {status}") .. tab:: CLI **Configuration File** Create a JSON configuration file: .. code-block:: json { "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 :ref:`cli-reference` for all the fields. **Running the Build** .. code-block:: shell neureco ODE build config.json Parameter Reference =================== Training Data ------------- .. list-table:: :header-rows: 1 :widths: 25 15 60 * - 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 -------------------- .. list-table:: :header-rows: 1 :widths: 25 15 60 * - 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 ------------ .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Default - Description * - ``write_model_to`` - ``"model"`` - Path for final trained model * - ``checkpoint_address`` - ``"ckpt"`` - Path for intermediate checkpoints Normalization ------------- .. list-table:: :header-rows: 1 :widths: 30 15 55 * - 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 ------------------------ .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Default - Description * - ``steady_state_exc`` - ``None`` - 1-D array of input values at steady state, one per input (see :ref:`ode-steady-state`) * - ``steady_state_out`` - ``None`` - 1-D array of output values at steady state, one per output; give both arrays or neither Model Architecture ------------------ .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Default - Description * - ``min_hidden_states`` - ``1`` - Starting number of hidden states * - ``max_hidden_states`` - ``0`` - Maximum hidden states (0 = no limit) .. _ode-steady-state: 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: .. code-block:: python # 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 :ref:`ode-evaluating`). 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. .. _ode-choosing-steady-state: 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: .. code-block:: python 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** :math:`F(t)` — varies over time - **Stiffness parameter** :math:`k` — constant for each experiment, varies between experiments While :math:`F(t)` is naturally time-dependent, :math:`k` does not change during a trajectory. To use :math:`k` as an input to NeurEco, replicate its value across all time steps: .. math:: 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: .. tabs:: .. tab:: Python API .. code-block:: python 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** .. code-block:: python # 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] .. tab:: GUI / CSV Files In CSV files, the parametric column contains the same value repeated for every row: .. code-block:: text 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: .. code-block:: python 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: .. code-block:: python # 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 ================ .. code-block:: python 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 ========== - :ref:`Evaluate ODE models ` - :ref:`Export Dynamic models `