• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tonegas / nnodely / 14268084640

04 Apr 2025 02:51PM UTC coverage: 97.035% (+0.04%) from 96.995%
14268084640

push

github

web-flow
Merge pull request #82 from tonegas/develop

Added some new features:

1. the import from pandas dataframe with resample feature
2. new example files for each layers
3. categorical loss

407 of 430 new or added lines in 20 files covered. (94.65%)

7 existing lines in 2 files now uncovered.

11453 of 11803 relevant lines covered (97.03%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.87
/nnodely/nnodely.py
1
# Extern packages
2
import random, torch, copy, os
1✔
3
import numpy as np
1✔
4
import pandas as pd
1✔
5
import pandas.api.types as ptypes
1✔
6

7
# nnodely packages
8
from nnodely.visualizer import TextVisualizer, Visualizer
1✔
9
from nnodely.loss import CustomLoss
1✔
10
from nnodely.model import Model
1✔
11
from nnodely.optimizer import Optimizer, SGD, Adam
1✔
12
from nnodely.exporter import Exporter, StandardExporter
1✔
13
from nnodely.modeldef import ModelDef
1✔
14
from nnodely.relation import NeuObj
1✔
15

16
from nnodely.utils import check, argmax_dict, argmin_dict, tensor_to_list, TORCH_DTYPE, NP_DTYPE, check_gradient_operations
1✔
17

18
from nnodely.logger import logging, nnLogger
1✔
19
log = nnLogger(__name__, logging.INFO)
1✔
20

21

22
def clearNames(names:str|None = None):
1✔
23
    NeuObj.clearNames(names)
1✔
24

25
class Modely:
1✔
26
    """
27
    Create the main object, the nnodely object, that will be used to create the network, train and export it.
28

29
    Parameters
30
    ----------
31
    visualizer : str, Visualizer, optional
32
        The visualizer to be used. Default is the 'Standard' visualizer.
33
    exporter : str, Exporter, optional
34
        The exporter to be used. Default is the 'Standard' exporter.
35
    seed : int, optional
36
        Set the seed for all the random modules inside the nnodely framework. Default is None.
37
    workspace : str
38
        The path of the workspace where all the exported files will be saved.
39
    log_internal : bool
40
        Whether or not save the logs. Default is False.
41
    save_history : bool
42
        Whether or not save the history. Default is False.
43

44
    Example
45
    -------
46
        >>> model = Modely()
47
    """
48
    def __init__(self,
1✔
49
                 visualizer:str|Visualizer|None = 'Standard',
50
                 exporter:str|Exporter|None = 'Standard',
51
                 seed:int|None = None,
52
                 workspace:str|None = None,
53
                 log_internal:bool = False,
54
                 save_history:bool = False):
55

56
        # Visualizer
57
        if visualizer == 'Standard':
1✔
58
            self.visualizer = TextVisualizer(1)
1✔
59
        elif visualizer != None:
1✔
60
            self.visualizer = visualizer
1✔
61
        else:
62
            self.visualizer = Visualizer()
1✔
63
        self.visualizer.set_n4m(self)
1✔
64

65
        # Exporter
66
        if exporter == 'Standard':
1✔
67
            self.exporter = StandardExporter(workspace, self.visualizer, save_history)
1✔
68
        elif exporter != None:
×
69
            self.exporter = exporter
×
70
        else:
71
            self.exporter = Exporter()
×
72

73
        ## Set the random seed for reproducibility
74
        if seed is not None:
1✔
75
            self.resetSeed(seed)
1✔
76

77
        # Save internal
78
        self.log_internal = log_internal
1✔
79
        if self.log_internal == True:
1✔
80
            self.internals = {}
1✔
81

82
        # Models definition
83
        self.model_def = ModelDef()
1✔
84
        self.input_n_samples = {}
1✔
85
        self.max_n_samples = 0
1✔
86
        self.neuralized = False
1✔
87
        self.traced = False
1✔
88
        self.model = None
1✔
89
        self.states = {}
1✔
90

91
        # Dataaset Parameters
92
        self.data_loaded = False
1✔
93
        self.file_count = 0
1✔
94
        self.num_of_samples = {}
1✔
95
        self.data = {}
1✔
96
        self.n_datasets = 0
1✔
97
        self.datasets_loaded = set()
1✔
98
        self.multifile = {}
1✔
99

100
        # Training Parameters
101
        self.standard_train_parameters = {
1✔
102
            'models' : None,
103
            'train_dataset' : None, 'validation_dataset' : None, 'test_dataset' : None, 'splits' : [70, 20, 10],
104
            'closed_loop' : {}, 'connect' : {}, 'step' : 0, 'prediction_samples' : 0,
105
            'shuffle_data' : True,
106
            'early_stopping' : None, 'early_stopping_params' : {},
107
            'select_model' : 'last', 'select_model_params' : {},
108
            'minimize_gain' : {},
109
            'num_of_epochs': 100,
110
            'train_batch_size' : 128, 'val_batch_size' : None, 'test_batch_size' : None,
111
            'optimizer' : 'Adam',
112
            'lr' : 0.001, 'lr_param' : {},
113
            'optimizer_params' : [], 'add_optimizer_params' : [],
114
            'optimizer_defaults' : {}, 'add_optimizer_defaults' : {}
115
        }
116

117
        # Optimizer
118
        self.optimizer = None
1✔
119

120
        # Training Losses
121
        self.loss_functions = {}
1✔
122

123
        # Validation Parameters
124
        self.training = {}
1✔
125
        self.performance = {}
1✔
126
        self.prediction = {}
1✔
127

128

129
    def resetSeed(self, seed):
1✔
130
        """
131
        Resets the random seed for reproducibility.
132

133
        This method sets the seed for various random number generators used in the project to ensure reproducibility of results.
134

135
        :param seed: The seed value to be used for the random number generators.
136
        :type seed: int
137

138
        Example:
139
            >>> model = nnodely()
140
            >>> model.resetSeed(42)
141
        """
142
        torch.manual_seed(seed)  ## set the pytorch seed
1✔
143
        torch.cuda.manual_seed_all(seed)
1✔
144
        random.seed(seed)  ## set the random module seed
1✔
145
        np.random.seed(seed)  ## set the numpy seed
1✔
146

147
    def count_operations(self, grad_fn):
1✔
NEW
148
        count = 0
×
NEW
149
        nodes = [grad_fn]
×
NEW
150
        while nodes:
×
NEW
151
            node = nodes.pop()
×
NEW
152
            count += 1
×
NEW
153
            nodes.extend(next_fn[0] for next_fn in node.next_functions if next_fn[0] is not None)
×
NEW
154
        return count
×
155

156
    def __call__(self, inputs={}, sampled=False, closed_loop={}, connect={}, prediction_samples='auto', num_of_samples=None): ##, align_input=False):
1✔
157
        """
158
        Performs inference on the model.
159

160
        Parameters
161
        ----------
162
        inputs : dict, optional
163
            A dictionary of input data. The keys are input names and the values are the corresponding data. Default is an empty dictionary.
164
        sampled : bool, optional
165
            A boolean indicating whether the inputs are already sampled. Default is False.
166
        closed_loop : dict, optional
167
            A dictionary specifying closed loop connections. The keys are input names and the values are output names. Default is an empty dictionary.
168
        connect : dict, optional
169
            A dictionary specifying connections. The keys are input names and the values are output names. Default is an empty dictionary.
170
        prediction_samples : str or int, optional
171
            The number of prediction samples. Can be 'auto', None or an integer. Default is 'auto'.
172
        num_of_samples : str or int, optional
173
            The number of samples. Can be 'auto', None or an integer. Default is 'auto'.
174

175
        Returns
176
        -------
177
        dict
178
            A dictionary containing the model's prediction outputs.
179

180
        Raises
181
        ------
182
        RuntimeError
183
            If the network is not neuralized.
184
        ValueError
185
            If an input variable is not in the model definition or if an output variable is not in the model definition.
186

187
        Examples
188
        --------
189
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
190
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/inference.ipynb
191
            :alt: Open in Colab
192

193
        Example usage:
194
            >>> model = Modely()
195
            >>> x = Input('x')
196
            >>> out = Output('out', Fir(x.last()))
197
            >>> model.addModel('example_model', [out])
198
            >>> model.neuralizeModel()
199
            >>> predictions = model(inputs={'x': [1, 2, 3]})
200
        """
201

202
        ## Copy dict for avoid python bug
203
        inputs = copy.deepcopy(inputs)
1✔
204
        closed_loop = copy.deepcopy(closed_loop)
1✔
205
        connect = copy.deepcopy(connect)
1✔
206

207
        ## Check neuralize
208
        check(self.neuralized, RuntimeError, "The network is not neuralized.")
1✔
209

210
        ## Check closed loop integrity
211
        for close_in, close_out in (closed_loop | connect).items():
1✔
212
            check(close_in in self.model_def['Inputs'], ValueError, f'the tag "{close_in}" is not an input variable.')
1✔
213
            check(close_out in self.model_def['Outputs'], ValueError, f'the tag "{close_out}" is not an output of the network')
1✔
214

215
        ## List of keys
216
        model_inputs = list(self.model_def['Inputs'].keys())
1✔
217
        model_states = list(self.model_def['States'].keys())
1✔
218
        json_inputs = self.model_def['Inputs'] | self.model_def['States']
1✔
219
        state_closed_loop = [key for key, value in self.model_def['States'].items() if 'closedLoop' in value.keys()] + list(closed_loop.keys())
1✔
220
        state_connect = [key for key, value in self.model_def['States'].items() if 'connect' in value.keys()] + list(connect.keys())
1✔
221
        extra_inputs = list(set(list(inputs.keys())) - set(model_inputs) - set(model_states))
1✔
222
        non_mandatory_inputs = state_closed_loop + state_connect 
1✔
223
        mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
1✔
224

225
        ## Remove extra inputs
226
        for key in extra_inputs:
1✔
227
            log.warning(f'The provided input {key} is not used inside the network. the inference will continue without using it')
1✔
228
            del inputs[key]
1✔
229

230
        ## Get the number of data windows for each input/state
231
        num_of_windows = {key: len(value) for key, value in inputs.items()} if sampled else {key: len(value) - self.input_n_samples[key] + 1 for key, value in inputs.items()}
1✔
232

233
        ## Get the maximum inference window
234
        if num_of_samples:
1✔
235
            window_dim = num_of_samples
1✔
236
            for key in inputs.keys():
1✔
237
                input_dim = self.model_def['Inputs'][key]['dim'] if key in model_inputs else self.model_def['States'][key]['dim']
1✔
238
                new_samples = num_of_samples - (len(inputs[key]) - self.input_n_samples[key] + 1)
1✔
239
                if input_dim > 1:
1✔
240
                    log.warning(f'The variable {key} is filled with {new_samples} samples equal to zeros.')
1✔
241
                    inputs[key] += [[0 for _ in range(input_dim)] for _ in range(new_samples)]
1✔
242
                else:
243
                    log.warning(f'The variable {key} is filled with {new_samples} samples equal to zeros.')
1✔
244
                    inputs[key] += [0 for _ in range(new_samples)]
1✔
245
        elif inputs:
1✔
246
            windows = []
1✔
247
            for key in inputs.keys():
1✔
248
                if key in mandatory_inputs:
1✔
249
                    n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self.model_def['Inputs'][key]['ntot'] + 1
1✔
250
                    windows.append(n_samples)
1✔
251
            if not windows:
1✔
252
                for key in inputs.keys():
1✔
253
                    if key in non_mandatory_inputs:
1✔
254
                        if key in model_inputs:
1✔
255
                            n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self.model_def['Inputs'][key]['ntot'] + 1
1✔
256
                        else:
257
                            n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self.model_def['States'][key]['ntot'] + 1
1✔
258
                        windows.append(n_samples)
1✔
259
            window_dim = min(windows) if windows else 0
1✔
260
        else: ## No inputs
261
            window_dim = 1 if non_mandatory_inputs else 0
1✔
262
        check(window_dim > 0, StopIteration, f'Missing samples in the input window')
1✔
263

264
        if len(set(num_of_windows.values())) > 1:
1✔
265
            max_ind_key, max_dim = argmax_dict(num_of_windows)
1✔
266
            min_ind_key, min_dim = argmin_dict(num_of_windows)
1✔
267
            log.warning(f'Different number of samples between inputs [MAX {num_of_windows[max_ind_key]} = {max_dim}; MIN {num_of_windows[min_ind_key]} = {min_dim}]')
1✔
268

269
        ## Autofill the missing inputs
270
        provided_inputs = list(inputs.keys())
1✔
271
        missing_inputs = list(set(mandatory_inputs) - set(provided_inputs))
1✔
272
        if missing_inputs:
1✔
273
            log.warning(f'Inputs not provided: {missing_inputs}. Autofilling with zeros..')
1✔
274
            for key in missing_inputs:
1✔
275
                inputs[key] = np.zeros(shape=(self.input_n_samples[key] + window_dim - 1, self.model_def['Inputs'][key]['dim']),dtype=NP_DTYPE).tolist()
1✔
276

277
        ## Transform inputs in 3D Tensors
278
        for key in inputs.keys():
1✔
279
            input_dim = json_inputs[key]['dim']
1✔
280
            inputs[key] = torch.from_numpy(np.array(inputs[key])).to(TORCH_DTYPE)
1✔
281

282
            if input_dim > 1:
1✔
283
                correct_dim = 3 if sampled else 2
1✔
284
                check(len(inputs[key].shape) == correct_dim, ValueError,f'The input {key} must have {correct_dim} dimensions')
1✔
285
                check(inputs[key].shape[correct_dim - 1] == input_dim, ValueError,f'The second dimension of the input "{key}" must be equal to {input_dim}')
1✔
286

287
            if input_dim == 1 and inputs[key].shape[-1] != 1: ## add the input dimension
1✔
288
                inputs[key] = inputs[key].unsqueeze(-1)
1✔
289
            if inputs[key].ndim <= 1: ## add the batch dimension
1✔
290
                inputs[key] = inputs[key].unsqueeze(0)
1✔
291
            if inputs[key].ndim <= 2: ## add the time dimension
1✔
292
                inputs[key] = inputs[key].unsqueeze(0)
1✔
293

294
        ## initialize the resulting dictionary
295
        result_dict = {}
1✔
296
        for key in self.model_def['Outputs'].keys():
1✔
297
            result_dict[key] = []
1✔
298

299
        ## Inference
300
        calculate_grad = False
1✔
301
        for key, value in json_inputs.items():
1✔
302
            if 'type' in value.keys():
1✔
303
                calculate_grad = True
1✔
304
                break
1✔
305
        with torch.enable_grad() if calculate_grad else torch.inference_mode():
1✔
306
            ## Update with virtual states
307
            if prediction_samples is not None:
1✔
308
                self.model.update(closed_loop=closed_loop, connect=connect)
1✔
309
            else:
310
                prediction_samples = 0
1✔
311
            X = {}
1✔
312
            count = 0
1✔
313
            first = True
1✔
314
            for idx in range(window_dim):
1✔
315
                ## Get mandatory data inputs
316
                for key in mandatory_inputs:
1✔
317
                    X[key] = inputs[key][idx:idx+1] if sampled else inputs[key][:, idx:idx + self.input_n_samples[key]]
1✔
318
                    if 'type' in json_inputs[key].keys():
1✔
319
                        X[key] = X[key].requires_grad_(True)
1✔
320
                ## reset states
321
                if count == 0 or prediction_samples=='auto':
1✔
322
                    count = prediction_samples
1✔
323
                    for key in non_mandatory_inputs: ## Get non mandatory data (from inputs, from states, or with zeros)
1✔
324
                        ## if prediction_samples is 'auto' and i have enough samples
325
                        ## if prediction_samples is NOT 'auto' but i have enough extended window (with zeros)
326
                        if (key in inputs.keys() and prediction_samples == 'auto' and idx < num_of_windows[key]) or (key in inputs.keys() and prediction_samples != 'auto' and idx < inputs[key].shape[1]):
1✔
327
                            X[key] = inputs[key][idx:idx+1] if sampled else inputs[key][:, idx:idx + self.input_n_samples[key]]
1✔
328
                        ## if im in the first reset
329
                        ## if i have a state in memory
330
                        ## if i have prediction_samples = 'auto' and not enough samples
331
                        elif (key in self.states.keys() and (first or prediction_samples == 'auto')) and (prediction_samples == 'auto' or prediction_samples == None):
1✔
332
                            X[key] = self.states[key]
1✔
333
                        else: ## if i have no samples and no states
334
                            window_size = self.input_n_samples[key]
1✔
335
                            dim = json_inputs[key]['dim']
1✔
336
                            X[key] = torch.zeros(size=(1, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
1✔
337
                            self.states[key] = X[key]
1✔
338
                        if 'type' in json_inputs[key].keys():
1✔
339
                            X[key] = X[key].requires_grad_(True)
1✔
340
                    first = False
1✔
341
                else:
342
                    # Remove the gradient of the previous forward
343
                    for key in X.keys():
1✔
344
                        if 'type' in json_inputs[key].keys():
1✔
345
                            X[key] = X[key].detach().requires_grad_(True)
1✔
346
                    count -= 1
1✔
347
                ## Forward pass
348
                result, _, out_closed_loop, out_connect = self.model(X)
1✔
349

350
                ## Append the prediction of the current sample to the result dictionary
351
                for key in self.model_def['Outputs'].keys():
1✔
352
                    if result[key].shape[-1] == 1:
1✔
353
                        result[key] = result[key].squeeze(-1)
1✔
354
                        if result[key].shape[-1] == 1:
1✔
355
                            result[key] = result[key].squeeze(-1)
1✔
356
                    result_dict[key].append(result[key].detach().squeeze(dim=0).tolist())
1✔
357

358
                ## Update closed_loop and connect
359
                if prediction_samples:
1✔
360
                    self.__updateState(X, out_closed_loop, out_connect)
1✔
361

362
        ## Remove virtual states
363
        for key in (connect.keys() | closed_loop.keys()):
1✔
364
            if key in self.states.keys():
1✔
365
                del self.states[key]
1✔
366
        
367
        return result_dict
1✔
368

369
    def getSamples(self, dataset, index = None, window=1):
1✔
370
        """
371
        Retrieves a window of samples from a given dataset.
372

373
        Parameters
374
        ----------
375
        dataset : str
376
            The name of the dataset to retrieve samples from.
377
        index : int, optional
378
            The starting index of the samples. If None, a random index is chosen. Default is None.
379
        window : int, optional
380
            The number of consecutive samples to retrieve. Default is 1.
381

382
        Returns
383
        -------
384
        dict
385
            A dictionary containing the retrieved samples. The keys are input and state names, and the values are lists of samples.
386

387
        Raises
388
        ------
389
        ValueError
390
            If the dataset is not loaded.
391

392
        Examples
393
        --------
394
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
395
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/dataset.ipynb
396
            :alt: Open in Colab
397

398
        Example usage:
399
            >>> model = Modely()
400
            >>> model.loadData('dataset_name')
401
            >>> samples = model.getSamples('dataset_name', index=10, window=5)
402
        """
403
        if index is None:
1✔
404
            index = random.randint(0, self.num_of_samples[dataset] - window)
1✔
405
        check(self.data_loaded, ValueError, 'The Dataset must first be loaded using <loadData> function!')
1✔
406
        if self.data_loaded:
1✔
407
            result_dict = {}
1✔
408
            for key in (self.model_def['Inputs'].keys() | self.model_def['States'].keys()):
1✔
409
                result_dict[key] = []
1✔
410
            for idx in range(window):
1✔
411
                for key ,samples in self.data[dataset].items():
1✔
412
                    if key in (self.model_def['Inputs'].keys() | self.model_def['States'].keys()):
1✔
413
                        result_dict[key].append(samples[index+idx])
1✔
414
            return result_dict
1✔
415

416
    def addConnect(self, stream_out, state_list_in):
1✔
417
        """
418
        Adds a connection from a relation stream to an input state.
419

420
        Parameters
421
        ----------
422
        stream_out : Stream
423
            The relation stream to connect from.
424
        state_list_in : list of State
425
            The list of input states to connect to.
426

427
        Examples
428
        --------
429
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
430
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/states.ipynb
431
            :alt: Open in Colab
432

433
        Example:
434
            >>> model = Modely()
435
            >>> x = Input('x')
436
            >>> y = State('y')
437
            >>> relation = Fir(x.last())
438
            >>> model.addConnect(relation, y)
439
        """
440
        self.model_def.addConnect(stream_out, state_list_in)
1✔
441

442
    def addClosedLoop(self, stream_out, state_list_in):
1✔
443
        """
444
        Adds a closed loop connection from a relation stream to an input state.
445

446
        Parameters
447
        ----------
448
        stream_out : Stream
449
            The relation stream to connect from.
450
        state_list_in : list of State
451
            The list of input states to connect to.
452

453
        Examples
454
        --------
455
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
456
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/states.ipynb
457
            :alt: Open in Colab
458

459
        Example:
460
            >>> model = Modely()
461
            >>> x = Input('x')
462
            >>> y = State('y')
463
            >>> relation = Fir(x.last())
464
            >>> model.addClosedLoop(relation, y)
465
        """
466
        self.model_def.addClosedLoop(stream_out, state_list_in)
1✔
467

468
    def addModel(self, name, stream_list):
1✔
469
        """
470
        Adds a new model with the given name along with a list of Outputs.
471

472
        Parameters
473
        ----------
474
        name : str
475
            The name of the model.
476
        stream_list : list of Stream
477
            The list of Outputs stream in the model.
478

479
        Example
480
        -------
481
        Example usage:
482
            >>> model = Modely()
483
            >>> x = Input('x')
484
            >>> out = Output('out', Fir(x.last()))
485
            >>> model.addModel('example_model', [out])
486
        """
487
        try:
1✔
488
            self.model_def.addModel(name, stream_list)
1✔
489
        except Exception as e:
1✔
490
            self.model_def.removeModel(name)
1✔
491
            raise e
1✔
492

493
    def removeModel(self, name_list):
1✔
494
        """
495
        Removes models with the given list of names.
496

497
        Parameters
498
        ----------
499
        name_list : list of str
500
            The list of model names to remove.
501

502
        Example
503
        -------
504
        Example usage:
505
            >>> model.removeModel(['sub_model1', 'sub_model2'])
506
        """
507
        self.model_def.removeModel(name_list)
×
508

509
    def addMinimize(self, name, streamA, streamB, loss_function='mse'):
1✔
510
        """
511
        Adds a minimize loss function to the model.
512

513
        Parameters
514
        ----------
515
        name : str
516
            The name of the cost function.
517
        streamA : Stream
518
            The first relation stream for the minimize operation.
519
        streamB : Stream
520
            The second relation stream for the minimize operation.
521
        loss_function : str, optional
522
            The loss function to use from the ones provided. Default is 'mse'.
523

524
        Example
525
        -------
526
        Example usage:
527
            >>> model.addMinimize('minimize_op', streamA, streamB, loss_function='mse')
528
        """
529
        self.model_def.addMinimize(name, streamA, streamB, loss_function)
1✔
530
        self.visualizer.showaddMinimize(name)
1✔
531

532
    def removeMinimize(self, name_list):
1✔
533
        """
534
        Removes minimize loss functions using the given list of names.
535

536
        Parameters
537
        ----------
538
        name_list : list of str
539
            The list of minimize operation names to remove.
540

541
        Example
542
        -------
543
        Example usage:
544
            >>> model.removeMinimize(['minimize_op1', 'minimize_op2'])
545
        """
546
        self.model_def.removeMinimize(name_list)
1✔
547

548
    def resetStates(self, states=[], batch=1):
1✔
549
        if states: ## reset only specific states
1✔
550
            for key in states:
1✔
551
                window_size = self.input_n_samples[key]
1✔
552
                dim = self.model_def['States'][key]['dim']
1✔
553
                self.states[key] = torch.zeros(size=(batch, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
1✔
554
        else: ## reset all states
555
            self.states = {}
1✔
556
            for key, state in self.model_def['States'].items():
1✔
557
                window_size = self.input_n_samples[key]
1✔
558
                dim = state['dim']
1✔
559
                self.states[key] = torch.zeros(size=(batch, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
1✔
560

561
    def __addInfo(self):
1✔
562
        total_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
1✔
563
        self.model_def['Info']['num_parameters'] = total_params
1✔
564
        from nnodely import __version__
1✔
565
        self.model_def['Info']['nnodely_version'] = __version__
1✔
566

567
    def neuralizeModel(self, sample_time = None, clear_model = False, model_def = None):
1✔
568
        """
569
        Neuralizes the model, preparing it for inference and training. This method creates a neural network model starting from the model definition.
570
        It will also create all the time windows for the inputs and states.
571

572
        Parameters
573
        ----------
574
        sample_time : float or None, optional
575
            The sample time for the model. Default is None.
576
        clear_model : bool, optional
577
            Whether to clear the existing model definition. Default is False.
578
        model_def : dict or None, optional
579
            A dictionary defining the model. If provided, it overrides the existing model definition. Default is None.
580

581
        Raises
582
        ------
583
        ValueError
584
            If sample_time is not None and model_def is provided.
585
            If clear_model is True and model_def is provided.
586

587
        Example
588
        -------
589
        Example usage:
590
            >>> model = Modely(name='example_model')
591
            >>> model.neuralizeModel(sample_time=0.1, clear_model=True)
592
        """
593
        if model_def is not None:
1✔
594
            check(sample_time == None, ValueError, 'The sample_time must be None if a model_def is provided')
1✔
595
            check(clear_model == False, ValueError, 'The clear_model must be False if a model_def is provided')
1✔
596
            self.model_def = ModelDef(model_def)
1✔
597
        else:
598
            if clear_model:
1✔
599
                self.model_def.update()
1✔
600
            else:
601
                self.model_def.updateParameters(self.model)
1✔
602

603
        for key, state in self.model_def['States'].items():
1✔
604
            check("connect" in state.keys() or  'closedLoop' in state.keys(), KeyError, f'The connect or closed loop missing for state "{key}"')
1✔
605

606
        self.model_def.setBuildWindow(sample_time)
1✔
607
        self.model = Model(self.model_def.json)
1✔
608
        self.__addInfo()
1✔
609

610
        input_ns_backward = {key:value['ns'][0] for key, value in (self.model_def['Inputs']|self.model_def['States']).items()}
1✔
611
        input_ns_forward = {key:value['ns'][1] for key, value in (self.model_def['Inputs']|self.model_def['States']).items()}
1✔
612
        self.input_n_samples = {}
1✔
613
        for key, value in (self.model_def['Inputs'] | self.model_def['States']).items():
1✔
614
            self.input_n_samples[key] = input_ns_backward[key] + input_ns_forward[key]
1✔
615
        self.max_n_samples = max(input_ns_backward.values()) + max(input_ns_forward.values())
1✔
616

617
        ## Initialize States 
618
        self.resetStates()
1✔
619

620
        self.neuralized = True
1✔
621
        self.traced = False
1✔
622
        self.visualizer.showModel(self.model_def.json)
1✔
623
        self.visualizer.showModelInputWindow()
1✔
624
        self.visualizer.showBuiltModel()
1✔
625

626
    def loadData(self, name, source, format=None, skiplines=0, delimiter=',', header=None, resampling=False):
1✔
627
        """
628
        Loads data into the model. The data can be loaded from a directory path containing the csv files or from a crafted dataset.
629

630
        Parameters
631
        ----------
632
        name : str
633
            The name of the dataset.
634
        source : str or list
635
            The source of the data. Can be a directory path containing the csv files or a list of custom data.
636
        format : list or None, optional
637
            The format of the data. When loading multiple csv files the format parameter will define how to read each column of the file. Default is None.
638
        skiplines : int, optional
639
            The number of lines to skip at the beginning of the file. Default is 0.
640
        delimiter : str, optional
641
            The delimiter used in the data files. Default is ','.
642
        header : list or None, optional
643
            The header of the data files. Default is None.
644

645
        Raises
646
        ------
647
        ValueError
648
            If the network is not neuralized.
649
            If the delimiter is not valid.
650

651
        Examples
652
        --------
653
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
654
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/dataset.ipynb
655
            :alt: Open in Colab
656
        
657
        Example - load data from files:
658
            >>> x = Input('x')
659
            >>> y = Input('y')
660
            >>> out = Output('out',Fir(x.tw(0.05)))
661
            >>> test = Modely(visualizer=None)
662
            >>> test.addModel('example_model', out)
663
            >>> test.neuralizeModel(0.01)
664
            >>> data_struct = ['x', '', 'y']
665
            >>> test.loadData(name='example_dataset', source='path/to/data', format=data_struct)
666

667
        Example - load data from a crafted dataset:
668
            >>> x = Input('x')
669
            >>> y = Input('y')
670
            >>> out = Output('out',Fir(x.tw(0.05)))
671
            >>> test = Modely(visualizer=None)
672
            >>> test.addModel('example_model', out)
673
            >>> test.neuralizeModel(0.01)
674
            >>> data_x = np.array(range(10))
675
            >>> dataset = {'x': data_x, 'y': (2*data_x)}
676
            >>> test.loadData(name='example_dataset',source=dataset)
677
        """
678
        check(self.neuralized, ValueError, "The network is not neuralized.")
1✔
679
        check(delimiter in ['\t', '\n', ';', ',', ' '], ValueError, 'delimiter not valid!')
1✔
680

681
        json_inputs = self.model_def['Inputs'] | self.model_def['States']
1✔
682
        model_inputs = list(json_inputs.keys())
1✔
683
        ## Initialize the dictionary containing the data
684
        if name in list(self.data.keys()):
1✔
685
            log.warning(f'Dataset named {name} already loaded! overriding the existing one..')
1✔
686
        self.data[name] = {}
1✔
687

688
        input_ns_backward = {key:value['ns'][0] for key, value in json_inputs.items()}
1✔
689
        input_ns_forward = {key:value['ns'][1] for key, value in json_inputs.items()}
1✔
690
        max_samples_backward = max(input_ns_backward.values())
1✔
691
        max_samples_forward = max(input_ns_forward.values())
1✔
692
        max_n_samples = max_samples_backward + max_samples_forward
1✔
693

694
        num_of_samples = {}
1✔
695
        if type(source) is str: ## we have a directory path containing the files
1✔
696
            ## collect column indexes
697
            format_idx = {}
1✔
698
            idx = 0
1✔
699
            for item in format:
1✔
700
                if isinstance(item, tuple):
1✔
701
                    for key in item:
×
702
                        if key not in model_inputs:
×
703
                            idx += 1
×
704
                            break
×
705
                        n_cols = json_inputs[key]['dim']
×
706
                        format_idx[key] = (idx, idx+n_cols)
×
707
                    idx += n_cols
×
708
                else:
709
                    if item not in model_inputs:
1✔
710
                        idx += 1
1✔
711
                        continue
1✔
712
                    n_cols = json_inputs[item]['dim']
1✔
713
                    format_idx[item] = (idx, idx+n_cols)
1✔
714
                    idx += n_cols
1✔
715

716
            ## Initialize each input key
717
            for key in format_idx.keys():
1✔
718
                self.data[name][key] = []
1✔
719

720
            ## obtain the file names
721
            try:
1✔
722
                _,_,files = next(os.walk(source))
1✔
723
                files.sort()
1✔
724
            except StopIteration as e:
×
725
                check(False,StopIteration, f'ERROR: The path "{source}" does not exist!')
×
726
                return
×
727
            self.file_count = len(files)
1✔
728
            if self.file_count > 1: ## Multifile
1✔
729
                self.multifile[name] = []
1✔
730

731
            ## Cycle through all the files
732
            for file in files:
1✔
733
                try:
1✔
734
                    ## read the csv
735
                    df = pd.read_csv(os.path.join(source,file), skiprows=skiplines, delimiter=delimiter, header=header)
1✔
736
                except:
×
737
                    log.warning(f'Cannot read file {os.path.join(source,file)}')
×
738
                    continue
×
739
                if self.file_count > 1:
1✔
740
                    self.multifile[name].append((self.multifile[name][-1] + (len(df) - max_n_samples + 1)) if self.multifile[name] else len(df) - max_n_samples + 1)
1✔
741
                ## Cycle through all the windows
742
                for key, idxs in format_idx.items():
1✔
743
                    back, forw = input_ns_backward[key], input_ns_forward[key]
1✔
744
                    ## Save as numpy array the data
745
                    data = df.iloc[:, idxs[0]:idxs[1]].to_numpy()
1✔
746
                    self.data[name][key] += [data[i-back:i+forw] for i in range(max_samples_backward, len(df)-max_samples_forward+1)]
1✔
747

748
            ## Stack the files
749
            for key in format_idx.keys():
1✔
750
                self.data[name][key] = np.stack(self.data[name][key])
1✔
751
                num_of_samples[key] = self.data[name][key].shape[0]
1✔
752

753
        elif type(source) is dict:  ## we have a crafted dataset
1✔
754
            self.file_count = 1
1✔
755

756
            ## Check if the inputs are correct
757
            #assert set(model_inputs).issubset(source.keys()), f'The dataset is missing some inputs. Inputs needed for the model: {model_inputs}'
758

759
            # Merge a list of inputs into a single dictionary
760
            for key in model_inputs:
1✔
761
                if key not in source.keys():
1✔
762
                    continue
1✔
763

764
                self.data[name][key] = []  ## Initialize the dataset
1✔
765

766
                back, forw = input_ns_backward[key], input_ns_forward[key]
1✔
767
                for idx in range(len(source[key]) - max_n_samples+1):
1✔
768
                    self.data[name][key].append(source[key][idx + (max_samples_backward - back):idx + (max_samples_backward + forw)])
1✔
769

770
            ## Stack the files
771
            for key in model_inputs:
1✔
772
                if key not in source.keys():
1✔
773
                    continue
1✔
774
                self.data[name][key] = np.stack(self.data[name][key])
1✔
775
                if self.data[name][key].ndim == 2: ## Add the sample dimension
1✔
776
                    self.data[name][key] = np.expand_dims(self.data[name][key], axis=-1)
1✔
777
                if self.data[name][key].ndim > 3:
1✔
778
                    self.data[name][key] = np.squeeze(self.data[name][key], axis=1)
×
779
                num_of_samples[key] = self.data[name][key].shape[0]
1✔
780

781
        elif isinstance(source, pd.DataFrame):  ## we have a crafted dataset
1✔
782
            self.file_count = 1
1✔
783

784
            ## Resampling if the time column is provided (must be a Datetime object)
785
            if resampling:
1✔
786
                if type(source.index) is pd.DatetimeIndex:
1✔
787
                    source = source.resample(f"{int(self.model_def.sample_time * 1e9)}ns").interpolate(method="linear")
1✔
788
                elif 'time' in source.columns:
1✔
789
                    if not ptypes.is_datetime64_any_dtype(source['time']):
1✔
790
                        source['time'] = pd.to_datetime(source['time'], unit='s')
1✔
791
                    source = source.set_index('time', drop=True)
1✔
792
                    source = source.resample(f"{int(self.model_def.sample_time * 1e9)}ns").interpolate(method="linear")
1✔
793
                else:
794
                    raise TypeError("No time column found in the DataFrame. Please provide a time column for resampling.")
1✔
795

796
            processed_data = {}
1✔
797
            for key in model_inputs:
1✔
798
                if key not in source.columns:
1✔
NEW
799
                    continue
×
800

801
                processed_data[key] = []  ## Initialize the dataset
1✔
802
                back, forw = input_ns_backward[key], input_ns_forward[key]
1✔
803

804
                for idx in range(len(source) - max_n_samples + 1):
1✔
805
                    window = source[key].iloc[idx + (max_samples_backward - back):idx + (max_samples_backward + forw)]
1✔
806
                    processed_data[key].append(window.to_numpy())
1✔
807

808
            ## Convert lists to numpy arrays
809
            for key in processed_data:
1✔
810
                processed_data[key] = np.stack(processed_data[key])
1✔
811
                if json_inputs[key]['dim'] > 1:
1✔
812
                    processed_data[key] = np.array(processed_data[key].tolist(), dtype=np.float64)
1✔
813
                if processed_data[key].ndim == 2:  ## Add the sample dimension
1✔
814
                    processed_data[key] = np.expand_dims(processed_data[key], axis=-1)
1✔
815
                if processed_data[key].ndim > 3:
1✔
NEW
816
                    processed_data[key] = np.squeeze(processed_data[key], axis=1)
×
817
                num_of_samples[key] = processed_data[key].shape[0]
1✔
818
            
819
            self.data[name] = processed_data
1✔
820

821

822
        # Check dim of the samples
823
        check(len(set(num_of_samples.values())) == 1, ValueError,
1✔
824
              f"The number of the sample of the dataset {name} are not the same for all input in the dataset: {num_of_samples}")
825
        self.num_of_samples[name] = num_of_samples[list(num_of_samples.keys())[0]]
1✔
826

827
        ## Set the Loaded flag to True
828
        self.data_loaded = True
1✔
829
        ## Update the number of datasets loaded
830
        self.n_datasets = len(self.data.keys())
1✔
831
        self.datasets_loaded.add(name)
1✔
832
        ## Show the dataset
833
        self.visualizer.showDataset(name=name)
1✔
834

835
    def filterData(self, filter_function, dataset_name = None):
1✔
836
        """
837
        Filters the data in the dataset using the provided filter function.
838

839
        Parameters
840
        ----------
841
        filter_function : Callable
842
            A function that takes a sample as input and returns True if the sample should be kept, and False if it should be removed.
843
        dataset_name : str or None, optional
844
            The name of the dataset to filter. If None, all datasets are filtered. Default is None.
845

846
        Examples
847
        --------
848
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
849
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/dataset.ipynb
850
            :alt: Open in Colab
851

852
        Example usage:
853
            >>> model = Modely()
854
            >>> model.loadData('dataset_name', 'path/to/data')
855
            >>> def filter_fn(sample):
856
            >>>     return sample['input1'] > 0
857
            >>> model.filterData(filter_fn, 'dataset_name')
858
        """
859
        idx_to_remove = []
×
860
        if dataset_name is None:
×
861
            for name in self.data.keys():
×
862
                dataset = self.data[name]
×
863
                n_samples = len(dataset[list(dataset.keys())[0]])
×
864

865
                data_for_filter = []
×
866
                for i in range(n_samples):
×
867
                    new_sample = {key: val[i] for key, val in dataset.items()}
×
868
                    data_for_filter.append(new_sample)
×
869

870
                for idx, sample in enumerate(data_for_filter):
×
871
                    if not filter_function(sample):
×
872
                        idx_to_remove.append(idx)
×
873

874
                for key in self.data[name].keys():
×
875
                    self.data[name][key] = np.delete(self.data[name][key], idx_to_remove, axis=0)
×
876
                    self.num_of_samples[name] = self.data[name][key].shape[0]
×
877
                self.visualizer.showDataset(name=name)
×
878

879
        else:
880
            dataset = self.data[dataset_name]
×
881
            n_samples = len(dataset[list(dataset.keys())[0]])
×
882

883
            data_for_filter = []
×
884
            for i in range(n_samples):
×
885
                new_sample = {key: val[i] for key, val in dataset.items()}
×
886
                data_for_filter.append(new_sample)
×
887

888
            for idx, sample in enumerate(data_for_filter):
×
889
                if not filter_function(sample):
×
890
                    idx_to_remove.append(idx)
×
891

892
            for key in self.data[dataset_name].keys():
×
893
                self.data[dataset_name][key] = np.delete(self.data[dataset_name][key], idx_to_remove, axis=0)
×
894
                self.num_of_samples[dataset_name] = self.data[dataset_name][key].shape[0]
×
895
            self.visualizer.showDataset(name=dataset_name)
×
896

897
    def __save_internal(self, key, value):
1✔
898
        self.internals[key] = tensor_to_list(value)
1✔
899

900
    def __get_train_parameters(self, training_params):
1✔
901
        run_train_parameters = copy.deepcopy(self.standard_train_parameters)
1✔
902
        if training_params is None:
1✔
903
            return run_train_parameters
1✔
904
        for key, value in training_params.items():
1✔
905
            check(key in run_train_parameters, KeyError, f"The param {key} is not exist as standard parameters")
1✔
906
            run_train_parameters[key] = value
1✔
907
        return run_train_parameters
1✔
908

909
    def __get_parameter(self, **parameter):
1✔
910
        assert len(parameter) == 1
1✔
911
        name = list(parameter.keys())[0]
1✔
912
        self.run_training_params[name] =  parameter[name] if parameter[name] is not None else self.run_training_params[name]
1✔
913
        return self.run_training_params[name]
1✔
914

915
    def __get_batch_sizes(self, train_batch_size, val_batch_size, test_batch_size):
1✔
916
        ## Check if the batch_size can be used for the current dataset, otherwise set the batch_size to the maximum value
917
        self.__get_parameter(train_batch_size = train_batch_size)
1✔
918
        self.__get_parameter(val_batch_size = val_batch_size)
1✔
919
        self.__get_parameter(test_batch_size = test_batch_size)
1✔
920

921
        if self.run_training_params['recurrent_train']:
1✔
922
            if self.run_training_params['train_batch_size'] > self.run_training_params['n_samples_train']:
1✔
923
                self.run_training_params['train_batch_size'] = self.run_training_params['n_samples_train'] - self.run_training_params['prediction_samples']
1✔
924
            if self.run_training_params['val_batch_size'] is None or self.run_training_params['val_batch_size'] > self.run_training_params['n_samples_val']:
1✔
925
                self.run_training_params['val_batch_size'] = max(0,self.run_training_params['n_samples_val'] - self.run_training_params['prediction_samples'])
1✔
926
            if self.run_training_params['test_batch_size'] is None or self.run_training_params['test_batch_size'] > self.run_training_params['n_samples_test']:
1✔
927
                self.run_training_params['test_batch_size'] = max(0,self.run_training_params['n_samples_test'] - self.run_training_params['prediction_samples'])
1✔
928
        else:
929
            if self.run_training_params['train_batch_size'] > self.run_training_params['n_samples_train']:
1✔
930
                self.run_training_params['train_batch_size'] = self.run_training_params['n_samples_train']
1✔
931
            if self.run_training_params['val_batch_size'] is None or self.run_training_params['val_batch_size'] > self.run_training_params['n_samples_val']:
1✔
932
                self.run_training_params['val_batch_size'] = self.run_training_params['n_samples_val']
1✔
933
            if self.run_training_params['test_batch_size'] is None or self.run_training_params['test_batch_size'] > self.run_training_params['n_samples_test']:
1✔
934
                self.run_training_params['test_batch_size'] = self.run_training_params['n_samples_test']
1✔
935

936
        check(self.run_training_params['train_batch_size'] > 0, ValueError, f'The auto train_batch_size ({self.run_training_params["train_batch_size"] }) = n_samples_train ({self.run_training_params["n_samples_train"]}) - prediction_samples ({self.run_training_params["prediction_samples"]}), must be greater than 0.')
1✔
937

938
        return self.run_training_params['train_batch_size'], self.run_training_params['val_batch_size'], self.run_training_params['test_batch_size']
1✔
939

940
    def __inizilize_optimizer(self, optimizer, optimizer_params, optimizer_defaults, add_optimizer_params, add_optimizer_defaults, models, lr, lr_param):
1✔
941
        # Get optimizer and initialization parameters
942
        optimizer = copy.deepcopy(self.__get_parameter(optimizer=optimizer))
1✔
943
        optimizer_params = copy.deepcopy(self.__get_parameter(optimizer_params=optimizer_params))
1✔
944
        optimizer_defaults = copy.deepcopy(self.__get_parameter(optimizer_defaults=optimizer_defaults))
1✔
945
        add_optimizer_params = copy.deepcopy(self.__get_parameter(add_optimizer_params=add_optimizer_params))
1✔
946
        add_optimizer_defaults = copy.deepcopy(self.__get_parameter(add_optimizer_defaults=add_optimizer_defaults))
1✔
947

948
        ## Get parameter to be trained
949
        json_models = []
1✔
950
        models = self.__get_parameter(models=models)
1✔
951
        if 'Models' in self.model_def:
1✔
952
            json_models = list(self.model_def['Models'].keys()) if type(self.model_def['Models']) is dict else [self.model_def['Models']]
1✔
953
        if models is None:
1✔
954
            models = json_models
1✔
955
        self.run_training_params['models'] = models
1✔
956
        params_to_train = set()
1✔
957
        if isinstance(models, str):
1✔
958
            models = [models]
1✔
959
        for model in models:
1✔
960
            check(model in json_models, ValueError, f'The model {model} is not in the model definition')
1✔
961
            if type(self.model_def['Models']) is dict:
1✔
962
                params_to_train |= set(self.model_def['Models'][model]['Parameters'])
1✔
963
            else:
964
                params_to_train |= set(self.model_def['Parameters'].keys())
1✔
965

966
        # Get the optimizer
967
        if type(optimizer) is str:
1✔
968
            if optimizer == 'SGD':
1✔
969
                optimizer = SGD({},[])
1✔
970
            elif optimizer == 'Adam':
1✔
971
                optimizer = Adam({},[])
1✔
972
        else:
973
            check(issubclass(type(optimizer), Optimizer), TypeError,
1✔
974
                  "The optimizer must be an Optimizer or str")
975

976
        optimizer.set_params_to_train(self.model.all_parameters, params_to_train)
1✔
977

978
        optimizer.add_defaults('lr', self.run_training_params['lr'])
1✔
979
        optimizer.add_option_to_params('lr', self.run_training_params['lr_param'])
1✔
980

981
        if optimizer_defaults != {}:
1✔
982
            optimizer.set_defaults(optimizer_defaults)
1✔
983
        if optimizer_params != []:
1✔
984
            optimizer.set_params(optimizer_params)
1✔
985

986
        for key, value in add_optimizer_defaults.items():
1✔
987
            optimizer.add_defaults(key, value)
1✔
988

989
        add_optimizer_params = optimizer.unfold(add_optimizer_params)
1✔
990
        for param in add_optimizer_params:
1✔
991
            par = param['params']
1✔
992
            del param['params']
1✔
993
            for key, value in param.items():
1✔
994
                optimizer.add_option_to_params(key, {par:value})
1✔
995

996
        # Modify the parameter
997
        optimizer.add_defaults('lr', lr)
1✔
998
        optimizer.add_option_to_params('lr', lr_param)
1✔
999

1000
        return optimizer
1✔
1001

1002
    def trainModel(self,
1✔
1003
                    models=None,
1004
                    train_dataset = None, validation_dataset = None, test_dataset = None, splits = None,
1005
                    closed_loop = None, connect = None, step = None, prediction_samples = None,
1006
                    shuffle_data = None,
1007
                    early_stopping = None, early_stopping_params = None,
1008
                    select_model = None, select_model_params = None,
1009
                    minimize_gain = None,
1010
                    num_of_epochs = None,
1011
                    train_batch_size = None, val_batch_size = None, test_batch_size = None,
1012
                    optimizer = None,
1013
                    lr = None, lr_param = None,
1014
                    optimizer_params = None, optimizer_defaults = None,
1015
                    training_params = None,
1016
                    add_optimizer_params = None, add_optimizer_defaults = None
1017
                   ):
1018
        """
1019
        Trains the model using the provided datasets and parameters.
1020

1021
        Parameters
1022
        ----------
1023
        models : list or None, optional
1024
            A list of models to train. Default is None.
1025
        train_dataset : str or None, optional
1026
            The name of the training dataset. Default is None.
1027
        validation_dataset : str or None, optional
1028
            The name of the validation dataset. Default is None.
1029
        test_dataset : str or None, optional
1030
            The name of the test dataset. Default is None.
1031
        splits : list or None, optional
1032
            A list of 3 elements specifying the percentage of splits for training, validation, and testing. The three elements must sum up to 100!
1033
            The parameter splits is only used when there is only 1 dataset loaded. Default is None.
1034
        closed_loop : dict or None, optional
1035
            A dictionary specifying closed loop connections. The keys are input names and the values are output names. Default is None.
1036
        connect : dict or None, optional
1037
            A dictionary specifying connections. The keys are input names and the values are output names. Default is None.
1038
        step : int or None, optional
1039
            The step size for training. A big value will result in less data used for each epochs and a faster train. Default is None.
1040
        prediction_samples : int or None, optional
1041
            The size of the prediction horizon. Number of samples at each recurrent window Default is None.
1042
        shuffle_data : bool or None, optional
1043
            Whether to shuffle the data during training. Default is None.
1044
        early_stopping : Callable or None, optional
1045
            A callable for early stopping. Default is None.
1046
        early_stopping_params : dict or None, optional
1047
            A dictionary of parameters for early stopping. Default is None.
1048
        select_model : Callable or None, optional
1049
            A callable for selecting the best model. Default is None.
1050
        select_model_params : dict or None, optional
1051
            A dictionary of parameters for selecting the best model. Default is None.
1052
        minimize_gain : dict or None, optional
1053
            A dictionary specifying the gain for each minimization loss function. Default is None.
1054
        num_of_epochs : int or None, optional
1055
            The number of epochs to train the model. Default is None.
1056
        train_batch_size : int or None, optional
1057
            The batch size for training. Default is None.
1058
        val_batch_size : int or None, optional
1059
            The batch size for validation. Default is None.
1060
        test_batch_size : int or None, optional
1061
            The batch size for testing. Default is None.
1062
        optimizer : Optimizer or None, optional
1063
            The optimizer to use for training. Default is None.
1064
        lr : float or None, optional
1065
            The learning rate. Default is None.
1066
        lr_param : dict or None, optional
1067
            A dictionary of learning rate parameters. Default is None.
1068
        optimizer_params : dict or None, optional
1069
            A dictionary of optimizer parameters. Default is None.
1070
        optimizer_defaults : dict or None, optional
1071
            A dictionary of default optimizer settings. Default is None.
1072
        training_params : dict or None, optional
1073
            A dictionary of training parameters. Default is None.
1074
        add_optimizer_params : dict or None, optional
1075
            Additional optimizer parameters. Default is None.
1076
        add_optimizer_defaults : dict or None, optional
1077
            Additional default optimizer settings. Default is None.
1078

1079
        Raises
1080
        ------
1081
        RuntimeError
1082
            If no data is loaded or if there are no modules with learnable parameters.
1083
        KeyError
1084
            If the sample horizon is not positive.
1085
        ValueError
1086
            If an input or output variable is not in the model definition.
1087

1088
        Examples
1089
        --------
1090
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1091
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/training.ipynb
1092
            :alt: Open in Colab
1093

1094
        Example - basic feed-forward training:
1095
            >>> x = Input('x')
1096
            >>> F = Input('F')
1097

1098
            >>> xk1 = Output('x[k+1]', Fir()(x.tw(0.2))+Fir()(F.last()))
1099

1100
            >>> mass_spring_damper = Modely(seed=0)
1101
            >>> mass_spring_damper.addModel('xk1',xk1)
1102
            >>> mass_spring_damper.neuralizeModel(sample_time = 0.05) 
1103

1104
            >>> data_struct = ['time','x','dx','F']
1105
            >>> data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'dataset','data')
1106
            >>> mass_spring_damper.loadData(name='mass_spring_dataset', source=data_folder, format=data_struct, delimiter=';')
1107

1108
            >>> params = {'num_of_epochs': 100,'train_batch_size': 128,'lr':0.001}
1109
            >>> mass_spring_damper.trainModel(splits=[70,20,10], training_params = params)
1110

1111
        Example - recurrent training:
1112
            >>> x = Input('x')
1113
            >>> F = Input('F')
1114

1115
            >>> xk1 = Output('x[k+1]', Fir()(x.tw(0.2))+Fir()(F.last()))
1116

1117
            >>> mass_spring_damper = Modely(seed=0)
1118
            >>> mass_spring_damper.addModel('xk1',xk1)
1119
            >>> mass_spring_damper.addClosedLoop(xk1, x)
1120
            >>> mass_spring_damper.neuralizeModel(sample_time = 0.05) 
1121

1122
            >>> data_struct = ['time','x','dx','F']
1123
            >>> data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'dataset','data')
1124
            >>> mass_spring_damper.loadData(name='mass_spring_dataset', source=data_folder, format=data_struct, delimiter=';')
1125

1126
            >>> params = {'num_of_epochs': 100,'train_batch_size': 128,'lr':0.001}
1127
            >>> mass_spring_damper.trainModel(splits=[70,20,10], prediction_samples=10, training_params = params)
1128
        """
1129
        check(self.data_loaded, RuntimeError, 'There is no data loaded! The Training will stop.')
1✔
1130
        check('Models' in self.model_def.json, RuntimeError, 'There are no models to train. Load a model using the addModel function.')
1✔
1131
        check(list(self.model.parameters()), RuntimeError, 'There are no modules with learnable parameters! The Training will stop.')
1✔
1132

1133
        ## Get running parameter from dict
1134
        self.run_training_params = copy.deepcopy(self.__get_train_parameters(training_params))
1✔
1135

1136
        ## Get connect and closed_loop
1137
        prediction_samples = self.__get_parameter(prediction_samples = prediction_samples)
1✔
1138
        check(prediction_samples >= 0, KeyError, 'The sample horizon must be positive!')
1✔
1139

1140
        ## Check close loop and connect
1141
        if self.log_internal:
1✔
1142
            self.internals = {}
1✔
1143
        step = self.__get_parameter(step = step)
1✔
1144
        closed_loop = self.__get_parameter(closed_loop = closed_loop)
1✔
1145
        connect = self.__get_parameter(connect = connect)
1✔
1146
        recurrent_train = True
1✔
1147
        if closed_loop:
1✔
1148
            for input, output in closed_loop.items():
1✔
1149
                check(input in self.model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.')
1✔
1150
                check(output in self.model_def['Outputs'], ValueError, f'the tag {output} is not an output of the network')
1✔
1151
                log.warning(f'Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples')
1✔
1152
        elif connect:
1✔
1153
            for connect_in, connect_out in connect.items():
1✔
1154
                check(connect_in in self.model_def['Inputs'], ValueError, f'the tag {connect_in} is not an input variable.')
1✔
1155
                check(connect_out in self.model_def['Outputs'], ValueError, f'the tag {connect_out} is not an output of the network')
1✔
1156
                log.warning(f'Recurrent train: connecting the input ports {connect_in} with output ports {connect_out} for {prediction_samples} samples')
1✔
1157
        elif self.model_def['States']: ## if we have state variables we have to do the recurrent train
1✔
1158
            log.warning(f"Recurrent train: update States variables {list(self.model_def['States'].keys())} for {prediction_samples} samples")
1✔
1159
        else:
1160
            if prediction_samples != 0:
1✔
1161
                log.warning(
1✔
1162
                    f"The value of the prediction_samples={prediction_samples} is not used in not recursive network.")
1163
            recurrent_train = False
1✔
1164
        self.run_training_params['recurrent_train'] = recurrent_train
1✔
1165

1166
        ## Get early stopping
1167
        early_stopping = self.__get_parameter(early_stopping = early_stopping)
1✔
1168
        if early_stopping:
1✔
1169
            self.run_training_params['early_stopping'] = early_stopping.__name__
×
1170
        early_stopping_params = self.__get_parameter(early_stopping_params = early_stopping_params)
1✔
1171

1172
        ## Get dataset for training
1173
        shuffle_data = self.__get_parameter(shuffle_data = shuffle_data)
1✔
1174

1175
        ## Get the dataset name
1176
        train_dataset = self.__get_parameter(train_dataset = train_dataset)
1✔
1177
        #TODO manage multiple datasets
1178
        if train_dataset is None: ## If we use all datasets with the splits
1✔
1179
            splits = self.__get_parameter(splits = splits)
1✔
1180
            check(len(splits)==3, ValueError, '3 elements must be inserted for the dataset split in training, validation and test')
1✔
1181
            check(sum(splits)==100, ValueError, 'Training, Validation and Test splits must sum up to 100.')
1✔
1182
            check(splits[0] > 0, ValueError, 'The training split cannot be zero.')
1✔
1183

1184
            ## Get the dataset name
1185
            dataset = list(self.data.keys())[0] ## take the dataset name
1✔
1186
            train_dataset_name = val_dataset_name = test_dataset_name = dataset
1✔
1187

1188
            ## Collect the split sizes
1189
            train_size = splits[0] / 100.0
1✔
1190
            val_size = splits[1] / 100.0
1✔
1191
            test_size = 1 - (train_size + val_size)
1✔
1192
            num_of_samples = self.num_of_samples[dataset]
1✔
1193
            n_samples_train = round(num_of_samples*train_size)
1✔
1194
            if splits[1] == 0:
1✔
1195
                n_samples_test = num_of_samples-n_samples_train
1✔
1196
                n_samples_val = 0
1✔
1197
            else:
1198
                n_samples_test = round(num_of_samples*test_size)
1✔
1199
                n_samples_val = num_of_samples-n_samples_train-n_samples_test
1✔
1200

1201
            ## Split into train, validation and test
1202
            XY_train, XY_val, XY_test = {}, {}, {}
1✔
1203
            for key, samples in self.data[dataset].items():
1✔
1204
                if val_size == 0.0 and test_size == 0.0: ## we have only training set
1✔
1205
                    XY_train[key] = torch.from_numpy(samples).to(TORCH_DTYPE)
1✔
1206
                elif val_size == 0.0 and test_size != 0.0: ## we have only training and test set
1✔
1207
                    XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
1✔
1208
                    XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE)
1✔
1209
                elif val_size != 0.0 and test_size == 0.0: ## we have only training and validation set
1✔
1210
                    XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
1✔
1211
                    XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE)
1✔
1212
                else: ## we have training, validation and test set
1213
                    XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
1✔
1214
                    XY_val[key] = torch.from_numpy(samples[n_samples_train:-n_samples_test]).to(TORCH_DTYPE)
1✔
1215
                    XY_test[key] = torch.from_numpy(samples[n_samples_train+n_samples_val:]).to(TORCH_DTYPE)
1✔
1216

1217
            ## Set name for resultsAnalysis
1218
            train_dataset = self.__get_parameter(train_dataset = f"train_{dataset}_{train_size:0.2f}")
1✔
1219
            validation_dataset = self.__get_parameter(validation_dataset =f"validation_{dataset}_{val_size:0.2f}")
1✔
1220
            test_dataset = self.__get_parameter(test_dataset = f"test_{dataset}_{test_size:0.2f}")
1✔
1221
        else: ## Multi-Dataset
1222
            ## Get the names of the datasets
1223
            datasets = list(self.data.keys())
1✔
1224
            validation_dataset = self.__get_parameter(validation_dataset=validation_dataset)
1✔
1225
            test_dataset = self.__get_parameter(test_dataset=test_dataset)
1✔
1226
            train_dataset_name, val_dataset_name, test_dataset_name = train_dataset, validation_dataset, test_dataset
1✔
1227

1228
            ## Collect the number of samples for each dataset
1229
            n_samples_train, n_samples_val, n_samples_test = 0, 0, 0
1✔
1230

1231
            check(train_dataset in datasets, KeyError, f'{train_dataset} Not Loaded!')
1✔
1232
            if validation_dataset is not None and validation_dataset not in datasets:
1✔
1233
                log.warning(f'Validation Dataset [{validation_dataset}] Not Loaded. The training will continue without validation')
×
1234
            if test_dataset is not None and test_dataset not in datasets:
1✔
1235
                log.warning(f'Test Dataset [{test_dataset}] Not Loaded. The training will continue without test')
×
1236

1237
            ## Split into train, validation and test
1238
            XY_train, XY_val, XY_test = {}, {}, {}
1✔
1239
            n_samples_train = self.num_of_samples[train_dataset]
1✔
1240
            XY_train = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in self.data[train_dataset].items()}
1✔
1241
            if validation_dataset in datasets:
1✔
1242
                n_samples_val = self.num_of_samples[validation_dataset]
1✔
1243
                XY_val = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in self.data[validation_dataset].items()}
1✔
1244
            if test_dataset in datasets:
1✔
1245
                n_samples_test = self.num_of_samples[test_dataset]
1✔
1246
                XY_test = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in self.data[test_dataset].items()}
1✔
1247

1248
        for key in XY_train.keys():
1✔
1249
            assert n_samples_train == XY_train[key].shape[0], f'The number of train samples {n_samples_train}!={XY_train[key].shape[0]} not compliant.'
1✔
1250
            if key in XY_val:
1✔
1251
                assert n_samples_val == XY_val[key].shape[0], f'The number of val samples {n_samples_val}!={XY_val[key].shape[0]} not compliant.'
1✔
1252
            if key in XY_test:
1✔
1253
                assert n_samples_test == XY_test[key].shape[0], f'The number of test samples {n_samples_test}!={XY_test[key].shape[0]} not compliant.'
1✔
1254

1255
        assert n_samples_train > 0, f'There are {n_samples_train} samples for training.'
1✔
1256
        self.run_training_params['n_samples_train'] = n_samples_train
1✔
1257
        self.run_training_params['n_samples_val'] = n_samples_val
1✔
1258
        self.run_training_params['n_samples_test'] = n_samples_test
1✔
1259
        train_batch_size, val_batch_size, test_batch_size = self.__get_batch_sizes(train_batch_size, val_batch_size, test_batch_size)
1✔
1260

1261
        ## Define the optimizer
1262
        optimizer = self.__inizilize_optimizer(optimizer, optimizer_params, optimizer_defaults, add_optimizer_params, add_optimizer_defaults, models, lr, lr_param)
1✔
1263
        self.run_training_params['optimizer'] = optimizer.name
1✔
1264
        self.run_training_params['optimizer_params'] = optimizer.optimizer_params
1✔
1265
        self.run_training_params['optimizer_defaults'] = optimizer.optimizer_defaults
1✔
1266
        self.optimizer = optimizer.get_torch_optimizer()
1✔
1267

1268
        ## Get num_of_epochs
1269
        num_of_epochs = self.__get_parameter(num_of_epochs = num_of_epochs)
1✔
1270

1271
        ## Define the loss functions
1272
        minimize_gain = self.__get_parameter(minimize_gain = minimize_gain)
1✔
1273
        self.run_training_params['minimizers'] = {}
1✔
1274
        for name, values in self.model_def['Minimizers'].items():
1✔
1275
            self.loss_functions[name] = CustomLoss(values['loss'])
1✔
1276
            self.run_training_params['minimizers'][name] = {}
1✔
1277
            self.run_training_params['minimizers'][name]['A'] = values['A']
1✔
1278
            self.run_training_params['minimizers'][name]['B'] = values['B']
1✔
1279
            self.run_training_params['minimizers'][name]['loss'] = values['loss']
1✔
1280
            if name in minimize_gain:
1✔
1281
                self.run_training_params['minimizers'][name]['gain'] = minimize_gain[name]
1✔
1282

1283
        ## Clean the dict of the training parameter
1284
        del self.run_training_params['minimize_gain']
1✔
1285
        del self.run_training_params['lr']
1✔
1286
        del self.run_training_params['lr_param']
1✔
1287
        if not recurrent_train:
1✔
1288
            del self.run_training_params['connect']
1✔
1289
            del self.run_training_params['closed_loop']
1✔
1290
            del self.run_training_params['step']
1✔
1291
            del self.run_training_params['prediction_samples']
1✔
1292
        if early_stopping is None:
1✔
1293
            del self.run_training_params['early_stopping']
1✔
1294
            del self.run_training_params['early_stopping_params']
1✔
1295

1296
        ## Create the train, validation and test loss dictionaries
1297
        train_losses, val_losses = {}, {}
1✔
1298
        for key in self.model_def['Minimizers'].keys():
1✔
1299
            train_losses[key] = []
1✔
1300
            if n_samples_val > 0:
1✔
1301
                val_losses[key] = []
1✔
1302

1303
        ## Check the needed keys are in the datasets
1304
        keys = set(self.model_def['Inputs'].keys())
1✔
1305
        keys |= {value['A'] for value in self.model_def['Minimizers'].values()}|{value['B'] for value in self.model_def['Minimizers'].values()}
1✔
1306
        keys -= set(self.model_def['Relations'].keys())
1✔
1307
        keys -= set(self.model_def['States'].keys())
1✔
1308
        keys -= set(self.model_def['Outputs'].keys())
1✔
1309
        if 'connect' in self.run_training_params:
1✔
1310
            keys -= set(self.run_training_params['connect'].keys())
1✔
1311
        if 'closed_loop' in self.run_training_params:
1✔
1312
            keys -= set(self.run_training_params['closed_loop'].keys())
1✔
1313
        check(set(keys).issubset(set(XY_train.keys())), KeyError, f"Not all the mandatory keys {keys} are present in the training dataset {set(XY_train.keys())}.")
1✔
1314

1315
        # Evaluate the number of update for epochs and the unsued samples
1316
        if recurrent_train:
1✔
1317
            list_of_batch_indexes = range(0, (n_samples_train - train_batch_size - prediction_samples + 1), (train_batch_size + step))
1✔
1318
            check(n_samples_train - train_batch_size - prediction_samples + 1 > 0, ValueError,
1✔
1319
                  f"The number of available sample are (n_samples_train ({n_samples_train}) - train_batch_size ({train_batch_size}) - prediction_samples ({prediction_samples}) + 1) = {n_samples_train - train_batch_size - prediction_samples + 1}.")
1320
            update_per_epochs = (n_samples_train - train_batch_size - prediction_samples + 1)//(train_batch_size + step) + 1
1✔
1321
            unused_samples = n_samples_train - list_of_batch_indexes[-1] - train_batch_size - prediction_samples
1✔
1322

1323
            model_inputs = list(self.model_def['Inputs'].keys())
1✔
1324
            state_closed_loop = [key for key, value in self.model_def['States'].items() if 'closedLoop' in value.keys()] + list(closed_loop.keys())
1✔
1325
            state_connect = [key for key, value in self.model_def['States'].items() if 'connect' in value.keys()] + list(connect.keys())
1✔
1326
            non_mandatory_inputs = state_closed_loop + state_connect 
1✔
1327
            mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
1✔
1328
            
1329
            list_of_batch_indexes_train, train_step = self.__get_batch_indexes(train_dataset_name, n_samples_train, prediction_samples, train_batch_size, step, type='train')
1✔
1330
            if n_samples_val > 0:
1✔
1331
                list_of_batch_indexes_val, val_step = self.__get_batch_indexes(val_dataset_name, n_samples_val, prediction_samples, val_batch_size, step, type='val')
1✔
1332
        else:
1333
            update_per_epochs =  (n_samples_train - train_batch_size)//train_batch_size + 1
1✔
1334
            unused_samples = n_samples_train - update_per_epochs * train_batch_size
1✔
1335

1336
        self.run_training_params['update_per_epochs'] = update_per_epochs
1✔
1337
        self.run_training_params['unused_samples'] = unused_samples
1✔
1338

1339
        ## Set the gradient to true if necessary
1340
        json_inputs = self.model_def['Inputs'] | self.model_def['States']
1✔
1341
        for key in json_inputs.keys():
1✔
1342
            if 'type' in json_inputs[key]:
1✔
1343
                if key in XY_train:
1✔
1344
                    XY_train[key].requires_grad_(True)
1✔
1345
                if key in XY_val:
1✔
1346
                    XY_val[key].requires_grad_(True)
1✔
1347
                if key in XY_test:
1✔
1348
                    XY_test[key].requires_grad_(True)
1✔
1349

1350
        ## Select the model
1351
        select_model = self.__get_parameter(select_model = select_model)
1✔
1352
        select_model_params = self.__get_parameter(select_model_params = select_model_params)
1✔
1353
        selected_model_def = ModelDef(self.model_def.json)
1✔
1354

1355
        ## Show the training parameters
1356
        self.visualizer.showTrainParams()
1✔
1357

1358
        import time
1✔
1359
        ## start the train timer
1360
        start = time.time()
1✔
1361
        self.visualizer.showStartTraining()
1✔
1362

1363
        for epoch in range(num_of_epochs):
1✔
1364
            ## TRAIN
1365
            self.model.train()
1✔
1366
            if recurrent_train:
1✔
1367
                losses = self.__recurrentTrain(XY_train, list_of_batch_indexes_train, train_batch_size, minimize_gain, closed_loop, connect, prediction_samples, train_step, non_mandatory_inputs, mandatory_inputs, shuffle=shuffle_data, train=True)
1✔
1368
            else:
1369
                losses = self.__Train(XY_train, n_samples_train, train_batch_size, minimize_gain, shuffle=shuffle_data, train=True)
1✔
1370
            ## save the losses
1371
            for ind, key in enumerate(self.model_def['Minimizers'].keys()):
1✔
1372
                train_losses[key].append(torch.mean(losses[ind]).tolist())
1✔
1373

1374
            if n_samples_val > 0:
1✔
1375
                ## VALIDATION
1376
                self.model.eval()
1✔
1377
                if recurrent_train:
1✔
1378
                    losses = self.__recurrentTrain(XY_val, list_of_batch_indexes_val, val_batch_size, minimize_gain, closed_loop, connect, prediction_samples, val_step, non_mandatory_inputs, mandatory_inputs, shuffle=False, train=False)
1✔
1379
                else:
1380
                    losses = self.__Train(XY_val, n_samples_val, val_batch_size, minimize_gain, shuffle=False, train=False)
1✔
1381
                ## save the losses
1382
                for ind, key in enumerate(self.model_def['Minimizers'].keys()):
1✔
1383
                    val_losses[key].append(torch.mean(losses[ind]).tolist())
1✔
1384

1385
            ## Early-stopping
1386
            if callable(early_stopping):
1✔
1387
                if early_stopping(train_losses, val_losses, early_stopping_params):
×
1388
                    log.info(f'Stopping the training at epoch {epoch} due to early stopping.')
×
1389
                    break
×
1390

1391
            if callable(select_model):
1✔
1392
                if select_model(train_losses, val_losses, select_model_params):
×
1393
                    best_model_epoch = epoch
×
1394
                    selected_model_def.updateParameters(self.model)
×
1395

1396
            ## Visualize the training...
1397
            self.visualizer.showTraining(epoch, train_losses, val_losses)
1✔
1398
            self.visualizer.showWeightsInTrain(epoch = epoch)
1✔
1399

1400
        ## Save the training time
1401
        end = time.time()
1✔
1402
        ## Visualize the training time
1403
        for key in self.model_def['Minimizers'].keys():
1✔
1404
            self.training[key] = {'train': train_losses[key]}
1✔
1405
            if n_samples_val > 0:
1✔
1406
                self.training[key]['val'] = val_losses[key]
1✔
1407
        self.visualizer.showEndTraining(num_of_epochs-1, train_losses, val_losses)
1✔
1408
        self.visualizer.showTrainingTime(end-start)
1✔
1409

1410
        ## Select the model
1411
        if callable(select_model):
1✔
1412
            log.info(f'Selected the model at the epoch {best_model_epoch+1}.')
×
1413
            self.model = Model(selected_model_def)
×
1414
        else:
1415
            log.info('The selected model is the LAST model of the training.')
1✔
1416

1417
        self.resultAnalysis(train_dataset, XY_train, minimize_gain, closed_loop, connect,  prediction_samples, step, train_batch_size)
1✔
1418
        if self.run_training_params['n_samples_val'] > 0:
1✔
1419
            self.resultAnalysis(validation_dataset, XY_val, minimize_gain, closed_loop, connect,  prediction_samples, step, val_batch_size)
1✔
1420
        if self.run_training_params['n_samples_test'] > 0:
1✔
1421
            self.resultAnalysis(test_dataset, XY_test, minimize_gain, closed_loop, connect,  prediction_samples, step, test_batch_size)
1✔
1422

1423
        self.visualizer.showResults()
1✔
1424

1425
        ## Get trained model from torch and set the model_def
1426
        self.model_def.updateParameters(self.model)
1✔
1427

1428
    def __get_batch_indexes(self, dataset_name, n_samples, prediction_samples, batch_size, step, type='train'):
1✔
1429
        available_samples = n_samples - prediction_samples
1✔
1430
        batch_indexes = list(range(available_samples))
1✔
1431
        if dataset_name in self.multifile.keys():
1✔
1432
            if type == 'train':
1✔
1433
                start_idx, end_idx = 0, n_samples
1✔
1434
            elif type == 'val':
1✔
1435
                start_idx, end_idx = self.run_training_params['n_samples_train'], self.run_training_params['n_samples_train'] + n_samples
1✔
NEW
1436
            elif type == 'test':
×
1437
                start_idx, end_idx = self.run_training_params['n_samples_train'] + self.run_training_params['n_samples_val'], self.run_training_params['n_samples_train'] + self.run_training_params['n_samples_val'] + n_samples
×
1438
            
1439
            forbidden_idxs = []
1✔
1440
            for i in self.multifile[dataset_name]:
1✔
1441
                if i < end_idx and i > start_idx:
1✔
1442
                    forbidden_idxs.extend(range(i-prediction_samples, i, 1))
1✔
1443
            batch_indexes = [idx for idx in batch_indexes if idx not in forbidden_idxs]
1✔
1444

1445
        ## Clip the step 
1446
        clipped_step = copy.deepcopy(step)
1✔
1447
        if clipped_step < 0: ## clip the step to zero
1✔
1448
            log.warning(f"The step is negative ({clipped_step}). The step is set to zero.", stacklevel=5)
1✔
1449
            clipped_step = 0
1✔
1450
        if clipped_step > (len(batch_indexes)-batch_size): ## Clip the step to the maximum number of samples
1✔
1451
            log.warning(f"The step ({clipped_step}) is greater than the number of available samples ({len(batch_indexes)-batch_size}). The step is set to the maximum number.", stacklevel=5)
1✔
1452
            clipped_step = len(batch_indexes)-batch_size
1✔
1453
        ## Loss vector 
1454
        check((batch_size+clipped_step)>0, ValueError, f"The sum of batch_size={batch_size} and the step={clipped_step} must be greater than 0.")
1✔
1455

1456
        return batch_indexes, clipped_step
1✔
1457
    
1458
    def __updateState(self, X, out_closed_loop, out_connect):
1✔
1459
        ## Update
1460
        for key, val in out_closed_loop.items():
1✔
1461
            shift = val.shape[1]  ## take the output time dimension
1✔
1462
            X[key] = torch.roll(X[key], shifts=-1, dims=1) ## Roll the time window
1✔
1463
            X[key][:, -shift:, :] = val ## substitute with the predicted value
1✔
1464
            self.states[key] = X[key].clone().detach()
1✔
1465
        for key, value in out_connect.items():
1✔
1466
            X[key] = value
1✔
1467
            self.states[key] = X[key].clone().detach()
1✔
1468

1469
    def __recurrentTrain(self, data, batch_indexes, batch_size, loss_gains, closed_loop, connect, prediction_samples, step, non_mandatory_inputs, mandatory_inputs, shuffle=False, train=True):
1✔
1470
        indexes = copy.deepcopy(batch_indexes)
1✔
1471
        json_inputs = self.model_def['States'] | self.model_def['Inputs']
1✔
1472
        aux_losses = torch.zeros([len(self.model_def['Minimizers']), round((len(indexes)+step)/(batch_size+step))])
1✔
1473
        ## Update with virtual states
1474
        self.model.update(closed_loop=closed_loop, connect=connect)
1✔
1475
        X = {}
1✔
1476
        batch_val = 0
1✔
1477
        while len(indexes) >= batch_size:
1✔
1478
            idxs = random.sample(indexes, batch_size) if shuffle else indexes[:batch_size]
1✔
1479
            for num in idxs:
1✔
1480
                indexes.remove(num)
1✔
1481
            if step > 0:
1✔
1482
                if len(indexes) >= step:
1✔
1483
                    step_idxs = random.sample(indexes, step) if shuffle else indexes[:step]
1✔
1484
                    for num in step_idxs:
1✔
1485
                        indexes.remove(num)
1✔
1486
                else:
1487
                    indexes = []
1✔
1488
            if train:
1✔
1489
                self.optimizer.zero_grad() ## Reset the gradient
1✔
1490
            ## Reset 
1491
            horizon_losses = {ind: [] for ind in range(len(self.model_def['Minimizers']))}
1✔
1492
            for key in non_mandatory_inputs:
1✔
1493
                if key in data.keys():
1✔
1494
                ## with data
1495
                    X[key] = data[key][idxs]
1✔
1496
                else: ## with zeros
1497
                    window_size = self.input_n_samples[key]
1✔
1498
                    dim = json_inputs[key]['dim']
1✔
1499
                    if 'type' in json_inputs[key]:
1✔
1500
                        X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True)
1✔
1501
                    else:
1502
                        X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
1✔
1503
                    self.states[key] = X[key]
1✔
1504

1505
            for horizon_idx in range(prediction_samples + 1):
1✔
1506
                ## Get data 
1507
                for key in mandatory_inputs:
1✔
1508
                    X[key] = data[key][[idx+horizon_idx for idx in idxs]]
1✔
1509
                ## Forward pass
1510
                out, minimize_out, out_closed_loop, out_connect = self.model(X)
1✔
1511

1512
                if self.log_internal and train:
1✔
1513
                    assert(check_gradient_operations(self.states)==0)
1✔
1514
                    assert(check_gradient_operations(data) == 0)
1✔
1515
                    internals_dict = {'XY':tensor_to_list(X),'out':out,'param':self.model.all_parameters,'closedLoop':self.model.closed_loop_update,'connect':self.model.connect_update}
1✔
1516

1517
                ## Loss Calculation
1518
                for ind, (key, value) in enumerate(self.model_def['Minimizers'].items()):
1✔
1519
                    loss = self.loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
1✔
1520
                    loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss  ## Multiply by the gain if necessary
1✔
1521
                    horizon_losses[ind].append(loss)
1✔
1522

1523
                ## Update
1524
                self.__updateState(X, out_closed_loop, out_connect)
1✔
1525

1526
                if self.log_internal and train:
1✔
1527
                    internals_dict['state'] = self.states
1✔
1528
                    self.__save_internal('inout_'+str(batch_val)+'_'+str(horizon_idx),internals_dict)
1✔
1529

1530
            ## Calculate the total loss
1531
            total_loss = 0
1✔
1532
            for ind in range(len(self.model_def['Minimizers'])):
1✔
1533
                loss = sum(horizon_losses[ind])/(prediction_samples+1)
1✔
1534
                aux_losses[ind][batch_val] = loss.item()
1✔
1535
                total_loss += loss
1✔
1536

1537
            ## Gradient Step
1538
            if train:
1✔
1539
                total_loss.backward() ## Backpropagate the error
1✔
1540
                self.optimizer.step()
1✔
1541
                self.visualizer.showWeightsInTrain(batch = batch_val)
1✔
1542
            batch_val += 1
1✔
1543

1544
        ## Remove virtual states
1545
        for key in (connect.keys() | closed_loop.keys()):
1✔
1546
            if key in self.states.keys():
1✔
1547
                del self.states[key]
1✔
1548

1549
        ## return the losses
1550
        return aux_losses
1✔
1551

1552
    def __Train(self, data, n_samples, batch_size, loss_gains, shuffle=True, train=True):
1✔
1553
        check((n_samples - batch_size + 1) > 0, ValueError, f"The number of available sample are (n_samples_train - train_batch_size + 1) = {n_samples - batch_size + 1}.")
1✔
1554
        if shuffle:
1✔
1555
            randomize = torch.randperm(n_samples)
1✔
1556
            data = {key: val[randomize] for key, val in data.items()}
1✔
1557
        ## Initialize the train losses vector
1558
        aux_losses = torch.zeros([len(self.model_def['Minimizers']),n_samples//batch_size])
1✔
1559
        for idx in range(0, (n_samples - batch_size + 1), batch_size):
1✔
1560
            ## Build the input tensor
1561
            XY = {key: val[idx:idx+batch_size] for key, val in data.items()}
1✔
1562
            ## Reset gradient
1563
            if train:
1✔
1564
                self.optimizer.zero_grad()
1✔
1565
            ## Model Forward
1566
            _, minimize_out, _, _ = self.model(XY)  ## Forward pass
1✔
1567
            ## Loss Calculation
1568
            total_loss = 0
1✔
1569
            for ind, (key, value) in enumerate(self.model_def['Minimizers'].items()):
1✔
1570
                loss = self.loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
1✔
1571
                loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss  ## Multiply by the gain if necessary
1✔
1572
                aux_losses[ind][idx//batch_size] = loss.item()
1✔
1573
                total_loss += loss
1✔
1574
            ## Gradient step
1575
            if train:
1✔
1576
                total_loss.backward()
1✔
1577
                self.optimizer.step()
1✔
1578
                self.visualizer.showWeightsInTrain(batch = idx//batch_size)
1✔
1579

1580
        ## return the losses
1581
        return aux_losses
1✔
1582

1583
    def resultAnalysis(self, dataset, data = None, minimize_gain = {}, closed_loop = {}, connect = {},  prediction_samples = None, step = 0, batch_size = None):
1✔
1584
        import warnings
1✔
1585
        json_inputs = self.model_def['Inputs'] | self.model_def['States']
1✔
1586
        calculate_grad = False
1✔
1587
        for key, value in json_inputs.items():
1✔
1588
            if 'type' in value.keys():
1✔
1589
                calculate_grad = True
1✔
1590
                break
1✔
1591
        with torch.enable_grad() if calculate_grad else torch.inference_mode():
1✔
1592
            ## Init model for retults analysis
1593
            self.model.eval()
1✔
1594
            self.performance[dataset] = {}
1✔
1595
            self.prediction[dataset] = {}
1✔
1596
            A = {}
1✔
1597
            B = {}
1✔
1598
            total_losses = {}
1✔
1599

1600
            # Create the losses
1601
            losses = {}
1✔
1602
            for name, values in self.model_def['Minimizers'].items():
1✔
1603
                losses[name] = CustomLoss(values['loss'])
1✔
1604

1605
            recurrent = False
1✔
1606
            if (closed_loop or connect or self.model_def['States']) and prediction_samples is not None:
1✔
1607
                recurrent = True
1✔
1608

1609
            if data is None:
1✔
1610
                check(dataset in self.data.keys(), ValueError, f'The dataset {dataset} is not loaded!')
1✔
1611
                data = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in self.data[dataset].items()}
1✔
1612
            n_samples = len(data[list(data.keys())[0]])
1✔
1613

1614
            if recurrent:
1✔
1615
                batch_size = batch_size if batch_size is not None else n_samples - prediction_samples
1✔
1616

1617
                model_inputs = list(self.model_def['Inputs'].keys())
1✔
1618

1619
                state_closed_loop = [key for key, value in self.model_def['States'].items() if 'closedLoop' in value.keys()] + list(closed_loop.keys())
1✔
1620
                state_connect = [key for key, value in self.model_def['States'].items() if 'connect' in value.keys()] + list(connect.keys())
1✔
1621

1622
                non_mandatory_inputs = state_closed_loop + state_connect 
1✔
1623
                mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
1✔
1624

1625
                for key, value in self.model_def['Minimizers'].items():
1✔
1626
                    total_losses[key], A[key], B[key] = [], [], []
1✔
1627
                    for horizon_idx in range(prediction_samples + 1):
1✔
1628
                        A[key].append([])
1✔
1629
                        B[key].append([])
1✔
1630

1631
                list_of_batch_indexes = list(range(n_samples - prediction_samples))
1✔
1632
                ## Remove forbidden indexes in case of a multi-file dataset
1633
                if dataset in self.multifile.keys(): ## Multi-file Dataset
1✔
1634
                    if n_samples == self.run_training_params['n_samples_train']: ## Training
×
NEW
1635
                        list_of_batch_indexes, step = self.__get_batch_indexes(dataset, n_samples, prediction_samples, batch_size, step, type='train')
×
1636
                    elif n_samples == self.run_training_params['n_samples_val']: ## Validation
×
NEW
1637
                        list_of_batch_indexes, step = self.__get_batch_indexes(dataset, n_samples, prediction_samples, batch_size, step, type='val')
×
1638
                    else:
NEW
1639
                        list_of_batch_indexes, step = self.__get_batch_indexes(dataset, n_samples, prediction_samples, batch_size, step, type='test')
×
1640

1641
                X = {}
1✔
1642
                ## Update with virtual states
1643
                self.model.update(closed_loop=closed_loop, connect=connect)
1✔
1644
                while len(list_of_batch_indexes) >= batch_size:
1✔
1645
                    idxs = list_of_batch_indexes[:batch_size]
1✔
1646
                    for num in idxs:
1✔
1647
                        list_of_batch_indexes.remove(num)
1✔
1648
                    if step > 0:
1✔
1649
                        if len(list_of_batch_indexes) >= step:
1✔
1650
                            step_idxs =  list_of_batch_indexes[:step]
1✔
1651
                            for num in step_idxs:
1✔
1652
                                list_of_batch_indexes.remove(num)
1✔
1653
                        else:
1654
                            list_of_batch_indexes = []
1✔
1655
                    ## Reset 
1656
                    horizon_losses = {key: [] for key in self.model_def['Minimizers'].keys()}
1✔
1657
                    for key in non_mandatory_inputs:
1✔
1658
                        if key in data.keys():
1✔
1659
                            ## with data
1660
                            X[key] = data[key][idxs]
1✔
1661
                        else:  ## with zeros
1662
                            window_size = self.input_n_samples[key]
1✔
1663
                            dim = json_inputs[key]['dim']
1✔
1664
                            if 'type' in json_inputs[key]:
1✔
1665
                                X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True)
1✔
1666
                            else:
1667
                                X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
1✔
1668
                            self.states[key] = X[key]
1✔
1669

1670
                    for horizon_idx in range(prediction_samples + 1):
1✔
1671
                        ## Get data 
1672
                        for key in mandatory_inputs:
1✔
1673
                            X[key] = data[key][[idx+horizon_idx for idx in idxs]]
1✔
1674
                        ## Forward pass
1675
                        out, minimize_out, out_closed_loop, out_connect = self.model(X)
1✔
1676

1677
                        ## Loss Calculation
1678
                        for key, value in self.model_def['Minimizers'].items():
1✔
1679
                            A[key][horizon_idx].append(minimize_out[value['A']].detach().numpy())
1✔
1680
                            B[key][horizon_idx].append(minimize_out[value['B']].detach().numpy())
1✔
1681
                            loss = losses[key](minimize_out[value['A']], minimize_out[value['B']])
1✔
1682
                            loss = (loss * minimize_gain[key]) if key in minimize_gain.keys() else loss  ## Multiply by the gain if necessary
1✔
1683
                            horizon_losses[key].append(loss)
1✔
1684

1685
                        ## Update
1686
                        self.__updateState(X, out_closed_loop, out_connect)
1✔
1687

1688
                    ## Calculate the total loss
1689
                    for key in self.model_def['Minimizers'].keys():
1✔
1690
                        loss = sum(horizon_losses[key]) / (prediction_samples + 1)
1✔
1691
                        total_losses[key].append(loss.detach().numpy())
1✔
1692

1693
                for key, value in self.model_def['Minimizers'].items():
1✔
1694
                    for horizon_idx in range(prediction_samples + 1):
1✔
1695
                        A[key][horizon_idx] = np.concatenate(A[key][horizon_idx])
1✔
1696
                        B[key][horizon_idx] = np.concatenate(B[key][horizon_idx])
1✔
1697
                    total_losses[key] = np.mean(total_losses[key])
1✔
1698

1699
            else:
1700
                if batch_size is None:
1✔
1701
                    batch_size = n_samples
1✔
1702

1703
                for key, value in self.model_def['Minimizers'].items():
1✔
1704
                    total_losses[key], A[key], B[key] = [], [], []
1✔
1705

1706
                for idx in range(0, (n_samples - batch_size + 1), batch_size):
1✔
1707
                    ## Build the input tensor
1708
                    XY = {key: val[idx:idx + batch_size] for key, val in data.items()}
1✔
1709

1710
                    ## Model Forward
1711
                    _, minimize_out, _, _ = self.model(XY)  ## Forward pass
1✔
1712
                    ## Loss Calculation
1713
                    for key, value in self.model_def['Minimizers'].items():
1✔
1714
                        A[key].append(minimize_out[value['A']].detach().numpy())
1✔
1715
                        B[key].append(minimize_out[value['B']].detach().numpy())
1✔
1716
                        loss = losses[key](minimize_out[value['A']], minimize_out[value['B']])
1✔
1717
                        loss = (loss * minimize_gain[key]) if key in minimize_gain.keys() else loss
1✔
1718
                        total_losses[key].append(loss.detach().numpy())
1✔
1719

1720
                for key, value in self.model_def['Minimizers'].items():
1✔
1721
                    A[key] = np.concatenate(A[key])
1✔
1722
                    B[key] = np.concatenate(B[key])
1✔
1723
                    total_losses[key] = np.mean(total_losses[key])
1✔
1724

1725
            for ind, (key, value) in enumerate(self.model_def['Minimizers'].items()):
1✔
1726
                A_np = np.array(A[key])
1✔
1727
                B_np = np.array(B[key])
1✔
1728
                self.performance[dataset][key] = {}
1✔
1729
                self.performance[dataset][key][value['loss']] = np.mean(total_losses[key]).item()
1✔
1730
                self.performance[dataset][key]['fvu'] = {}
1✔
1731
                # Compute FVU
1732
                residual = A_np - B_np
1✔
1733
                error_var = np.var(residual)
1✔
1734
                error_mean = np.mean(residual)
1✔
1735
                #error_var_manual = np.sum((residual-error_mean) ** 2) / (len(self.prediction['B'][ind]) - 0)
1736
                #print(f"{key} var np:{new_error_var} and var manual:{error_var_manual}")
1737
                with warnings.catch_warnings(record=True) as w:
1✔
1738
                    self.performance[dataset][key]['fvu']['A'] = (error_var / np.var(A_np)).item()
1✔
1739
                    self.performance[dataset][key]['fvu']['B'] = (error_var / np.var(B_np)).item()
1✔
1740
                    if w and np.var(A_np) == 0.0 and  np.var(B_np) == 0.0:
1✔
1741
                        self.performance[dataset][key]['fvu']['A'] = np.nan
1✔
1742
                        self.performance[dataset][key]['fvu']['B'] = np.nan
1✔
1743
                self.performance[dataset][key]['fvu']['total'] = np.mean([self.performance[dataset][key]['fvu']['A'],self.performance[dataset][key]['fvu']['B']]).item()
1✔
1744
                # Compute AIC
1745
                #normal_dist = norm(0, error_var ** 0.5)
1746
                #probability_of_residual = normal_dist.pdf(residual)
1747
                #log_likelihood_first = sum(np.log(probability_of_residual))
1748
                p1 = -len(residual)/2.0*np.log(2*np.pi)
1✔
1749
                with warnings.catch_warnings(record=True) as w:
1✔
1750
                    p2 = -len(residual)/2.0*np.log(error_var)
1✔
1751
                    p3 = -1 / (2.0 * error_var) * np.sum(residual ** 2)
1✔
1752
                    if w and p2 == np.float32(np.inf) and p3 == np.float32(-np.inf):
1✔
1753
                        p2 = p3 = 0.0
1✔
1754
                log_likelihood = p1+p2+p3
1✔
1755
                #print(f"{key} log likelihood second mode:{log_likelihood} = {p1}+{p2}+{p3} first mode: {log_likelihood_first}")
1756
                total_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
1✔
1757
                #print(f"{key} total_params:{total_params}")
1758
                aic = - 2 * log_likelihood + 2 * total_params
1✔
1759
                #print(f"{key} aic:{aic}")
1760
                self.performance[dataset][key]['aic'] = {'value':aic,'total_params':total_params,'log_likelihood':log_likelihood}
1✔
1761
                # Prediction and target
1762
                self.prediction[dataset][key] = {}
1✔
1763
                self.prediction[dataset][key]['A'] = A_np.tolist()
1✔
1764
                self.prediction[dataset][key]['B'] = B_np.tolist()
1✔
1765

1766
            ## Remove virtual states
1767
            for key in (connect.keys() | closed_loop.keys()):
1✔
1768
                if key in self.states.keys():
1✔
1769
                    del self.states[key]
1✔
1770

1771
            self.performance[dataset]['total'] = {}
1✔
1772
            self.performance[dataset]['total']['mean_error'] = np.mean([value for key,value in total_losses.items()])
1✔
1773
            self.performance[dataset]['total']['fvu'] = np.mean([self.performance[dataset][key]['fvu']['total'] for key in self.model_def['Minimizers'].keys()])
1✔
1774
            self.performance[dataset]['total']['aic'] = np.mean([self.performance[dataset][key]['aic']['value']for key in self.model_def['Minimizers'].keys()])
1✔
1775

1776
        self.visualizer.showResult(dataset)
1✔
1777

1778
    def getWorkspace(self):
1✔
1779
        return self.exporter.getWorkspace()
1✔
1780

1781
    def saveTorchModel(self, name = 'net', model_folder = None, models = None):
1✔
1782
        """
1783
        Saves the neural network model in PyTorch format.
1784

1785
        Parameters
1786
        ----------
1787
        name : str, optional
1788
            The name of the saved model file. Default is 'net'.
1789
        model_folder : str or None, optional
1790
            The folder to save the model file in. Default is None.
1791
        models : list or None, optional
1792
            A list of model names to save. If None, the entire model is saved. Default is None.
1793

1794
        Raises
1795
        ------
1796
        RuntimeError
1797
            If the model is not neuralized.
1798

1799
        Examples
1800
        --------
1801
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1802
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
1803
            :alt: Open in Colab
1804

1805
        Example usage:
1806
            >>> model = Modely()
1807
            >>> model.neuralizeModel()
1808
            >>> model.saveTorchModel(name='example_model', model_folder='path/to/save')
1809
        """
1810
        check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet!')
1✔
1811
        if models is not None:
1✔
1812
            if name == 'net':
×
1813
                name += '_' + '_'.join(models)
×
1814
            model_def = ModelDef()
×
1815
            model_def.update(model_dict = {key: self.model_dict[key] for key in models if key in self.model_dict})
×
1816
            model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
×
1817
            model_def.updateParameters(self.model)
×
1818
            model = Model(model_def.json)
×
1819
        else:
1820
            model = self.model
1✔
1821
        self.exporter.saveTorchModel(model, name, model_folder)
1✔
1822

1823
    def loadTorchModel(self, name = 'net', model_folder = None):
1✔
1824
        """
1825
        Loads a neural network model from a PyTorch format file.
1826

1827
        Parameters
1828
        ----------
1829
        name : str, optional
1830
            The name of the model file to load. Default is 'net'.
1831
        model_folder : str or None, optional
1832
            The folder to load the model file from. Default is None.
1833

1834
        Raises
1835
        ------
1836
        RuntimeError
1837
            If the model is not neuralized.
1838

1839
        Examples
1840
        --------
1841
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1842
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
1843
            :alt: Open in Colab
1844

1845
        Example usage:
1846
            >>> model = Modely()
1847
            >>> model.neuralizeModel()
1848
            >>> model.loadTorchModel(name='example_model', model_folder='path/to/load')
1849
        """
1850
        check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
1✔
1851
        self.exporter.loadTorchModel(self.model, name, model_folder)
1✔
1852

1853
    def saveModel(self, name = 'net', model_path = None, models = None):
1✔
1854
        ## TODO: Add tests passing the attribute 'models'
1855
        """
1856
        Saves the neural network model definition in a json file.
1857

1858
        Parameters
1859
        ----------
1860
        name : str, optional
1861
            The name of the saved model file. Default is 'net'.
1862
        model_path : str or None, optional
1863
            The path to save the model file. Default is None.
1864
        models : list or None, optional
1865
            A list of model names to save. If None, the entire model is saved. Default is None.
1866

1867
        Raises
1868
        ------
1869
        RuntimeError
1870
            If the network has not been defined.
1871

1872
        Examples
1873
        --------
1874
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1875
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
1876
            :alt: Open in Colab
1877

1878
        Example usage:
1879
            >>> model = Modely()
1880
            >>> model.neuralizeModel()
1881
            >>> model.saveModel(name='example_model', model_path='path/to/save')
1882
        """
1883
        if models is not None:
1✔
1884
            if name == 'net':
×
1885
                name += '_' + '_'.join(models)
×
1886
            model_def = ModelDef()
×
NEW
1887
            model_def.update(model_dict = {key: self.model_def.model_dict[key] for key in models if key in self.model_def.model_dict})
×
1888
            model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
×
1889
            model_def.updateParameters(self.model)
×
1890
        else:
1891
            model_def = self.model_def
1✔
1892
        check(model_def.isDefined(), RuntimeError, "The network has not been defined.")
1✔
1893
        self.exporter.saveModel(model_def.json, name, model_path)
1✔
1894

1895
    def loadModel(self, name = None, model_folder = None):
1✔
1896
        """
1897
        Loads a neural network model from a json file containing the model definition.
1898

1899
        Parameters
1900
        ----------
1901
        name : str or None, optional
1902
            The name of the model file to load. Default is 'net'.
1903
        model_folder : str or None, optional
1904
            The folder to load the model file from. Default is None.
1905

1906
        Raises
1907
        ------
1908
        RuntimeError
1909
            If there is an error loading the network.
1910

1911
        Examples
1912
        --------
1913
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1914
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
1915
            :alt: Open in Colab
1916

1917
        Example usage:
1918
            >>> model = Modely()
1919
            >>> model.loadModel(name='example_model', model_folder='path/to/load')
1920
        """
1921
        if name is None:
1✔
1922
            name = 'net'
1✔
1923
        model_def = self.exporter.loadModel(name, model_folder)
1✔
1924
        check(model_def, RuntimeError, "Error to load the network.")
1✔
1925
        self.model_def = ModelDef(model_def)
1✔
1926
        self.model = None
1✔
1927
        self.neuralized = False
1✔
1928
        self.traced = False
1✔
1929

1930
    def exportPythonModel(self, name = 'net', model_path = None, models = None):
1✔
1931
        """
1932
        Exports the neural network model as a standalone PyTorch Module class.
1933

1934
        Parameters
1935
        ----------
1936
        name : str, optional
1937
            The name of the exported model file. Default is 'net'.
1938
        model_path : str or None, optional
1939
            The path to save the exported model file. Default is None.
1940
        models : list or None, optional
1941
            A list of model names to export. If None, the entire model is exported. Default is None.
1942

1943
        Raises
1944
        ------
1945
        RuntimeError
1946
            If the network has not been defined.
1947
            If the model is traced and cannot be exported to Python.
1948
            If the model is not neuralized.
1949

1950
        Examples
1951
        --------
1952
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1953
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
1954
            :alt: Open in Colab
1955

1956
        Example usage:
1957
            >>> model = Modely(name='example_model')
1958
            >>> model.neuralizeModel()
1959
            >>> model.exportPythonModel(name='example_model', model_path='path/to/export')
1960
        """
1961
        if models is not None:
1✔
1962
            if name == 'net':
×
1963
                name += '_' + '_'.join(models)
×
1964
            model_def = ModelDef()
×
NEW
1965
            model_def.update(model_dict = {key: self.model_def.model_dict[key] for key in models if key in self.model_def.model_dict})
×
1966
            model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
×
1967
            model_def.updateParameters(self.model)
×
1968
            model = Model(model_def.json)
×
1969
        else:
1970
            model_def = self.model_def
1✔
1971
            model = self.model
1✔
1972
        #check(model_def['States'] == {}, TypeError, "The network has state variables. The export to python is not possible.")
1973
        check(model_def.isDefined(), RuntimeError, "The network has not been defined.")
1✔
1974
        check(self.traced == False, RuntimeError,
1✔
1975
                  'The model is traced and cannot be exported to Python.\n Run neuralizeModel() to recreate a standard model.')
1976
        check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
1✔
1977
        self.exporter.saveModel(model_def.json, name, model_path)
1✔
1978
        self.exporter.exportPythonModel(model_def, model, name, model_path)
1✔
1979

1980
    def importPythonModel(self, name = None, model_folder = None):
1✔
1981
        """
1982
        Imports a neural network model from a standalone PyTorch Module class.
1983

1984
        Parameters
1985
        ----------
1986
        name : str or None, optional
1987
            The name of the model file to import. Default is 'net'.
1988
        model_folder : str or None, optional
1989
            The folder to import the model file from. Default is None.
1990

1991
        Raises
1992
        ------
1993
        RuntimeError
1994
            If there is an error loading the network.
1995

1996
        Examples
1997
        --------
1998
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
1999
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
2000
            :alt: Open in Colab
2001

2002
        Example usage:
2003
            >>> model = Modely()
2004
            >>> model.importPythonModel(name='example_model', model_folder='path/to/import')
2005
        """
2006
        if name is None:
1✔
2007
            name = 'net'
1✔
2008
        model_def = self.exporter.loadModel(name, model_folder)
1✔
2009
        check(model_def is not None, RuntimeError, "Error to load the network.")
1✔
2010
        self.neuralizeModel(model_def=model_def)
1✔
2011
        self.model = self.exporter.importPythonModel(name, model_folder)
1✔
2012
        self.traced = True
1✔
2013
        self.model_def.updateParameters(self.model)
1✔
2014

2015
    def exportONNX(self, inputs_order=None, outputs_order=None,  models = None, name = 'net', model_folder = None):
1✔
2016
        """
2017
        Exports the neural network model to an ONNX file.
2018

2019
        .. note::
2020
            The input_order may contain all the inputs and states of the model in the order that you want to export them.
2021

2022
        Parameters
2023
        ----------
2024
        inputs_order : list
2025
            The order of the input and state variables.
2026
        outputs_order : list
2027
            The order of the output variables.
2028
        models : list or None, optional
2029
            A list of model names to export. If None, the entire model is exported. Default is None.
2030
        name : str, optional
2031
            The name of the exported ONNX file. Default is 'net'.
2032
        model_folder : str or None, optional
2033
            The folder to save the exported ONNX file. Default is None.
2034

2035
        Raises
2036
        ------
2037
        RuntimeError
2038
            If the network has not been defined.
2039
            If the model is traced and cannot be exported to ONNX.
2040
            If the model is not neuralized.
2041
            If the model is loaded and not created.
2042

2043
        Examples
2044
        --------
2045
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
2046
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
2047
            :alt: Open in Colab
2048

2049
        Example usage:
2050
            >>> input1 = Input('input1').last()
2051
            >>> input2 = Input('input2').last()
2052
            >>> out = Output('output1', input1+input2)
2053
 
2054
            >>> model = Modely()
2055
            >>> model.neuralizeModel()
2056
            >>> model.exportONNX(inputs_order=['input1', 'input2'], outputs_order=['output1'], name='example_model', model_folder='path/to/export')
2057
        """
2058
        check(self.model_def.isDefined(), RuntimeError, "The network has not been defined.")
1✔
2059
        check(self.traced == False, RuntimeError, 'The model is traced and cannot be exported to ONNX.\n Run neuralizeModel() to recreate a standard model.')
1✔
2060
        check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
1✔
2061
        check(self.model_def.model_dict != {}, RuntimeError, 'The model is loaded and not created.')
1✔
2062
        model_def = ModelDef()
1✔
2063
        if models is not None:
1✔
2064
            if name == 'net':
1✔
2065
                name += '_' + '_'.join(models)
1✔
2066
            model_def.update(model_dict = {key: self.model_def.model_dict[key] for key in models if key in self.model_def.model_dict})
1✔
2067
        else:
2068
            model_def.update(model_dict = self.model_def.model_dict)
1✔
2069
        model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
1✔
2070
        model_def.updateParameters(self.model)
1✔
2071
        model = Model(model_def.json)
1✔
2072
        model.update()
1✔
2073
        self.exporter.exportONNX(model_def, model, inputs_order, outputs_order, name, model_folder)
1✔
2074

2075
    def onnxInference(self, inputs:dict, path:str):
1✔
2076
        """
2077
        Run an inference session using an onnx model previously exported using the nnodely framework. 
2078

2079
        .. note:: Feed-Forward ONNX model
2080
            For feed-forward models, the onnx model expect all the inputs and states to have 3 dimensions. The first dimension is the batch size, the second is the time window and the third is the feature dimension.
2081
        .. note:: Recurrent ONNX model
2082
            For recurrent models, the onnx model expect all the inputs to have 4 dimensions. The first dimension is the prediction horizon, the second is the batch size, the third is the time window and the fourth is the feature dimension.
2083
            For recurrent models, the onnx model expect all the States to have 3 dimensions. The first dimension is the batch size, the second is the time window, the third is the feature dimension
2084

2085
        Parameters
2086
        ----------
2087
        inputs : dict
2088
            A dictionary containing the input and state variables to be used to make the inference. 
2089
            State variables are mandatory and are used to initialize the states of the model.
2090
        path : str
2091
            The path to the ONNX file to use.
2092

2093
        Raises
2094
        ------
2095
        RuntimeError
2096
            If the shape of the inputs are not equals to the ones defined in the onnx model.
2097
            If the batch size is not equal for all the inputs and states.
2098

2099
        Examples
2100
        --------
2101
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
2102
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
2103
            :alt: Open in Colab
2104

2105
        Example - Feed-Forward:
2106
            >>> x = Input('x')
2107
 
2108
            >>> onnx_model_path = path/to/net.onnx
2109
            >>> dummy_input = {'x':np.ones(shape=(3, 1, 1)).astype(np.float32)}
2110
            >>> predictions = Modely().onnxInference(dummy_input, onnx_model_path)
2111
        Example - Recurrent:
2112
            >>> x = Input('x')
2113
            >>> y = State('y')
2114
 
2115
            >>> onnx_model_path = path/to/net.onnx
2116
            >>> dummy_input = {'x':np.ones(shape=(3, 1, 1, 1)).astype(np.float32)
2117
                                'y':np.ones(shape=(1, 1, 1)).astype(np.float32)}
2118
            >>> predictions = Modely().onnxInference(dummy_input, onnx_model_path)
2119
        """
2120
        return self.exporter.onnxInference(inputs, path)
1✔
2121

2122
    def exportReport(self, name = 'net', model_folder = None):
1✔
2123
        """
2124
        Generates a PDF report with plots containing the results of the training and validation of the neural network.
2125

2126
        Parameters
2127
        ----------
2128
        name : str, optional
2129
            The name of the exported report file. Default is 'net'.
2130
        model_folder : str or None, optional
2131
            The folder to save the exported report file. Default is None.
2132

2133
        Examples
2134
        --------
2135
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
2136
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/export.ipynb
2137
            :alt: Open in Colab
2138
            
2139
        Example usage:
2140
            >>> model = Modely()
2141
            >>> model.neuralizeModel()
2142
            >>> model.trainModel(train_dataset='train_dataset', validation_dataset='val_dataset', num_of_epochs=10)
2143
            >>> model.exportReport(name='example_model', model_folder='path/to/export')
2144
        """
2145
        self.exporter.exportReport(self, name, model_folder)
1✔
2146

2147
nnodely = Modely
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc