Tutorial: using NeurEco Python API for a Discrete Dynamic problem#

The following section uses two test cases:

These test cases are included in the NeurEco installation package.

Discrete Dynamic proposes various settings for the build and the evaluation. This tutorial is divided into two parts: Build NeurEco Discrete Dynamic model with the Python API and Evaluate NeurEco Discrete Dynamic model with the Python API.

Building a discrete dynamic model#

There are two main options to build a Discrete Dynamic model:

  • with a validation percentage, the validation data is chosen from the training data by NeurEco

  • with validation data, the validation data is set manually.

For each option, the build can be done without selecting any of the advanced settings (steady state and hidden state, see Build NeurEco Discrete Dynamic model with the Python API), or with these settings provided by the user.

Create an empty directory (TemperatureForecasting Example), extract the Temperature forecasting test case data there. The created directory contains the following files:

  • x_first_year.npy

  • x_second_year.npy

  • y_first_year.npy

  • y_second_year.npy

Simple build without validation data#

To build a model without any of the advanced settings and without manually setting validation data:

  • Import the required libraries (NeurEco and numpy):

from NeurEco import NeurEcoDynamic as Dynamic
import numpy as np
  • Load the data:

x_train = np.load("x_first_year.npy")
t_train = x_train[:, 0:1]
x_train = x_train[:, 1:]
y_train = np.load("y_first_year.npy")
y_train = y_train[:, 1:]
x_year_2 = np.load("x_second_year.npy")
t_year_2 = x_year_2[:, 0:1]
x_year_2 = x_year_2[:, 1:]
y_year_2 = np.load("y_second_year.npy")
y_year_2 = y_year_2[:, 1:]
  • Initialize a NeurEco object to handle the Discrete Dynamic problem:

simple_builder_1= Dynamic.DiscreteDynamic()

All the methods provided by the DiscretDynamic class, can be viewed by calling the __method__ attributes:

print(simple_builder_1.__methods__)
**** NeurEco Dynamic DiscreteDynamic methods: ****
- load
- save
- delete
- evaluate
- build
- get_input_count
- get_output_count
- load_model_from_checkpoint
- get_number_of_networks_from_checkpoint
- get_weights
- export_fmu
- compute_error

To understand what each parameter of any method does and how to use it print the doc of the method:

print(simple_builder_1.export_fmu.__doc__)
exports a neureco model to FMU (Functional Mock-up Interface)
:param fmu_path: string : path where to save the fmu file
:return: export_status: int: 0 if export is successful, other int if no
  • To start the build, run the build method with the building parameters adjusted to the problem at hand (see Build NeurEco Discrete Dynamic model with the Python API). For this example, the validation percentage is set to 30%. Meaning that NeurEco will use the last 30% of the training trajectory as validation data. For example, if the training trajectory contains 1000 time steps, the first 700 steps will be used for training and the last 300 steps will be used for validation.

simple_builder_1.build(train_time_list=[t_train], train_exc_list=[x_train], train_out_list=[y_train],
                   valid_percentage=30,
                   # the rest of these parameters are optional
                   write_model_to="./TemperatureForecasting/TemperatureForecasting_simple1.ernn",
                   checkpoint_address="./TemperatureForecasting/TemperatureForecasting_simple1.checkpoint")

Note

The data (excitations, time and outputs) are always provided as lists. If there are multiple separate experiences (trajectories), the user should avoid stacking such data, and pass a list of arrays instead (each experience is an array).

During the build NeurEco saves the intermediate modes to the checkpoint file (defined by the parameter checkpoint_address). To load and use the intermediate models from this checkpoint:

model = Dynamic.DiscreteDynamic()
n = model.get_number_of_networks_from_checkpoint("./TemperatureForecasting/TemperatureForecasting_simple1.checkpoint")
for i in range(n):
    model.load_model_from_checkpoint("./TemperatureForecasting/TemperatureForecasting_simple1.checkpoint", i)
    n_inputs = model.get_input_count()
    n_outputs = model.get_output_count()
    print("N° Inputs:", n_inputs)
    print("N° Outputs:", n_outputs)
00h00m00s info > "Checkpoint ./TemperatureForecasting/TemperatureForecasting_simple1.checkpoint" successfully loaded.
N° Inputs: 13
N° Outputs: 1
00h00m00s info > "Checkpoint ./TemperatureForecasting/TemperatureForecasting_simple1.checkpoint" successfully loaded.
N° Inputs: 13
N° Outputs: 1
...

Simple build with validation data#

In this part, for the same test case, Temperature forecasting, instead of using 1 year of measurement as training and one year as testing, the second year is used as validation data.

  • Create the validation data:

nb_valid_tsteps = x_year_2.shape[0] // 2
t_valid = t_year_2[nb_valid_tsteps:]
t_test = t_year_2[:nb_valid_tsteps]

x_valid = x_year_2[nb_valid_tsteps:, :]
x_test = x_year_2[:nb_valid_tsteps, :]

y_valid = y_year_2[nb_valid_tsteps:, :]
y_test = y_year_2[:nb_valid_tsteps, :]
  • Create a new NeurEco object to handle the Discrete Dynamic problem:

simple_builder_2= Dynamic.DiscreteDynamic()
  • Call the build method with the validation data provided explicitly:

simple_builder_2.build(train_time_list=[t_train], train_exc_list=[x_train], train_out_list=[y_train],
                   valid_time_list=[t_valid], valid_exc_list=[x_valid], valid_out_list=[y_valid],
                   # the rest of these parameters are optional
                   write_model_to="./TemperatureForecasting/TemperatureForecasting_simple2.ernn",
                   checkpoint_address="./TemperatureForecasting/TemperatureForecasting_simple2.checkpoint")

During the build NeurEco saves the intermediate modes to the checkpoint file (defined by the parameter checkpoint_address). As before, it is possible to load and explore the intermediate models from this checkpoint.

Advanced build#

This part uses Nonlinear oscillator and illustrates the usage of the advanced parameters (see Build NeurEco Discrete Dynamic model with the Python API):

  • Steady state: (steady_state_exc and steady_state_out)

  • Hidden state: (min_hidden_state, max_hidden_state)

Create an empty directory (NonLinearOscillator Example), and extract the Nonlinear oscillator data there. The data files are inside the directories “data/learn”, “data/valid” and “data/test”.

To build the model:

  • Import the required libraries: NeurEco, os and numpy:

from NeurEco import NeurEcoDynamic as Dynamic
import numpy as np
import os
  • Load the data:

train_directory = "./data/train"
valid_directory = "./data/valid"
test_directory = "./data/test"

# Training data
train_excitations = []
train_times = []
train_outputs = []
train_file_names = os.listdir(train_directory)
for file_name in train_file_names:
    print(">> Loading the data from the file {0} in the Training Directory".format(file_name))
    data = np.genfromtxt(os.path.join(train_directory, file_name), skip_header=True, delimiter=",")
    if "exc" in file_name:
        train_excitations.append(data[:, 1:])
        train_times.append(data[:, 0:1])
    elif "out" in file_name:
        train_outputs.append(data[:, 1:])

# Validation data
valid_excitations = []
valid_times = []
valid_outputs = []
valid_file_names = os.listdir(valid_directory)
for file_name in valid_file_names:
    print(">> Loading the data from the file {0} in the Validation Directory".format(file_name))
    data = np.genfromtxt(os.path.join(valid_directory, file_name), skip_header=True, delimiter=",")
    if "exc" in file_name:
        valid_excitations.append(data[:, 1:])
        valid_times.append(data[:, 0:1])
    elif "out" in file_name:
        valid_outputs.append(data[:, 1:])

# Testing Data
test_excitations = []
test_times = []
test_outputs = []
test_file_names = os.listdir(test_directory)
for file_name in test_file_names:
    print(">> Loading the data from the file {0} in the Testing Directory".format(file_name))
    data = np.genfromtxt(os.path.join(test_directory, file_name), skip_header=True, delimiter=",")
    if "exc" in file_name:
        test_excitations.append(data[:, 1:])
        test_times.append(data[:, 0:1])
    elif "out" in file_name:
        test_outputs.append(data[:, 1:])
  • Initialize a NeurEco object to handle the Discrete Dynamic problem:

builder = Dynamic.DiscreteDynamic()

From the equation governing the outputs, one can see that a stationary state (see Build NeurEco Discrete Dynamic model with the Python API) is described by the excitation set to \(0\) and thus the corresponding output to \(0\):

  • Set steady_state_exc to \(0\)

  • Set steady_state_out to \(0\)

Note

The Discrete Dynamic model supports providing of only one steady state of the model.

The governing equation is of the second degree, which could imply that two hidden states (see Build NeurEco Discrete Dynamic model with the Python API) are sufficient to describe the system. Here, one more hidden state is added to take the non linearity into account:

  • Set max_hidden_states to \(3\)

  • Call the build method:

builder.build(train_time_list=train_times,
          train_exc_list=train_excitations,
          train_out_list=train_outputs,
          valid_time_list=valid_times,
          valid_exc_list=valid_excitations,
          valid_out_list=valid_outputs,
          steady_state_exc=np.array([0]),
          steady_state_out=np.array([0]),
          min_hidden_states=1,
          max_hidden_states=3,
          checkpoint_address="./NLOscillator/model.checkpoint",
          write_model_to="./NLOscillator/model.ernn",
          inputs_normalize_per_feature=True,
          outputs_normalize_per_feature=True)

Once the build ended, the created model can be loaded and explored:

evaluator = Dynamic.DiscreteDynamic()

load_state = evaluator.load("./NLOscillator/model.ernn")
if load_state == 0:
    print("Loading state = Success")
else:
    print("Loading state = Fail")
    raise RuntimeError("Error loading the NeurEco dynamic model")

n_inputs = evaluator.get_input_count()
n_outputs = evaluator.get_output_count()
n_init_time_steps = evaluator.get_number_of_init_tsteps()
steady_exc = evaluator.get_steady_input()
steady_out = evaluator.get_steady_output()
print("N° Inputs:", n_inputs)
print("N° Outputs:", n_outputs)
print("N° Initialization time steps:", n_init_time_steps)
print("N° hidden state:", evaluator.get_state_count())
print("Steady state excitations:",steady_exc)
print("Steady state outputs:", steady_out)
Loading state = Success
N° Inputs: 1
N° Outputs: 1
N° Initialization time steps: 3
N° hidden state: 3
Steady state excitations: [0.]
Steady state outputs: [3.46944695e-18]

Evaluating a discrete dynamic model in python#

There are two options to evaluate a discrete dynamic model: with and without initial conditions (see Evaluate NeurEco Discrete Dynamic model with the Python API).

Evaluate a model without initial conditions#

  • load an already created model, here the one created in the previous section for the test case Temperature forecasting.

  • Call the evaluate method:

evaluator = Dynamic.DiscreteDynamic()
load_state = evaluator.load("./TemperatureForecasting/TemperatureForecasting_simple1.ernn")
if load_state == 0:
   print("Loading state = Success")
else:
   raise RuntimeError("Error loading the neureco model")

neureco_outputs_train = evaluator.evaluate([t_train], [x_train])
neureco_outputs_test = evaluator.evaluate([t_year_2], [x_year_2])
  • To plot the prediction and the measured data for all the sets in a continuous way (matplotlib library is required):

import matplotlib.pyplot as plt
nb_training_tsteps = int(x_train.shape[0] * 70 / 100)
plt.figure(1)
plt.plot(np.vstack((t_train, t_year_2)), np.vstack((y_train, y_year_2)), label="measured data", marker='.', markersize=3, linestyle="none")
plt.plot(t_train[:nb_training_tsteps], neureco_outputs_train[0][:nb_training_tsteps, :], label="training prediction")
plt.plot(t_train[nb_training_tsteps:], neureco_outputs_train[0][nb_training_tsteps:, :], label="validation prediction")
plt.plot(t_year_2, neureco_outputs_test[0], label="test prediction")
plt.legend()
plt.title("Simple Build 1: Automatic Validation Data Selection")
plt.show()
TempForecastPythonEvalWithoutInit

python API operations: evaluating a model without initial condition: test case - Temperature forecasting#

  • To perform a sensitivity analysis using the built model without initialization data:

sensitivity_array = evaluator.sensitivity(time=t_test, excitations=x_test, id_output=0)
x_ticks = ["input " + str(i) for i in range(sensitivity_array.size)]
plt.figure(2)
plt.bar(x_ticks, sensitivity_array[0, :], color="darkorange")
plt.grid()
plt.ylabel("Sensitivity")
plt.title("Simple build 1 -- Sensitivity analysis")
plt.show()
TempForecastPythonSensitivityWithoutInit

python API operations: Performing a sensitivity analysis without initial condition: test case - Temperature forecasting#

Evaluate a model with the explicit initialization#

  • load an already created model, here the one created in the previous section for the test case Temperature forecasting:

evaluator = Dynamic.DiscreteDynamic()
load_state = evaluator.load("./TemperatureForecasting/TemperatureForecasting_simple1.ernn")
if load_state == 0:
   print("Loading state = Success")
else:
   raise RuntimeError("Error loading the neureco model")
  • Get the number of initialization steps deduced during the build:

n_init_steps = evaluator.get_number_of_init_tsteps()
  • Use this number to create initialization arrays to evaluate the training set and the testing set:

t_train_init = t_train[:n_init_steps, :]
x_train_init = x_train[:n_init_steps, :]
y_train_init = y_train[:n_init_steps, :]
t_year_2_init = t_year_2[:n_init_steps, :]
x_year_2_init = x_year_2[:n_init_steps, :]
y_year_2_init = y_year_2[:n_init_steps, :]
  • Evaluate the model using these initialization arrays:

neureco_outputs_train2 = evaluator.evaluate([t_train], [x_train], initialization_excitations_arrays_list=[x_train_init],
                                        initialization_outputs_arrays_list=[y_train_init],
                                        initialization_time_arrays_list=[t_train_init])[0]
neureco_outputs_test2 = evaluator.evaluate([t_year_2], [x_year_2], initialization_excitations_arrays_list=[x_year_2_init],
                                        initialization_outputs_arrays_list=[y_year_2_init],
                                        initialization_time_arrays_list=[t_year_2_init])[0]
  • Plot the prediction and the measured data for all the sets in a continuous way (matplotlib library is required):

import matplotlib.pyplot as plt
neureco_outputs_train2 = evaluator.evaluate([t_train], [x_train],     initialization_excitations_arrays_list=[x_train_init],
                                            initialization_outputs_arrays_list=[y_train_init],
                                            initialization_time_arrays_list=[t_train_init])
neureco_outputs_test2 = evaluator.evaluate([t_year_2], [x_year_2], initialization_excitations_arrays_list=[x_year_2_init],
                                            initialization_outputs_arrays_list=[y_year_2_init],
                                            initialization_time_arrays_list=[t_year_2_init])

nb_training_tsteps = int(x_train.shape[0] * 70 / 100)
plt.figure(1)
plt.plot(np.vstack((t_train, t_year_2)), np.vstack((y_train, y_year_2)), label="measured data", marker='.', markersize=3, linestyle="none")
plt.plot(t_train[:nb_training_tsteps], neureco_outputs_train2[0][:nb_training_tsteps, :], label="training prediction")
plt.plot(t_train[nb_training_tsteps:], neureco_outputs_train2[0][nb_training_tsteps:, :], label="validation prediction")
plt.plot(t_year_2, neureco_outputs_test2[0], label="test prediction")

plt.legend()
plt.title("Simple Build 1: Automatic Validation Data Selection -- Evaluation with initialization")
plt.show()
TempForecastPythonEvalWithInit

python API operations: evaluating a model with initial condition: test case - Temperature forecasting#

  • Perform a sensitivity analysis using the built model with initialization data:

n_init = evaluator.get_number_of_init_tsteps()
exc_init = x_test[:n_init, :]
time_init = t_test[:n_init]
out_init = y_test[:n_init, :]
time_ = t_test[n_init-1:]
excs_ = x_test[n_init-1:, :]

sensitivity_array = evaluator.sensitivity(time=time_, excitations=excs_, id_output=0,
                                              initialization_excitations=exc_init, initialization_time=time_init,
                                              initialization_outputs=out_init)
x_ticks = ["input " + str(i) for i in range(sensitivity_array.size)]
plt.figure(2)
plt.bar(x_ticks, sensitivity_array[0, :], color="darkorange")
plt.grid()
plt.ylabel("Sensitivity")
plt.title("Simple build 2 -- Sensitivity analysis")
plt.show()
TempForecastPythonSensitivityWithInit

python API operations: Performing a sensitivity analysis with initial condition: test case - Temperature forecasting#

Exporting a Discrete Dynamic model#

To export the model as an FMU file:

evaluator.export_fmu("./TemperatureForecasting/TemperatureForecasting_simple1.fmu")

This export requires embed license.

Warning

Once the NeurEco object is no longer needed, free the memory by deleting the object by calling the delete method. For the example above, five objects must be deleted:

builder.delete()
model.delete()
simple_builder1.delete()
simple_builder2.delete()
evaluator.delete()