Simulai models autoencoder

AutoEncoder#

AutoencoderMLP#

Bases: NetworkTemplate

This is an implementation of a Fully-connected AutoEncoder as Reduced Order Model;

A MLP autoencoder architecture consists of two stages:

  • Fully-connected encoder
  • Fully connected decoder

Graphical scheme:

        |         |
        |  |   |  |
Z ->    |  | | |  |  -> Z_til
        |  |   |  |
        |         |

ENCODER DECODER

Source code in simulai/models/_pytorch_models/_autoencoder.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
class AutoencoderMLP(NetworkTemplate):
    r"""This is an implementation of a Fully-connected AutoEncoder as
    Reduced Order Model;

    A MLP autoencoder architecture consists of two stages:

    - Fully-connected encoder
    - Fully connected decoder

    Graphical scheme:

                |         |
                |  |   |  |
        Z ->    |  | | |  |  -> Z_til
                |  |   |  |
                |         |

    ENCODER       DECODER

    """

    def __init__(
        self,
        encoder: DenseNetwork = None,
        decoder: DenseNetwork = None,
        input_dim: Optional[int] = None,
        output_dim: Optional[int] = None,
        latent_dim: Optional[int] = None,
        activation: Optional[Union[list, str]] = None,
        shallow: Optional[bool] = False,
        devices: Union[str, list] = "cpu",
        name: str = None,
    ) -> None:
        """Initialize the AutoencoderMLP network

        Args:
            encoder (DenseNetwork, optional): The encoder network architecture. (Default value = None)
            decoder (DenseNetwork, optional): The decoder network architecture. (Default value = None)
            input_dim (Optional[int], optional): The input dimensions of the data, by default None.
            output_dim (Optional[int], optional): The output dimensions of the data, by default None.
            latent_dim (Optional[int], optional): The dimensions of the latent space, by default None.
            activation (Optional[Union[list, str]], optional): The activation functions used by the network, by default None.
            shallow (Optional[bool], optional): Whether the network should be shallow or not, by default False.
            devices (Union[str, list], optional): The device(s) to be used for allocating subnetworks, by default "cpu".
            name (str, optional): The name of the network, by default None.

        """

        super(AutoencoderMLP, self).__init__(name=name)

        self.weights = list()

        # This option is used when no network is provided
        # and it uses default choices for the architectures
        if encoder == None and decoder == None:
            encoder, decoder = mlp_autoencoder_auto(
                input_dim=input_dim,
                latent_dim=latent_dim,
                output_dim=output_dim,
                activation=activation,
                shallow=shallow,
            )

        # Determining the kind of device to be used for allocating the
        # subnetworks used in the DeepONet model
        self.device = self._set_device(devices=devices)

        self.encoder = self.to_wrap(entity=encoder, device=self.device)
        self.decoder = self.to_wrap(entity=decoder, device=self.device)

        self.add_module("encoder", self.encoder)
        self.add_module("decoder", self.decoder)

        self.weights += self.encoder.weights
        self.weights += self.decoder.weights

        self.last_encoder_channels = None

        self.shapes_dict = dict()

    def summary(self) -> None:
        """Prints the summary of the network architecture"""
        self.encoder.summary()
        self.decoder.summary()

    def projection(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        """Project the input dataset into the latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be projected, by default None.

        Returns:
            torch.Tensor: The dataset projected over the latent space.

        """
        latent = self.encoder.forward(input_data=input_data)

        return latent

    def reconstruction(
        self, input_data: Union[torch.Tensor, np.ndarray] = None
    ) -> torch.Tensor:
        """Reconstruct the latent dataset to the original one.

        Args:
            input_data (Union[torch.Tensor, np.ndarray], optional): The dataset to be reconstructed, by default None.

        Returns:
            torch.Tensor: The dataset reconstructed.

        """
        reconstructed = self.decoder.forward(input_data=input_data)

        return reconstructed

    def forward(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        """Execute the complete projection/reconstruction pipeline.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input dataset, by default None.

        Returns:
            torch.Tensor: The dataset reconstructed.

        """
        latent = self.projection(input_data=input_data)
        reconstructed = self.reconstruction(input_data=latent)

        return reconstructed

    def eval_projection(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> np.ndarray:
        """Evaluate the projection of the input dataset into the latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be projected, by default None.

        Returns:
            np.ndarray: The dataset projected over the latent space.

        """
        return self.projection(input_data=input_data).detach().numpy()

__init__(encoder=None, decoder=None, input_dim=None, output_dim=None, latent_dim=None, activation=None, shallow=False, devices='cpu', name=None) #

Initialize the AutoencoderMLP network

Parameters:

Name Type Description Default
encoder DenseNetwork

The encoder network architecture. (Default value = None)

None
decoder DenseNetwork

The decoder network architecture. (Default value = None)

None
input_dim Optional[int]

The input dimensions of the data, by default None.

None
output_dim Optional[int]

The output dimensions of the data, by default None.

None
latent_dim Optional[int]

The dimensions of the latent space, by default None.

None
activation Optional[Union[list, str]]

The activation functions used by the network, by default None.

None
shallow Optional[bool]

Whether the network should be shallow or not, by default False.

False
devices Union[str, list]

The device(s) to be used for allocating subnetworks, by default "cpu".

'cpu'
name str

The name of the network, by default None.

None
Source code in simulai/models/_pytorch_models/_autoencoder.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __init__(
    self,
    encoder: DenseNetwork = None,
    decoder: DenseNetwork = None,
    input_dim: Optional[int] = None,
    output_dim: Optional[int] = None,
    latent_dim: Optional[int] = None,
    activation: Optional[Union[list, str]] = None,
    shallow: Optional[bool] = False,
    devices: Union[str, list] = "cpu",
    name: str = None,
) -> None:
    """Initialize the AutoencoderMLP network

    Args:
        encoder (DenseNetwork, optional): The encoder network architecture. (Default value = None)
        decoder (DenseNetwork, optional): The decoder network architecture. (Default value = None)
        input_dim (Optional[int], optional): The input dimensions of the data, by default None.
        output_dim (Optional[int], optional): The output dimensions of the data, by default None.
        latent_dim (Optional[int], optional): The dimensions of the latent space, by default None.
        activation (Optional[Union[list, str]], optional): The activation functions used by the network, by default None.
        shallow (Optional[bool], optional): Whether the network should be shallow or not, by default False.
        devices (Union[str, list], optional): The device(s) to be used for allocating subnetworks, by default "cpu".
        name (str, optional): The name of the network, by default None.

    """

    super(AutoencoderMLP, self).__init__(name=name)

    self.weights = list()

    # This option is used when no network is provided
    # and it uses default choices for the architectures
    if encoder == None and decoder == None:
        encoder, decoder = mlp_autoencoder_auto(
            input_dim=input_dim,
            latent_dim=latent_dim,
            output_dim=output_dim,
            activation=activation,
            shallow=shallow,
        )

    # Determining the kind of device to be used for allocating the
    # subnetworks used in the DeepONet model
    self.device = self._set_device(devices=devices)

    self.encoder = self.to_wrap(entity=encoder, device=self.device)
    self.decoder = self.to_wrap(entity=decoder, device=self.device)

    self.add_module("encoder", self.encoder)
    self.add_module("decoder", self.decoder)

    self.weights += self.encoder.weights
    self.weights += self.decoder.weights

    self.last_encoder_channels = None

    self.shapes_dict = dict()

eval_projection(input_data=None) #

Evaluate the projection of the input dataset into the latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The dataset to be projected, by default None.

None

Returns:

Type Description
ndarray

np.ndarray: The dataset projected over the latent space.

Source code in simulai/models/_pytorch_models/_autoencoder.py
168
169
170
171
172
173
174
175
176
177
178
179
180
def eval_projection(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> np.ndarray:
    """Evaluate the projection of the input dataset into the latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be projected, by default None.

    Returns:
        np.ndarray: The dataset projected over the latent space.

    """
    return self.projection(input_data=input_data).detach().numpy()

forward(input_data=None) #

Execute the complete projection/reconstruction pipeline.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input dataset, by default None.

None

Returns:

Type Description
Tensor

torch.Tensor: The dataset reconstructed.

Source code in simulai/models/_pytorch_models/_autoencoder.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def forward(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    """Execute the complete projection/reconstruction pipeline.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input dataset, by default None.

    Returns:
        torch.Tensor: The dataset reconstructed.

    """
    latent = self.projection(input_data=input_data)
    reconstructed = self.reconstruction(input_data=latent)

    return reconstructed

projection(input_data=None) #

Project the input dataset into the latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The dataset to be projected, by default None.

None

Returns:

Type Description
Tensor

torch.Tensor: The dataset projected over the latent space.

Source code in simulai/models/_pytorch_models/_autoencoder.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def projection(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    """Project the input dataset into the latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be projected, by default None.

    Returns:
        torch.Tensor: The dataset projected over the latent space.

    """
    latent = self.encoder.forward(input_data=input_data)

    return latent

reconstruction(input_data=None) #

Reconstruct the latent dataset to the original one.

Parameters:

Name Type Description Default
input_data Union[Tensor, ndarray]

The dataset to be reconstructed, by default None.

None

Returns:

Type Description
Tensor

torch.Tensor: The dataset reconstructed.

Source code in simulai/models/_pytorch_models/_autoencoder.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def reconstruction(
    self, input_data: Union[torch.Tensor, np.ndarray] = None
) -> torch.Tensor:
    """Reconstruct the latent dataset to the original one.

    Args:
        input_data (Union[torch.Tensor, np.ndarray], optional): The dataset to be reconstructed, by default None.

    Returns:
        torch.Tensor: The dataset reconstructed.

    """
    reconstructed = self.decoder.forward(input_data=input_data)

    return reconstructed

summary() #

Prints the summary of the network architecture

Source code in simulai/models/_pytorch_models/_autoencoder.py
114
115
116
117
def summary(self) -> None:
    """Prints the summary of the network architecture"""
    self.encoder.summary()
    self.decoder.summary()

AutoencoderCNN#

Bases: NetworkTemplate

This is an implementation of a convolutional autoencoder as Reduced Order Model. An autoencoder architecture consists of three stages:

  • The convolutional encoder
  • The bottleneck stage, subdivided in:
    • Fully-connected encoder
    • Fully connected decoder
  • The convolutional decoder

Graphical scheme

Z -> [Conv] -> [Conv] -> ... [Conv] -> |  | | |  | -> [Conv.T] -> [Conv.T] -> ... [Conv.T] -> Z_til


                ENCODER               DENSE BOTTLENECK           DECODER
Source code in simulai/models/_pytorch_models/_autoencoder.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
class AutoencoderCNN(NetworkTemplate):
    r"""This is an implementation of a convolutional autoencoder as Reduced Order Model.
    An autoencoder architecture consists of three stages:

    - The convolutional encoder
    - The bottleneck stage, subdivided in:
        - Fully-connected encoder
        - Fully connected decoder
    - The convolutional decoder

    Graphical scheme

        Z -> [Conv] -> [Conv] -> ... [Conv] -> |  | | |  | -> [Conv.T] -> [Conv.T] -> ... [Conv.T] -> Z_til


                        ENCODER               DENSE BOTTLENECK           DECODER

    """

    def __init__(
        self,
        encoder: ConvolutionalNetwork = None,
        bottleneck_encoder: Linear = None,
        bottleneck_decoder: Linear = None,
        decoder: ConvolutionalNetwork = None,
        encoder_activation: str = "relu",
        input_dim: Optional[Tuple[int, ...]] = None,
        output_dim: Optional[Tuple[int, ...]] = None,
        latent_dim: Optional[int] = None,
        kernel_size: Optional[int] = None,
        activation: Optional[Union[list, str]] = None,
        channels: Optional[int] = None,
        case: Optional[str] = None,
        shallow: Optional[bool] = False,
        devices: Union[str, list] = "cpu",
        name: str = None,
        **kwargs,
    ) -> None:
        """Initialize the AutoencoderCNN network.

        Args:
            encoder (ConvolutionalNetwork, optional): The encoder network architecture, by default None.
            bottleneck_encoder (Linear, optional): The bottleneck encoder network architecture, by default None.
            bottleneck_decoder (Linear, optional): The bottleneck decoder network architecture, by default None.
            decoder (ConvolutionalNetwork, optional): The decoder network architecture, by default None.
            encoder_activation (str, optional): The activation function used by the encoder network, by default 'relu'.
            input_dim (Optional[Tuple[int, ...]], optional): The input dimensions of the data, by default None.
            output_dim (Optional[Tuple[int, ...]], optional): The output dimensions of the data, by default None.
            latent_dim (Optional[int], optional): The dimensions of the latent space, by default None.
            kernel_size (Optional[int], optional):  (Default value = None)
            activation (Optional[Union[list, str]], optional): The activation functions used by the network, by default None.
            channels (Optional[int], optional): The number of channels of the convolutional layers, by default None.
            case (Optional[str], optional): The type of convolutional encoder and decoder to be used, by default None.
            shallow (Optional[bool], optional): Whether the network should be shallow or not, by default False.
            devices (Union[str, list], optional): The device(s) to be used for allocating subnetworks, by default 'cpu'.
            name (str, optional): The name of the network, by default None.
            **kwargs

        """

        super(AutoencoderCNN, self).__init__(name=name)

        self.weights = list()

        # Determining the kind of device to be used for allocating the
        # subnetworks
        self.device = self._set_device(devices=devices)

        self.input_dim = None

        # If not network is provided, the automatic generation
        # pipeline is activated.
        if all(
            [
                isn == None
                for isn in [encoder, decoder, bottleneck_encoder, bottleneck_decoder]
            ]
        ):
            self.input_dim = input_dim

            (
                encoder,
                decoder,
                bottleneck_encoder,
                bottleneck_decoder,
            ) = cnn_autoencoder_auto(
                input_dim=input_dim,
                latent_dim=latent_dim,
                output_dim=output_dim,
                activation=activation,
                kernel_size=kernel_size,
                channels=channels,
                case=case,
                shallow=shallow,
            )

        self.encoder = self.to_wrap(entity=encoder, device=self.device)
        self.bottleneck_encoder = self.to_wrap(
            entity=bottleneck_encoder, device=self.device
        )
        self.bottleneck_decoder = self.to_wrap(
            entity=bottleneck_decoder, device=self.device
        )
        self.decoder = self.to_wrap(entity=decoder, device=self.device)

        self.add_module("encoder", self.encoder)
        self.add_module("bottleneck_encoder", self.bottleneck_encoder)
        self.add_module("bottleneck_decoder", self.bottleneck_decoder)
        self.add_module("decoder", self.decoder)

        self.weights += self.encoder.weights
        self.weights += self.bottleneck_encoder.weights
        self.weights += self.bottleneck_decoder.weights
        self.weights += self.decoder.weights

        self.last_encoder_channels = None
        self.before_flatten_dimension = None

        self.encoder_activation = self._get_operation(operation=encoder_activation)

        self.shapes_dict = dict()

    def summary(
        self,
        input_data: Union[np.ndarray, torch.Tensor] = None,
        input_shape: list = None,
        verbose: bool = True,
    ) -> torch.Tensor:
        """Prints the summary of the network architecture.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input dataset. (Default value = None)
            input_shape (list, optional): The shape of the input data. (Default value = None)
            verbose (bool, optional):  (Default value = True)

        Returns:
            torch.Tensor: The dataset projected over the latent space.

        """

        if verbose == True:
            if self.input_dim != None:
                input_shape = self.input_dim
            else:
                pass

            self.encoder.summary(
                input_data=input_data, input_shape=input_shape, device=self.device
            )

            if isinstance(input_data, np.ndarray):
                btnk_input = self.encoder.forward(input_data=input_data)
            else:
                assert (
                    input_shape
                ), "It is necessary to have input_shape when input_data is None."
                input_shape = self.encoder.input_size
                input_shape[0] = 1

                input_data = self.to_wrap(
                    entity=torch.ones(input_shape), device=self.device
                )

                btnk_input = self.encoder.forward(input_data=input_data)

            before_flatten_dimension = tuple(btnk_input.shape[1:])
            btnk_input = btnk_input.reshape((-1, np.prod(btnk_input.shape[1:])))

            latent = self.bottleneck_encoder.forward(input_data=btnk_input)

            self.bottleneck_encoder.summary()
            self.bottleneck_decoder.summary()

            bottleneck_output = self.encoder_activation(
                self.bottleneck_decoder.forward(input_data=latent)
            )

            bottleneck_output = bottleneck_output.reshape(
                (-1, *before_flatten_dimension)
            )

            self.decoder.summary(input_data=bottleneck_output, device=self.device)

            # Saving the content of the subnetworks to the overall architecture dictionary
            self.shapes_dict.update({"encoder": self.encoder.shapes_dict})
            self.shapes_dict.update(
                {"bottleneck_encoder": self.bottleneck_encoder.shapes_dict}
            )
            self.shapes_dict.update(
                {"bottleneck_decoder": self.bottleneck_decoder.shapes_dict}
            )
            self.shapes_dict.update({"decoder": self.decoder.shapes_dict})

        else:
            print(self)

    @as_tensor
    def projection(self, input_data: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
        """Project input dataset into the latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor]): The dataset to be projected.

        Returns:
            torch.Tensor: The dataset projected over the latent space.

        """

        btnk_input = self.encoder.forward(input_data=input_data)

        self.before_flatten_dimension = tuple(btnk_input.shape[1:])

        btnk_input = btnk_input.reshape((-1, np.prod(self.before_flatten_dimension)))

        latent = self.bottleneck_encoder.forward(input_data=btnk_input)

        return latent

    @as_tensor
    def reconstruction(
        self, input_data: Union[torch.Tensor, np.ndarray]
    ) -> torch.Tensor:
        """Reconstruct the latent dataset to the original one.

        Args:
            input_data (Union[torch.Tensor, np.ndarray]): The dataset to be reconstructed.

        Returns:
            torch.Tensor: The reconstructed dataset.

        """

        bottleneck_output = self.encoder_activation(
            self.bottleneck_decoder.forward(input_data=input_data)
        )

        bottleneck_output = bottleneck_output.reshape(
            (-1,) + self.before_flatten_dimension
        )

        reconstructed = self.decoder.forward(input_data=bottleneck_output)

        return reconstructed

    def forward(self, input_data: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
        """Execute the complete projection/reconstruction pipeline.

        Args:
            input_data (Union[np.ndarray, torch.Tensor]): The input dataset.

        Returns:
            torch.Tensor: The reconstructed dataset.

        """

        latent = self.projection(input_data=input_data)
        reconstructed = self.reconstruction(input_data=latent)

        return reconstructed

    def eval(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
        """Evaluate the autoencoder on the given dataset.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be evaluated, by default None.

        Returns:
            np.ndarray: The dataset projected over the latent space.

        """

        if isinstance(input_data, np.ndarray):
            input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

        input_data = input_data.to(self.device)

        return super().eval(input_data=input_data)

    def project(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
        """Project the input dataset into the latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be projected, by default None.

        Returns:
            np.ndarray: The dataset projected over the latent space.

        """

        projected_data = self.projection(input_data=input_data)

        return projected_data.cpu().detach().numpy()

    def reconstruct(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> np.ndarray:
        """Reconstructs the latent dataset to the original one.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be reconstructed. If not provided, uses the original input data, by default None.

        Returns:
            np.ndarray: The reconstructed dataset.

        """
        reconstructed_data = self.reconstruction(input_data=input_data)
        return reconstructed_data.cpu().detach().numpy()

__init__(encoder=None, bottleneck_encoder=None, bottleneck_decoder=None, decoder=None, encoder_activation='relu', input_dim=None, output_dim=None, latent_dim=None, kernel_size=None, activation=None, channels=None, case=None, shallow=False, devices='cpu', name=None, **kwargs) #

Initialize the AutoencoderCNN network.

Parameters:

Name Type Description Default
encoder ConvolutionalNetwork

The encoder network architecture, by default None.

None
bottleneck_encoder Linear

The bottleneck encoder network architecture, by default None.

None
bottleneck_decoder Linear

The bottleneck decoder network architecture, by default None.

None
decoder ConvolutionalNetwork

The decoder network architecture, by default None.

None
encoder_activation str

The activation function used by the encoder network, by default 'relu'.

'relu'
input_dim Optional[Tuple[int, ...]]

The input dimensions of the data, by default None.

None
output_dim Optional[Tuple[int, ...]]

The output dimensions of the data, by default None.

None
latent_dim Optional[int]

The dimensions of the latent space, by default None.

None
kernel_size Optional[int]

(Default value = None)

None
activation Optional[Union[list, str]]

The activation functions used by the network, by default None.

None
channels Optional[int]

The number of channels of the convolutional layers, by default None.

None
case Optional[str]

The type of convolutional encoder and decoder to be used, by default None.

None
shallow Optional[bool]

Whether the network should be shallow or not, by default False.

False
devices Union[str, list]

The device(s) to be used for allocating subnetworks, by default 'cpu'.

'cpu'
name str

The name of the network, by default None.

None
Source code in simulai/models/_pytorch_models/_autoencoder.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
def __init__(
    self,
    encoder: ConvolutionalNetwork = None,
    bottleneck_encoder: Linear = None,
    bottleneck_decoder: Linear = None,
    decoder: ConvolutionalNetwork = None,
    encoder_activation: str = "relu",
    input_dim: Optional[Tuple[int, ...]] = None,
    output_dim: Optional[Tuple[int, ...]] = None,
    latent_dim: Optional[int] = None,
    kernel_size: Optional[int] = None,
    activation: Optional[Union[list, str]] = None,
    channels: Optional[int] = None,
    case: Optional[str] = None,
    shallow: Optional[bool] = False,
    devices: Union[str, list] = "cpu",
    name: str = None,
    **kwargs,
) -> None:
    """Initialize the AutoencoderCNN network.

    Args:
        encoder (ConvolutionalNetwork, optional): The encoder network architecture, by default None.
        bottleneck_encoder (Linear, optional): The bottleneck encoder network architecture, by default None.
        bottleneck_decoder (Linear, optional): The bottleneck decoder network architecture, by default None.
        decoder (ConvolutionalNetwork, optional): The decoder network architecture, by default None.
        encoder_activation (str, optional): The activation function used by the encoder network, by default 'relu'.
        input_dim (Optional[Tuple[int, ...]], optional): The input dimensions of the data, by default None.
        output_dim (Optional[Tuple[int, ...]], optional): The output dimensions of the data, by default None.
        latent_dim (Optional[int], optional): The dimensions of the latent space, by default None.
        kernel_size (Optional[int], optional):  (Default value = None)
        activation (Optional[Union[list, str]], optional): The activation functions used by the network, by default None.
        channels (Optional[int], optional): The number of channels of the convolutional layers, by default None.
        case (Optional[str], optional): The type of convolutional encoder and decoder to be used, by default None.
        shallow (Optional[bool], optional): Whether the network should be shallow or not, by default False.
        devices (Union[str, list], optional): The device(s) to be used for allocating subnetworks, by default 'cpu'.
        name (str, optional): The name of the network, by default None.
        **kwargs

    """

    super(AutoencoderCNN, self).__init__(name=name)

    self.weights = list()

    # Determining the kind of device to be used for allocating the
    # subnetworks
    self.device = self._set_device(devices=devices)

    self.input_dim = None

    # If not network is provided, the automatic generation
    # pipeline is activated.
    if all(
        [
            isn == None
            for isn in [encoder, decoder, bottleneck_encoder, bottleneck_decoder]
        ]
    ):
        self.input_dim = input_dim

        (
            encoder,
            decoder,
            bottleneck_encoder,
            bottleneck_decoder,
        ) = cnn_autoencoder_auto(
            input_dim=input_dim,
            latent_dim=latent_dim,
            output_dim=output_dim,
            activation=activation,
            kernel_size=kernel_size,
            channels=channels,
            case=case,
            shallow=shallow,
        )

    self.encoder = self.to_wrap(entity=encoder, device=self.device)
    self.bottleneck_encoder = self.to_wrap(
        entity=bottleneck_encoder, device=self.device
    )
    self.bottleneck_decoder = self.to_wrap(
        entity=bottleneck_decoder, device=self.device
    )
    self.decoder = self.to_wrap(entity=decoder, device=self.device)

    self.add_module("encoder", self.encoder)
    self.add_module("bottleneck_encoder", self.bottleneck_encoder)
    self.add_module("bottleneck_decoder", self.bottleneck_decoder)
    self.add_module("decoder", self.decoder)

    self.weights += self.encoder.weights
    self.weights += self.bottleneck_encoder.weights
    self.weights += self.bottleneck_decoder.weights
    self.weights += self.decoder.weights

    self.last_encoder_channels = None
    self.before_flatten_dimension = None

    self.encoder_activation = self._get_operation(operation=encoder_activation)

    self.shapes_dict = dict()

eval(input_data=None) #

Evaluate the autoencoder on the given dataset.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The dataset to be evaluated, by default None.

None

Returns:

Type Description
ndarray

np.ndarray: The dataset projected over the latent space.

Source code in simulai/models/_pytorch_models/_autoencoder.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
def eval(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
    """Evaluate the autoencoder on the given dataset.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be evaluated, by default None.

    Returns:
        np.ndarray: The dataset projected over the latent space.

    """

    if isinstance(input_data, np.ndarray):
        input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

    input_data = input_data.to(self.device)

    return super().eval(input_data=input_data)

forward(input_data) #

Execute the complete projection/reconstruction pipeline.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input dataset.

required

Returns:

Type Description
Tensor

torch.Tensor: The reconstructed dataset.

Source code in simulai/models/_pytorch_models/_autoencoder.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def forward(self, input_data: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
    """Execute the complete projection/reconstruction pipeline.

    Args:
        input_data (Union[np.ndarray, torch.Tensor]): The input dataset.

    Returns:
        torch.Tensor: The reconstructed dataset.

    """

    latent = self.projection(input_data=input_data)
    reconstructed = self.reconstruction(input_data=latent)

    return reconstructed

project(input_data=None) #

Project the input dataset into the latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The dataset to be projected, by default None.

None

Returns:

Type Description
ndarray

np.ndarray: The dataset projected over the latent space.

Source code in simulai/models/_pytorch_models/_autoencoder.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
def project(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
    """Project the input dataset into the latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be projected, by default None.

    Returns:
        np.ndarray: The dataset projected over the latent space.

    """

    projected_data = self.projection(input_data=input_data)

    return projected_data.cpu().detach().numpy()

projection(input_data) #

Project input dataset into the latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The dataset to be projected.

required

Returns:

Type Description
Tensor

torch.Tensor: The dataset projected over the latent space.

Source code in simulai/models/_pytorch_models/_autoencoder.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
@as_tensor
def projection(self, input_data: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
    """Project input dataset into the latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor]): The dataset to be projected.

    Returns:
        torch.Tensor: The dataset projected over the latent space.

    """

    btnk_input = self.encoder.forward(input_data=input_data)

    self.before_flatten_dimension = tuple(btnk_input.shape[1:])

    btnk_input = btnk_input.reshape((-1, np.prod(self.before_flatten_dimension)))

    latent = self.bottleneck_encoder.forward(input_data=btnk_input)

    return latent

reconstruct(input_data=None) #

Reconstructs the latent dataset to the original one.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The dataset to be reconstructed. If not provided, uses the original input data, by default None.

None

Returns:

Type Description
ndarray

np.ndarray: The reconstructed dataset.

Source code in simulai/models/_pytorch_models/_autoencoder.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
def reconstruct(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> np.ndarray:
    """Reconstructs the latent dataset to the original one.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The dataset to be reconstructed. If not provided, uses the original input data, by default None.

    Returns:
        np.ndarray: The reconstructed dataset.

    """
    reconstructed_data = self.reconstruction(input_data=input_data)
    return reconstructed_data.cpu().detach().numpy()

reconstruction(input_data) #

Reconstruct the latent dataset to the original one.

Parameters:

Name Type Description Default
input_data Union[Tensor, ndarray]

The dataset to be reconstructed.

required

Returns:

Type Description
Tensor

torch.Tensor: The reconstructed dataset.

Source code in simulai/models/_pytorch_models/_autoencoder.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
@as_tensor
def reconstruction(
    self, input_data: Union[torch.Tensor, np.ndarray]
) -> torch.Tensor:
    """Reconstruct the latent dataset to the original one.

    Args:
        input_data (Union[torch.Tensor, np.ndarray]): The dataset to be reconstructed.

    Returns:
        torch.Tensor: The reconstructed dataset.

    """

    bottleneck_output = self.encoder_activation(
        self.bottleneck_decoder.forward(input_data=input_data)
    )

    bottleneck_output = bottleneck_output.reshape(
        (-1,) + self.before_flatten_dimension
    )

    reconstructed = self.decoder.forward(input_data=bottleneck_output)

    return reconstructed

summary(input_data=None, input_shape=None, verbose=True) #

Prints the summary of the network architecture.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input dataset. (Default value = None)

None
input_shape list

The shape of the input data. (Default value = None)

None
verbose bool

(Default value = True)

True

Returns:

Type Description
Tensor

torch.Tensor: The dataset projected over the latent space.

Source code in simulai/models/_pytorch_models/_autoencoder.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def summary(
    self,
    input_data: Union[np.ndarray, torch.Tensor] = None,
    input_shape: list = None,
    verbose: bool = True,
) -> torch.Tensor:
    """Prints the summary of the network architecture.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input dataset. (Default value = None)
        input_shape (list, optional): The shape of the input data. (Default value = None)
        verbose (bool, optional):  (Default value = True)

    Returns:
        torch.Tensor: The dataset projected over the latent space.

    """

    if verbose == True:
        if self.input_dim != None:
            input_shape = self.input_dim
        else:
            pass

        self.encoder.summary(
            input_data=input_data, input_shape=input_shape, device=self.device
        )

        if isinstance(input_data, np.ndarray):
            btnk_input = self.encoder.forward(input_data=input_data)
        else:
            assert (
                input_shape
            ), "It is necessary to have input_shape when input_data is None."
            input_shape = self.encoder.input_size
            input_shape[0] = 1

            input_data = self.to_wrap(
                entity=torch.ones(input_shape), device=self.device
            )

            btnk_input = self.encoder.forward(input_data=input_data)

        before_flatten_dimension = tuple(btnk_input.shape[1:])
        btnk_input = btnk_input.reshape((-1, np.prod(btnk_input.shape[1:])))

        latent = self.bottleneck_encoder.forward(input_data=btnk_input)

        self.bottleneck_encoder.summary()
        self.bottleneck_decoder.summary()

        bottleneck_output = self.encoder_activation(
            self.bottleneck_decoder.forward(input_data=latent)
        )

        bottleneck_output = bottleneck_output.reshape(
            (-1, *before_flatten_dimension)
        )

        self.decoder.summary(input_data=bottleneck_output, device=self.device)

        # Saving the content of the subnetworks to the overall architecture dictionary
        self.shapes_dict.update({"encoder": self.encoder.shapes_dict})
        self.shapes_dict.update(
            {"bottleneck_encoder": self.bottleneck_encoder.shapes_dict}
        )
        self.shapes_dict.update(
            {"bottleneck_decoder": self.bottleneck_decoder.shapes_dict}
        )
        self.shapes_dict.update({"decoder": self.decoder.shapes_dict})

    else:
        print(self)

AutoencoderKoopman#

Bases: NetworkTemplate

This is an implementation of a Koopman autoencoder as a Reduced Order Model.

A Koopman autoencoder architecture consists of five stages:

  • The convolutional encoder [Optional]
  • Fully-connected encoder
  • Koopman operator
  • Fully connected decoder
  • The convolutional decoder [Optional]

Graphical scheme

                                        (Koopman OPERATOR)
                                                 ^
                                          |      |      |
                                          |  |   |   |  |
   Z -> [Conv] -> [Conv] -> ... [Conv] -> |  | | - | |  | -> [Conv.T] -> [Conv.T] -> ... [Conv.T] -> Z_til
                                          |  |       |  |
                                          |             |

                        ENCODER          DENSE BOTTLENECK        DECODER
Source code in simulai/models/_pytorch_models/_autoencoder.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
class AutoencoderKoopman(NetworkTemplate):
    r"""This is an implementation of a Koopman autoencoder as a Reduced Order Model.

    A Koopman autoencoder architecture consists of five stages:

    - The convolutional encoder [Optional]
    - Fully-connected encoder
    - Koopman operator
    - Fully connected decoder
    - The convolutional decoder [Optional]

    Graphical scheme

                                                (Koopman OPERATOR)
                                                         ^
                                                  |      |      |
                                                  |  |   |   |  |
           Z -> [Conv] -> [Conv] -> ... [Conv] -> |  | | - | |  | -> [Conv.T] -> [Conv.T] -> ... [Conv.T] -> Z_til
                                                  |  |       |  |
                                                  |             |

                                ENCODER          DENSE BOTTLENECK        DECODER

    """

    def __init__(
        self,
        encoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
        bottleneck_encoder: Optional[Union[Linear, DenseNetwork]] = None,
        bottleneck_decoder: Optional[Union[Linear, DenseNetwork]] = None,
        decoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
        input_dim: Optional[Tuple[int, ...]] = None,
        output_dim: Optional[Tuple[int, ...]] = None,
        latent_dim: Optional[int] = None,
        activation: Optional[Union[list, str]] = None,
        channels: Optional[int] = None,
        case: Optional[str] = None,
        architecture: Optional[str] = None,
        shallow: Optional[bool] = False,
        use_batch_norm: Optional[bool] = False,
        encoder_activation: str = "relu",
        devices: Union[str, list] = "cpu",
        name: str = None,
    ) -> None:
        """Constructs a new instance of the Autoencoder

        Args:
            encoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The encoder network. Defaults to None.
            bottleneck_encoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck encoder network. Defaults to None.
            bottleneck_decoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck decoder network. Defaults to None.
            decoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The decoder network. Defaults to None.
            input_dim (Optional[Tuple[int, ...]], optional): The input dimensions. Used for automatic network generation. Defaults to None.
            output_dim (Optional[Tuple[int, ...]], optional): The output dimensions. Used for automatic network generation. Defaults to None.
            latent_dim (Optional[int], optional): The latent dimensions. Used for automatic network generation. Defaults to None.
            activation (Optional[Union[list, str]], optional): The activation functions for each layer. Used for automatic network generation. Defaults to None.
            channels (Optional[int], optional): The number of channels. Used for automatic network generation. Defaults to None.
            case (Optional[str], optional): The type of problem. Used for automatic network generation. Defaults to None.
            architecture (Optional[str], optional): The network architecture. Used for automatic network generation. Defaults to None.
            shallow (Optional[bool], optional): Whether to use shallow or deep network. Used for automatic network generation. Defaults to False.
            use_batch_norm (Optional[bool], optional):  (Default value = False)
            encoder_activation (str, optional): The activation function for the encoder. Defaults to "relu".
            devices (Union[str, list], optional): The devices to use. Defaults to "cpu".
            name (str, optional): The name of the autoencoder. Defaults to None.

        """
        super(AutoencoderKoopman, self).__init__(name=name)

        self.weights = list()

        # Determining the kind of device to be used for allocating the
        # subnetworks
        self.device = self._set_device(devices=devices)

        self.input_dim = None

        # If not network is provided, the automatic generation
        # pipeline is activated.
        if all(
            [
                isn == None
                for isn in [encoder, decoder, bottleneck_encoder, bottleneck_decoder]
            ]
        ):
            self.input_dim = input_dim

            encoder, decoder, bottleneck_encoder, bottleneck_decoder = autoencoder_auto(
                input_dim=input_dim,
                latent_dim=latent_dim,
                output_dim=output_dim,
                activation=activation,
                channels=channels,
                architecture=architecture,
                case=case,
                shallow=shallow,
                use_batch_norm=use_batch_norm,
            )

        self.encoder = encoder.to(self.device)
        self.decoder = decoder.to(self.device)

        self.add_module("encoder", self.encoder)
        self.add_module("decoder", self.decoder)

        self.weights += self.encoder.weights
        self.weights += self.decoder.weights

        # These subnetworks are optional
        if bottleneck_encoder is not None and bottleneck_decoder is not None:
            self.bottleneck_encoder = self.to_wrap(
                entity=bottleneck_encoder, device=self.device
            )
            self.bottleneck_decoder = self.to_wrap(
                entity=bottleneck_decoder, device=self.device
            )

            self.add_module("bottleneck_encoder", self.bottleneck_encoder)
            self.add_module("bottleneck_decoder", self.bottleneck_decoder)

            self.weights += self.bottleneck_encoder.weights
            self.weights += self.bottleneck_decoder.weights

        # These subnetworks are optional
        if bottleneck_encoder is not None and bottleneck_decoder is not None:
            self.bottleneck_encoder = self.to_wrap(
                entity=bottleneck_encoder, device=self.device
            )
            self.bottleneck_decoder = self.to_wrap(
                entity=bottleneck_decoder, device=self.device
            )

            self.add_module("bottleneck_encoder", self.bottleneck_encoder)
            self.add_module("bottleneck_decoder", self.bottleneck_decoder)

            self.weights += self.bottleneck_encoder.weights
            self.weights += self.bottleneck_decoder.weights

        if bottleneck_encoder is not None and bottleneck_decoder is not None:
            self.projection = self._projection_with_bottleneck
            self.reconstruction = self._reconstruction_with_bottleneck
        else:
            self.projection = self._projection
            self.reconstruction = self._reconstruction

        self.last_encoder_channels = None
        self.before_flatten_dimension = None

        self.latent_dimension = None

        if bottleneck_encoder is not None:
            self.latent_dimension = bottleneck_encoder.output_size
        else:
            self.latent_dimension = self.encoder.output_size

        self.K_op = self.to_wrap(
            entity=torch.nn.Linear(
                self.latent_dimension, self.latent_dimension, bias=False
            ).weight,
            device=self.device,
        )

        self.encoder_activation = self._get_operation(operation=encoder_activation)

        self.shapes_dict = dict()

    def summary(
        self,
        input_data: Union[np.ndarray, torch.Tensor] = None,
        input_shape: list = None,
        verbose: bool = True,
    ) -> torch.Tensor:
        if verbose == True:
            if self.input_dim != None:
                input_shape = list(self.input_dim)
            else:
                pass

            self.encoder.summary(
                input_data=input_data, input_shape=input_shape, device=self.device
            )

            self.before_flatten_dimension = tuple(self.encoder.output_size[1:])

            if isinstance(input_data, np.ndarray):
                btnk_input = self.encoder.forward(input_data=input_data)
            else:
                assert (
                    input_shape
                ), "It is necessary to have input_shape when input_data is None."
                input_shape = self.encoder.input_size
                input_shape[0] = 1

                input_data = self.to_wrap(
                    entity=torch.ones(input_shape), device=self.device
                )

                btnk_input = self.encoder.forward(input_data=input_data)

            before_flatten_dimension = tuple(btnk_input.shape[1:])
            btnk_input = btnk_input.reshape((-1, np.prod(btnk_input.shape[1:])))

            latent = self.bottleneck_encoder.forward(input_data=btnk_input)

            self.bottleneck_encoder.summary()

            print(f"The Koopman Operator has shape: {self.K_op.shape} ")

            self.bottleneck_decoder.summary()

            bottleneck_output = self.encoder_activation(
                self.bottleneck_decoder.forward(input_data=latent)
            )

            bottleneck_output = bottleneck_output.reshape(
                (-1, *before_flatten_dimension)
            )

            self.decoder.summary(input_data=bottleneck_output, device=self.device)

            # Saving the content of the subnetworks to the overall architecture dictionary
            self.shapes_dict.update({"encoder": self.encoder.shapes_dict})
            self.shapes_dict.update(
                {"bottleneck_encoder": self.bottleneck_encoder.shapes_dict}
            )
            self.shapes_dict.update(
                {"bottleneck_decoder": self.bottleneck_decoder.shapes_dict}
            )
            self.shapes_dict.update({"decoder": self.decoder.shapes_dict})

        else:
            print(self)

    @as_tensor
    def _projection_with_bottleneck(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        """Computes the projection of the input data onto the bottleneck encoder.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

        Returns:
            torch.Tensor: The projected latent representation.

        """
        btnk_input = self.encoder.forward(input_data=input_data)

        self.before_flatten_dimension = tuple(btnk_input.shape[1:])

        btnk_input = btnk_input.reshape((-1, np.prod(self.before_flatten_dimension)))

        latent = self.bottleneck_encoder.forward(input_data=btnk_input)

        return latent

    @as_tensor
    def _projection(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        """Computes the projection of the input data onto the encoder.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

        Returns:
            torch.Tensor: The projected latent representation.

        """
        latent = self.encoder.forward(input_data=input_data)

        return latent

    @as_tensor
    def _reconstruction_with_bottleneck(
        self, input_data: Union[torch.Tensor, np.ndarray] = None
    ) -> torch.Tensor:
        """Reconstructs the input data using the bottleneck decoder.

        Args:
            input_data (Union[torch.Tensor, np.ndarray], optional): The input data. Defaults to None.

        Returns:
            torch.Tensor: The reconstructed data.

        """
        bottleneck_output = self.encoder_activation(
            self.bottleneck_decoder.forward(input_data=input_data)
        )

        bottleneck_output = bottleneck_output.reshape(
            (-1,) + self.before_flatten_dimension
        )

        reconstructed = self.decoder.forward(input_data=bottleneck_output)

        return reconstructed

    @as_tensor
    def _reconstruction(
        self, input_data: Union[torch.Tensor, np.ndarray] = None
    ) -> torch.Tensor:
        """Reconstructs the input data using the decoder.

        Args:
            input_data (Union[torch.Tensor, np.ndarray], optional): The input data. Defaults to None.

        Returns:
            torch.Tensor: The reconstructed data.

        """
        reconstructed = self.decoder.forward(input_data=input_data)

        return reconstructed

    def latent_forward_m(
        self, input_data: Union[np.ndarray, torch.Tensor] = None, m: int = 1
    ) -> torch.Tensor:
        """Evaluates the operation $u^{u+m} = K^m u^{i}$

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.
            m (int, optional): The number of Koopman iterations. Defaults to 1.

        Returns:
            torch.Tensor: The computed latent representation.

        """
        return torch.matmul(input_data, torch.pow(self.K_op.T, m))

    def latent_forward(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        """Evaluates the operation u^{u+1} = K u^{i}

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

        Returns:
            torch.Tensor: The computed latent representation.

        """
        return torch.matmul(input_data, self.K_op.T)

    def reconstruction_forward(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        """Evaluates the operation Ũ = D(E(U))

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

        Returns:
            torch.Tensor: The reconstructed data.

        """
        latent = self.projection(input_data=input_data)
        reconstructed = self.reconstruction(input_data=latent)

        return reconstructed

    def reconstruction_forward_m(
        self, input_data: Union[np.ndarray, torch.Tensor] = None, m: int = 1
    ) -> torch.Tensor:
        """Evaluates the operation Ũ_m = D(K^m E(U))

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.
            m (int, optional): The number of Koopman iterations. Defaults to 1.

        Returns:
            torch.Tensor: The reconstructed data.

        """
        latent = self.projection(input_data=input_data)
        latent_m = self.latent_forward_m(input_data=latent, m=m)
        reconstructed_m = self.reconstruction(input_data=latent_m)

        return reconstructed_m

    def predict(
        self, input_data: Union[np.ndarray, torch.Tensor] = None, n_steps: int = 1
    ) -> np.ndarray:
        """Predicts the reconstructed data for the input data after n_steps extrapolation in the latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.
            n_steps (int, optional): The number of extrapolations to perform. Defaults to 1.

        Returns:
            np.ndarray: The predicted reconstructed data.

        """
        if isinstance(input_data, np.ndarray):
            input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

        predictions = list()
        latent = self.projection(input_data=input_data)
        init_latent = latent

        # Extrapolating in the latent space over n_steps steps
        for s in range(n_steps):
            latent_s = self.latent_forward(input_data=init_latent)
            init_latent = latent_s
            predictions.append(latent_s)

        predictions = torch.vstack(predictions)

        reconstructed_predictions = self.reconstruction(input_data=predictions)

        return reconstructed_predictions.detach().numpy()

    def project(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
        """Projects the input data into the latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

        Returns:
            np.ndarray: The projected data.

        """
        projected_data = self.projection(input_data=input_data)

        return projected_data.cpu().detach().numpy()

    def reconstruct(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> np.ndarray:
        """Reconstructs the input data.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

        Returns:
            np.ndarray: The reconstructed data.

        """
        reconstructed_data = self.reconstruction(input_data=input_data)

        return reconstructed_data.cpu().detach().numpy()

__init__(encoder=None, bottleneck_encoder=None, bottleneck_decoder=None, decoder=None, input_dim=None, output_dim=None, latent_dim=None, activation=None, channels=None, case=None, architecture=None, shallow=False, use_batch_norm=False, encoder_activation='relu', devices='cpu', name=None) #

Constructs a new instance of the Autoencoder

Parameters:

Name Type Description Default
encoder Union[ConvolutionalNetwork, DenseNetwork]

The encoder network. Defaults to None.

None
bottleneck_encoder Optional[Union[Linear, DenseNetwork]]

The bottleneck encoder network. Defaults to None.

None
bottleneck_decoder Optional[Union[Linear, DenseNetwork]]

The bottleneck decoder network. Defaults to None.

None
decoder Union[ConvolutionalNetwork, DenseNetwork]

The decoder network. Defaults to None.

None
input_dim Optional[Tuple[int, ...]]

The input dimensions. Used for automatic network generation. Defaults to None.

None
output_dim Optional[Tuple[int, ...]]

The output dimensions. Used for automatic network generation. Defaults to None.

None
latent_dim Optional[int]

The latent dimensions. Used for automatic network generation. Defaults to None.

None
activation Optional[Union[list, str]]

The activation functions for each layer. Used for automatic network generation. Defaults to None.

None
channels Optional[int]

The number of channels. Used for automatic network generation. Defaults to None.

None
case Optional[str]

The type of problem. Used for automatic network generation. Defaults to None.

None
architecture Optional[str]

The network architecture. Used for automatic network generation. Defaults to None.

None
shallow Optional[bool]

Whether to use shallow or deep network. Used for automatic network generation. Defaults to False.

False
use_batch_norm Optional[bool]

(Default value = False)

False
encoder_activation str

The activation function for the encoder. Defaults to "relu".

'relu'
devices Union[str, list]

The devices to use. Defaults to "cpu".

'cpu'
name str

The name of the autoencoder. Defaults to None.

None
Source code in simulai/models/_pytorch_models/_autoencoder.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
def __init__(
    self,
    encoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
    bottleneck_encoder: Optional[Union[Linear, DenseNetwork]] = None,
    bottleneck_decoder: Optional[Union[Linear, DenseNetwork]] = None,
    decoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
    input_dim: Optional[Tuple[int, ...]] = None,
    output_dim: Optional[Tuple[int, ...]] = None,
    latent_dim: Optional[int] = None,
    activation: Optional[Union[list, str]] = None,
    channels: Optional[int] = None,
    case: Optional[str] = None,
    architecture: Optional[str] = None,
    shallow: Optional[bool] = False,
    use_batch_norm: Optional[bool] = False,
    encoder_activation: str = "relu",
    devices: Union[str, list] = "cpu",
    name: str = None,
) -> None:
    """Constructs a new instance of the Autoencoder

    Args:
        encoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The encoder network. Defaults to None.
        bottleneck_encoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck encoder network. Defaults to None.
        bottleneck_decoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck decoder network. Defaults to None.
        decoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The decoder network. Defaults to None.
        input_dim (Optional[Tuple[int, ...]], optional): The input dimensions. Used for automatic network generation. Defaults to None.
        output_dim (Optional[Tuple[int, ...]], optional): The output dimensions. Used for automatic network generation. Defaults to None.
        latent_dim (Optional[int], optional): The latent dimensions. Used for automatic network generation. Defaults to None.
        activation (Optional[Union[list, str]], optional): The activation functions for each layer. Used for automatic network generation. Defaults to None.
        channels (Optional[int], optional): The number of channels. Used for automatic network generation. Defaults to None.
        case (Optional[str], optional): The type of problem. Used for automatic network generation. Defaults to None.
        architecture (Optional[str], optional): The network architecture. Used for automatic network generation. Defaults to None.
        shallow (Optional[bool], optional): Whether to use shallow or deep network. Used for automatic network generation. Defaults to False.
        use_batch_norm (Optional[bool], optional):  (Default value = False)
        encoder_activation (str, optional): The activation function for the encoder. Defaults to "relu".
        devices (Union[str, list], optional): The devices to use. Defaults to "cpu".
        name (str, optional): The name of the autoencoder. Defaults to None.

    """
    super(AutoencoderKoopman, self).__init__(name=name)

    self.weights = list()

    # Determining the kind of device to be used for allocating the
    # subnetworks
    self.device = self._set_device(devices=devices)

    self.input_dim = None

    # If not network is provided, the automatic generation
    # pipeline is activated.
    if all(
        [
            isn == None
            for isn in [encoder, decoder, bottleneck_encoder, bottleneck_decoder]
        ]
    ):
        self.input_dim = input_dim

        encoder, decoder, bottleneck_encoder, bottleneck_decoder = autoencoder_auto(
            input_dim=input_dim,
            latent_dim=latent_dim,
            output_dim=output_dim,
            activation=activation,
            channels=channels,
            architecture=architecture,
            case=case,
            shallow=shallow,
            use_batch_norm=use_batch_norm,
        )

    self.encoder = encoder.to(self.device)
    self.decoder = decoder.to(self.device)

    self.add_module("encoder", self.encoder)
    self.add_module("decoder", self.decoder)

    self.weights += self.encoder.weights
    self.weights += self.decoder.weights

    # These subnetworks are optional
    if bottleneck_encoder is not None and bottleneck_decoder is not None:
        self.bottleneck_encoder = self.to_wrap(
            entity=bottleneck_encoder, device=self.device
        )
        self.bottleneck_decoder = self.to_wrap(
            entity=bottleneck_decoder, device=self.device
        )

        self.add_module("bottleneck_encoder", self.bottleneck_encoder)
        self.add_module("bottleneck_decoder", self.bottleneck_decoder)

        self.weights += self.bottleneck_encoder.weights
        self.weights += self.bottleneck_decoder.weights

    # These subnetworks are optional
    if bottleneck_encoder is not None and bottleneck_decoder is not None:
        self.bottleneck_encoder = self.to_wrap(
            entity=bottleneck_encoder, device=self.device
        )
        self.bottleneck_decoder = self.to_wrap(
            entity=bottleneck_decoder, device=self.device
        )

        self.add_module("bottleneck_encoder", self.bottleneck_encoder)
        self.add_module("bottleneck_decoder", self.bottleneck_decoder)

        self.weights += self.bottleneck_encoder.weights
        self.weights += self.bottleneck_decoder.weights

    if bottleneck_encoder is not None and bottleneck_decoder is not None:
        self.projection = self._projection_with_bottleneck
        self.reconstruction = self._reconstruction_with_bottleneck
    else:
        self.projection = self._projection
        self.reconstruction = self._reconstruction

    self.last_encoder_channels = None
    self.before_flatten_dimension = None

    self.latent_dimension = None

    if bottleneck_encoder is not None:
        self.latent_dimension = bottleneck_encoder.output_size
    else:
        self.latent_dimension = self.encoder.output_size

    self.K_op = self.to_wrap(
        entity=torch.nn.Linear(
            self.latent_dimension, self.latent_dimension, bias=False
        ).weight,
        device=self.device,
    )

    self.encoder_activation = self._get_operation(operation=encoder_activation)

    self.shapes_dict = dict()

latent_forward(input_data=None) #

Evaluates the operation u^{u+1} = K u^{i}

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None

Returns:

Type Description
Tensor

torch.Tensor: The computed latent representation.

Source code in simulai/models/_pytorch_models/_autoencoder.py
820
821
822
823
824
825
826
827
828
829
830
831
832
def latent_forward(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    """Evaluates the operation u^{u+1} = K u^{i}

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

    Returns:
        torch.Tensor: The computed latent representation.

    """
    return torch.matmul(input_data, self.K_op.T)

latent_forward_m(input_data=None, m=1) #

Evaluates the operation $u^{u+m} = K^m u^{i}$

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None
m int

The number of Koopman iterations. Defaults to 1.

1

Returns:

Type Description
Tensor

torch.Tensor: The computed latent representation.

Source code in simulai/models/_pytorch_models/_autoencoder.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
def latent_forward_m(
    self, input_data: Union[np.ndarray, torch.Tensor] = None, m: int = 1
) -> torch.Tensor:
    """Evaluates the operation $u^{u+m} = K^m u^{i}$

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.
        m (int, optional): The number of Koopman iterations. Defaults to 1.

    Returns:
        torch.Tensor: The computed latent representation.

    """
    return torch.matmul(input_data, torch.pow(self.K_op.T, m))

predict(input_data=None, n_steps=1) #

Predicts the reconstructed data for the input data after n_steps extrapolation in the latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None
n_steps int

The number of extrapolations to perform. Defaults to 1.

1

Returns:

Type Description
ndarray

np.ndarray: The predicted reconstructed data.

Source code in simulai/models/_pytorch_models/_autoencoder.py
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
def predict(
    self, input_data: Union[np.ndarray, torch.Tensor] = None, n_steps: int = 1
) -> np.ndarray:
    """Predicts the reconstructed data for the input data after n_steps extrapolation in the latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.
        n_steps (int, optional): The number of extrapolations to perform. Defaults to 1.

    Returns:
        np.ndarray: The predicted reconstructed data.

    """
    if isinstance(input_data, np.ndarray):
        input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

    predictions = list()
    latent = self.projection(input_data=input_data)
    init_latent = latent

    # Extrapolating in the latent space over n_steps steps
    for s in range(n_steps):
        latent_s = self.latent_forward(input_data=init_latent)
        init_latent = latent_s
        predictions.append(latent_s)

    predictions = torch.vstack(predictions)

    reconstructed_predictions = self.reconstruction(input_data=predictions)

    return reconstructed_predictions.detach().numpy()

project(input_data=None) #

Projects the input data into the latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None

Returns:

Type Description
ndarray

np.ndarray: The projected data.

Source code in simulai/models/_pytorch_models/_autoencoder.py
902
903
904
905
906
907
908
909
910
911
912
913
914
def project(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
    """Projects the input data into the latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

    Returns:
        np.ndarray: The projected data.

    """
    projected_data = self.projection(input_data=input_data)

    return projected_data.cpu().detach().numpy()

reconstruct(input_data=None) #

Reconstructs the input data.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None

Returns:

Type Description
ndarray

np.ndarray: The reconstructed data.

Source code in simulai/models/_pytorch_models/_autoencoder.py
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
def reconstruct(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> np.ndarray:
    """Reconstructs the input data.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

    Returns:
        np.ndarray: The reconstructed data.

    """
    reconstructed_data = self.reconstruction(input_data=input_data)

    return reconstructed_data.cpu().detach().numpy()

reconstruction_forward(input_data=None) #

Evaluates the operation Ũ = D(E(U))

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None

Returns:

Type Description
Tensor

torch.Tensor: The reconstructed data.

Source code in simulai/models/_pytorch_models/_autoencoder.py
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
def reconstruction_forward(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    """Evaluates the operation Ũ = D(E(U))

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.

    Returns:
        torch.Tensor: The reconstructed data.

    """
    latent = self.projection(input_data=input_data)
    reconstructed = self.reconstruction(input_data=latent)

    return reconstructed

reconstruction_forward_m(input_data=None, m=1) #

Evaluates the operation Ũ_m = D(K^m E(U))

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data. Defaults to None.

None
m int

The number of Koopman iterations. Defaults to 1.

1

Returns:

Type Description
Tensor

torch.Tensor: The reconstructed data.

Source code in simulai/models/_pytorch_models/_autoencoder.py
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
def reconstruction_forward_m(
    self, input_data: Union[np.ndarray, torch.Tensor] = None, m: int = 1
) -> torch.Tensor:
    """Evaluates the operation Ũ_m = D(K^m E(U))

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data. Defaults to None.
        m (int, optional): The number of Koopman iterations. Defaults to 1.

    Returns:
        torch.Tensor: The reconstructed data.

    """
    latent = self.projection(input_data=input_data)
    latent_m = self.latent_forward_m(input_data=latent, m=m)
    reconstructed_m = self.reconstruction(input_data=latent_m)

    return reconstructed_m

AutoencoderVariational#

Bases: NetworkTemplate

This is an implementation of a Koopman autoencoder as a reduced order model.

A variational autoencoder architecture consists of five stages:

  • The convolutional encoder [Optional]
  • Fully-connected encoder
  • Gaussian noise
  • Fully connected decoder
  • The convolutional decoder [Optional]

Graphical scheme

                                              Gaussian noise
                                              ^
                                       |      |      |
                                       |  |   |   |  |
Z -> [Conv] -> [Conv] -> ... [Conv] -> |  | | - | |  | -> [Conv.T] -> [Conv.T] -> ... [Conv.T] -> Z_til
                                       |  |       |  |
                                       |             |

               ENCODER               DENSE BOTTLENECK           DECODER
Source code in simulai/models/_pytorch_models/_autoencoder.py
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
class AutoencoderVariational(NetworkTemplate):
    r"""This is an implementation of a Koopman autoencoder as a reduced order model.

    A variational autoencoder architecture consists of five stages:

    - The convolutional encoder [Optional]
    - Fully-connected encoder
    - Gaussian noise
    - Fully connected decoder
    - The convolutional decoder [Optional]

    Graphical scheme

                                                      Gaussian noise
                                                      ^
                                               |      |      |
                                               |  |   |   |  |
        Z -> [Conv] -> [Conv] -> ... [Conv] -> |  | | - | |  | -> [Conv.T] -> [Conv.T] -> ... [Conv.T] -> Z_til
                                               |  |       |  |
                                               |             |

                       ENCODER               DENSE BOTTLENECK           DECODER

    """

    def __init__(
        self,
        encoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
        bottleneck_encoder: Optional[Union[Linear, DenseNetwork]] = None,
        bottleneck_decoder: Optional[Union[Linear, DenseNetwork]] = None,
        decoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
        encoder_activation: str = "relu",
        input_dim: Optional[Tuple[int, ...]] = None,
        output_dim: Optional[Tuple[int, ...]] = None,
        latent_dim: Optional[int] = None,
        activation: Optional[Union[list, str]] = None,
        channels: Optional[int] = None,
        kernel_size: Optional[int] = None,
        case: Optional[str] = None,
        architecture: Optional[str] = None,
        use_batch_norm: Optional[bool] = False,
        shallow: Optional[bool] = False,
        scale: float = 1e-3,
        devices: Union[str, list] = "cpu",
        name: str = None,
        **kwargs,
    ) -> None:
        r"""Constructor method.

        Args:
            encoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The encoder network. Defaults to None.
            bottleneck_encoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck encoder network. Defaults to None.
            bottleneck_decoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck decoder network. Defaults to None.
            decoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The decoder network. Defaults to None.
            encoder_activation (str, optional): The activation function to use in the encoder. Defaults to "relu".
            input_dim (Optional[Tuple[int, ...]], optional): The input dimension of the data. Defaults to None.
            output_dim (Optional[Tuple[int, ...]], optional): The output dimension of the data. Defaults to None.
            latent_dim (Optional[int], optional): The size of the bottleneck layer. Defaults to None.
            activation (Optional[Union[list, str]], optional): The activation function to use in the networks. Defaults to None.
            channels (Optional[int], optional): The number of channels in the input data. Defaults to None.
            kernel_size (Optional[int], optional): Convolutional kernel size. (Default value = None)
            case (Optional[str], optional): The name of the autoencoder variant. Defaults to None.
            architecture (Optional[str], optional): The architecture of the networks. Defaults to None.
            use_batch_norm (Optional[bool], optional):  (Default value = False)
            shallow (Optional[bool], optional): Whether to use a shallow network architecture. Defaults to False.
            scale (float, optional): The scale of the initialization. Defaults to 1e-3.
            devices (Union[str, list], optional): The device(s) to use for computation. Defaults to "cpu".
            name (str, optional): The name of the autoencoder. Defaults to None.
            **kwargs

        """
        super(AutoencoderVariational, self).__init__(name=name)

        self.weights = list()

        # Determining the kind of device to be used for allocating the
        # subnetworks
        self.device = self._set_device(devices=devices)

        self.input_dim = None

        # If not network is provided, the automatic generation
        # pipeline is activated.
        if all(
            [
                isn == None
                for isn in [encoder, decoder, bottleneck_encoder, bottleneck_decoder]
            ]
        ):
            self.input_dim = input_dim

            encoder, decoder, bottleneck_encoder, bottleneck_decoder = autoencoder_auto(
                input_dim=input_dim,
                latent_dim=latent_dim,
                output_dim=output_dim,
                activation=activation,
                channels=channels,
                kernel_size=kernel_size,
                architecture=architecture,
                case=case,
                shallow=shallow,
                use_batch_norm=use_batch_norm,
                name=self.name,
                **kwargs,
            )

        self.encoder = self.to_wrap(entity=encoder, device=self.device)
        self.decoder = decoder.to(self.device)

        self.add_module("encoder", self.encoder)
        self.add_module("decoder", self.decoder)

        self.weights += self.encoder.weights
        self.weights += self.decoder.weights

        self.there_is_bottleneck = False

        # These subnetworks are optional
        if bottleneck_encoder is not None and bottleneck_decoder is not None:
            self.bottleneck_encoder = self.to_wrap(
                entity=bottleneck_encoder, device=self.device
            )
            self.bottleneck_decoder = self.to_wrap(
                entity=bottleneck_decoder, device=self.device
            )

            self.add_module("bottleneck_encoder", self.bottleneck_encoder)
            self.add_module("bottleneck_decoder", self.bottleneck_decoder)

            self.weights += self.bottleneck_encoder.weights
            self.weights += self.bottleneck_decoder.weights

            self.projection = self._projection_with_bottleneck
            self.reconstruction = self._reconstruction_with_bottleneck

            self.there_is_bottleneck = True

        else:
            self.projection = self._projection
            self.reconstruction = self._reconstruction

        self.last_encoder_channels = None
        self.before_flatten_dimension = None

        self.latent_dimension = None

        if bottleneck_encoder is not None:
            self.latent_dimension = bottleneck_encoder.output_size
        else:
            self.latent_dimension = self.encoder.output_size

        self.z_mean = self.to_wrap(
            entity=torch.nn.Linear(self.latent_dimension, self.latent_dimension),
            device=self.device,
        )

        self.z_log_var = self.to_wrap(
            entity=torch.nn.Linear(self.latent_dimension, self.latent_dimension),
            device=self.device,
        )

        self.add_module("z_mean", self.z_mean)
        self.add_module("z_log_var", self.z_log_var)

        self.weights += [self.z_mean.weight]
        self.weights += [self.z_log_var.weight]

        self.mu = None
        self.log_v = None
        self.scale = scale

        self.encoder_activation = self._get_operation(operation=encoder_activation)

        self.shapes_dict = dict()

    def summary(
        self,
        input_data: Union[np.ndarray, torch.Tensor] = None,
        input_shape: list = None,
        verbose: bool = True,
        display: bool = True,
    ) -> torch.Tensor:
        r"""Summarizes the overall architecture of the autoencoder and saves the content of the subnetworks to a dictionary.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): Input data to pass through the encoder, by default None
            input_shape (list, optional): The shape of the input data if input_data is None, by default None
            verbose (bool, optional):  (Default value = True)
            display (bool, optional):  (Default value = True)

        Returns:
            torch.Tensor: The output of the autoencoder's decoder applied to the input data.

        Raises:
            Exception: If self.input_dim is not a tuple or an integer.
            AssertionError: If input_shape is None when input_data is None.

        Note:
            The summary method calls the `summary` method of each of the subnetworks and saves the content of the subnetworks to the overall architecture dictionary. If there is a bottleneck network, it is also summarized and saved to the architecture dictionary.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> output_data = autoencoder.summary(input_data=input_data)
        """

        if verbose == True:
            if self.input_dim != None:
                if type(self.input_dim) == tuple:
                    input_shape = list(self.input_dim)
                elif type(self.input_dim) == int:
                    input_shape = [None, self.input_dim]
                else:
                    raise Exception(
                        f"input_dim is expected to be tuple or int, but received {type(self.input_dim)}"
                    )
            else:
                pass

            self.encoder.summary(
                input_data=input_data,
                input_shape=input_shape,
                device=self.device,
                display=display,
            )

            if type(self.encoder.output_size) == tuple:
                self.before_flatten_dimension = tuple(self.encoder.output_size[1:])
                input_shape = self.encoder.input_size
            elif type(self.encoder.output_size) == int:
                input_shape = [None, self.encoder.input_size]
            else:
                pass

            if isinstance(input_data, np.ndarray):
                btnk_input = self.encoder.forward(input_data=input_data)
            else:
                assert (
                    input_shape
                ), "It is necessary to have input_shape when input_data is None."

                input_shape[0] = 1

                input_data = self.to_wrap(
                    entity=torch.ones(input_shape), device=self.device
                )

                btnk_input = self.encoder.forward(input_data=input_data)

            before_flatten_dimension = tuple(btnk_input.shape[1:])
            btnk_input = btnk_input.reshape((-1, np.prod(btnk_input.shape[1:])))

            # Bottleneck networks is are optional
            if self.there_is_bottleneck:
                latent = self.bottleneck_encoder.forward(input_data=btnk_input)

                self.bottleneck_encoder.summary(display=display)
                self.bottleneck_decoder.summary(display=display)

                bottleneck_output = self.encoder_activation(
                    self.bottleneck_decoder.forward(input_data=latent)
                )

                bottleneck_output = bottleneck_output.reshape(
                    (-1, *before_flatten_dimension)
                )
            else:
                bottleneck_output = btnk_input

            self.decoder.summary(
                input_data=bottleneck_output, device=self.device, display=display
            )

            # Saving the content of the subnetworks to the overall architecture dictionary
            self.shapes_dict.update({"encoder": self.encoder.shapes_dict})

            # Bottleneck networks is are optional
            if self.there_is_bottleneck:
                self.shapes_dict.update(
                    {"bottleneck_encoder": self.bottleneck_encoder.shapes_dict}
                )
                self.shapes_dict.update(
                    {"bottleneck_decoder": self.bottleneck_decoder.shapes_dict}
                )

            self.shapes_dict.update({"decoder": self.decoder.shapes_dict})

        else:
            print(self)

    @as_tensor
    def _projection_with_bottleneck(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        r"""Applies the encoder and bottleneck encoder to input data and returns the output.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to pass through the encoder, by default None

        Returns:
            torch.Tensor: The output of the bottleneck encoder applied to the input data.
        Note:
            This function is used for projection of the input data into the bottleneck space.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> output_data = autoencoder._projection_with_bottleneck(input_data=input_data)
        """
        btnk_input = self.encoder.forward(input_data=input_data)

        self.before_flatten_dimension = tuple(self.encoder.output_size[1:])

        btnk_input = btnk_input.reshape((-1, np.prod(self.before_flatten_dimension)))

        latent = self.bottleneck_encoder.forward(input_data=btnk_input)

        return latent

    @as_tensor
    def _projection(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        r"""Applies the encoder to input data and returns the output.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to pass through the encoder, by default None

        Returns:
            torch.Tensor: The output of the encoder applied to the input data.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> output_data = autoencoder._projection(input_data=input_data)
        """
        latent = self.encoder.forward(input_data=input_data)

        return latent

    @as_tensor
    def _reconstruction_with_bottleneck(
        self, input_data: Union[torch.Tensor, np.ndarray] = None
    ) -> torch.Tensor:
        r"""Applies the bottleneck decoder and decoder to input data and returns the output.

        Args:
            input_data (Union[torch.Tensor, np.ndarray], optional): The input data to pass through the bottleneck decoder and decoder, by default None

        Returns:
            torch.Tensor: The output of the decoder applied to the bottleneck decoder's output.
        Note:
            This function is used for reconstruction of the input data from the bottleneck space.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> bottleneck_output = autoencoder._projection_with_bottleneck(input_data=input_data)
            >>> output_data = autoencoder._reconstruction_with_bottleneck(input_data=bottleneck_output)
        """
        bottleneck_output = self.encoder_activation(
            (self.bottleneck_decoder.forward(input_data=input_data))
        )

        bottleneck_output = bottleneck_output.reshape(
            (-1,) + self.before_flatten_dimension
        )

        reconstructed = self.decoder.forward(input_data=bottleneck_output)

        return reconstructed

    @as_tensor
    def _reconstruction(
        self, input_data: Union[torch.Tensor, np.ndarray] = None
    ) -> torch.Tensor:
        r"""Applies the decoder to input data and returns the output.

        Args:
            input_data (Union[torch.Tensor, np.ndarray], optional): The input data to pass through the decoder, by default None

        Returns:
            torch.Tensor: The output of the decoder applied to the input data.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> output_data = autoencoder._reconstruction(input_data=input_data)
        """
        reconstructed = self.decoder.forward(input_data=input_data)

        return reconstructed

    def Mu(
        self, input_data: Union[np.ndarray, torch.Tensor] = None, to_numpy: bool = False
    ) -> Union[np.ndarray, torch.Tensor]:
        r"""Computes the mean of the encoded input data.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and compute the mean, by default None
            to_numpy (bool, optional): If True, returns the result as a NumPy array, by default False

        Returns:
            Union[np.ndarray, torch.Tensor]: The mean of the encoded input data.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> mu = autoencoder.Mu(input_data=input_data)
        """
        latent = self.projection(input_data=input_data)

        if to_numpy == True:
            return self.z_mean(latent).detach().numpy()
        else:
            return self.z_mean(latent)

    def Sigma(
        self, input_data: Union[np.ndarray, torch.Tensor] = None, to_numpy: bool = False
    ) -> Union[np.ndarray, torch.Tensor]:
        r"""Computes the standard deviation of the encoded input data.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and compute the standard deviation, by default None
            to_numpy (bool, optional): If True, returns the result as a NumPy array, by default False

        Returns:
            Union[np.ndarray, torch.Tensor]: The standard deviation of the encoded input data.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> sigma = autoencoder.Sigma(input_data=input_data)
        """
        latent = self.projection(input_data=input_data)

        if to_numpy == True:
            return torch.exp(self.z_log_var(latent) / 2).detach().numpy()
        else:
            return torch.exp(self.z_log_var(latent) / 2)

    def CoVariance(
        self,
        input_data: Union[np.ndarray, torch.Tensor] = None,
        inv: bool = False,
        to_numpy: bool = False,
    ) -> Union[np.ndarray, torch.Tensor]:
        r"""Computes the covariance matrix of the encoded input data.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and compute the covariance matrix, by default None
            inv (bool, optional): If True, returns the inverse of the covariance matrix, by default False
            to_numpy (bool, optional): If True, returns the result as a NumPy array, by default False

        Returns:
            Union[np.ndarray, torch.Tensor]: The covariance matrix (or its inverse) of the encoded input data.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> covariance = autoencoder.CoVariance(input_data=input_data)
        """
        if inv == False:
            Sigma_inv = 1 / self.Sigma(input_data=input_data)
            covariance = torch.diag_embed(Sigma_inv)

        else:
            Sigma = self.Sigma(input_data=input_data)
            covariance = torch.diag_embed(Sigma)

        if to_numpy == True:
            return covariance.detach().numpy()
        else:
            return covariance

    def latent_gaussian_noisy(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        r"""Generates a noisy latent representation of the input data.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and generate a noisy latent representation, by default None

        Returns:
            torch.Tensor: A noisy latent representation of the input data.
        Note:
            This function adds Gaussian noise to the mean and standard deviation of the encoded input data to generate a noisy latent representation.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> noisy_latent = autoencoder.latent_gaussian_noisy(input_data=input_data)
        """
        self.mu = self.z_mean(input_data)
        self.log_v = self.z_log_var(input_data)
        eps = self.scale * torch.autograd.Variable(
            torch.randn(*self.log_v.size())
        ).type_as(self.log_v)

        return self.mu + torch.exp(self.log_v / 2.0) * eps

    def reconstruction_forward(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        r"""Applies the encoder, adds Gaussian noise to the encoded data, and then applies the decoder to generate a reconstructed output.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to pass through the autoencoder, by default None

        Returns:
            torch.Tensor: The reconstructed output of the autoencoder.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> reconstructed_data = autoencoder.reconstruction_forward(input_data=input_data)
        """
        latent = self.projection(input_data=input_data)
        latent_noisy = self.latent_gaussian_noisy(input_data=latent)
        reconstructed = self.reconstruction(input_data=latent_noisy)

        return reconstructed

    def reconstruction_eval(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> torch.Tensor:
        r"""Applies the encoder, computes the mean of the encoded data, and then applies the decoder to generate a reconstructed output.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to pass through the autoencoder, by default None

        Returns:
            torch.Tensor: The reconstructed output of the autoencoder.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> reconstructed_data = autoencoder.reconstruction_eval(input_data=input_data)
        """
        encoder_output = self.projection(input_data=input_data)
        latent = self.z_mean(encoder_output)
        reconstructed = self.reconstruction(input_data=latent)

        return reconstructed

    def project(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
        r"""Projects the input data onto the autoencoder's latent space.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to project onto the autoencoder's latent space, by default None

        Returns:
            np.ndarray: The input data projected onto the autoencoder's latent space.
        Example:

            >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> projected_data = autoencoder.project(input_data=input_data)
        """
        if isinstance(input_data, np.ndarray):
            input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

        input_data = input_data.to(self.device)

        projected_data_latent = self.Mu(input_data=input_data)

        return projected_data_latent.cpu().detach().numpy()

    def reconstruct(
        self, input_data: Union[np.ndarray, torch.Tensor] = None
    ) -> np.ndarray:
        r"""Reconstructs the input data using the trained autoencoder.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to reconstruct, by default None

        Returns:
            np.ndarray: The reconstructed data.
        Example:

            >>> autoencoder = Autoencoder(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> reconstructed_data = autoencoder.reconstruct(input_data=input_data)
        """
        if isinstance(input_data, np.ndarray):
            input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

        input_data = input_data.to(self.device)

        reconstructed_data = self.reconstruction(input_data=input_data)

        return reconstructed_data.cpu().detach().numpy()

    def eval(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
        r"""Reconstructs the input data using the mean of the encoded data.

        Args:
            input_data (Union[np.ndarray, torch.Tensor], optional): The input data to reconstruct, by default None

        Returns:
            np.ndarray: The reconstructed data.
        Example:

            >>> autoencoder = Autoencoder(input_dim=(28, 28, 1))
            >>> input_data = np.random.rand(1, 28, 28, 1)
            >>> reconstructed_data = autoencoder.eval(input_data=input_data)
        """

        if isinstance(input_data, np.ndarray):
            input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

        input_data = input_data.to(self.device)

        return self.reconstruction_eval(input_data=input_data).cpu().detach().numpy()

CoVariance(input_data=None, inv=False, to_numpy=False) #

Computes the covariance matrix of the encoded input data.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to encode and compute the covariance matrix, by default None

None
inv bool

If True, returns the inverse of the covariance matrix, by default False

False
to_numpy bool

If True, returns the result as a NumPy array, by default False

False

Returns:

Type Description
Union[ndarray, Tensor]

Union[np.ndarray, torch.Tensor]: The covariance matrix (or its inverse) of the encoded input data.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> covariance = autoencoder.CoVariance(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
def CoVariance(
    self,
    input_data: Union[np.ndarray, torch.Tensor] = None,
    inv: bool = False,
    to_numpy: bool = False,
) -> Union[np.ndarray, torch.Tensor]:
    r"""Computes the covariance matrix of the encoded input data.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and compute the covariance matrix, by default None
        inv (bool, optional): If True, returns the inverse of the covariance matrix, by default False
        to_numpy (bool, optional): If True, returns the result as a NumPy array, by default False

    Returns:
        Union[np.ndarray, torch.Tensor]: The covariance matrix (or its inverse) of the encoded input data.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> covariance = autoencoder.CoVariance(input_data=input_data)
    """
    if inv == False:
        Sigma_inv = 1 / self.Sigma(input_data=input_data)
        covariance = torch.diag_embed(Sigma_inv)

    else:
        Sigma = self.Sigma(input_data=input_data)
        covariance = torch.diag_embed(Sigma)

    if to_numpy == True:
        return covariance.detach().numpy()
    else:
        return covariance

Mu(input_data=None, to_numpy=False) #

Computes the mean of the encoded input data.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to encode and compute the mean, by default None

None
to_numpy bool

If True, returns the result as a NumPy array, by default False

False

Returns:

Type Description
Union[ndarray, Tensor]

Union[np.ndarray, torch.Tensor]: The mean of the encoded input data.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> mu = autoencoder.Mu(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
def Mu(
    self, input_data: Union[np.ndarray, torch.Tensor] = None, to_numpy: bool = False
) -> Union[np.ndarray, torch.Tensor]:
    r"""Computes the mean of the encoded input data.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and compute the mean, by default None
        to_numpy (bool, optional): If True, returns the result as a NumPy array, by default False

    Returns:
        Union[np.ndarray, torch.Tensor]: The mean of the encoded input data.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> mu = autoencoder.Mu(input_data=input_data)
    """
    latent = self.projection(input_data=input_data)

    if to_numpy == True:
        return self.z_mean(latent).detach().numpy()
    else:
        return self.z_mean(latent)

Sigma(input_data=None, to_numpy=False) #

Computes the standard deviation of the encoded input data.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to encode and compute the standard deviation, by default None

None
to_numpy bool

If True, returns the result as a NumPy array, by default False

False

Returns:

Type Description
Union[ndarray, Tensor]

Union[np.ndarray, torch.Tensor]: The standard deviation of the encoded input data.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> sigma = autoencoder.Sigma(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
def Sigma(
    self, input_data: Union[np.ndarray, torch.Tensor] = None, to_numpy: bool = False
) -> Union[np.ndarray, torch.Tensor]:
    r"""Computes the standard deviation of the encoded input data.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and compute the standard deviation, by default None
        to_numpy (bool, optional): If True, returns the result as a NumPy array, by default False

    Returns:
        Union[np.ndarray, torch.Tensor]: The standard deviation of the encoded input data.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> sigma = autoencoder.Sigma(input_data=input_data)
    """
    latent = self.projection(input_data=input_data)

    if to_numpy == True:
        return torch.exp(self.z_log_var(latent) / 2).detach().numpy()
    else:
        return torch.exp(self.z_log_var(latent) / 2)

__init__(encoder=None, bottleneck_encoder=None, bottleneck_decoder=None, decoder=None, encoder_activation='relu', input_dim=None, output_dim=None, latent_dim=None, activation=None, channels=None, kernel_size=None, case=None, architecture=None, use_batch_norm=False, shallow=False, scale=0.001, devices='cpu', name=None, **kwargs) #

Constructor method.

Parameters:

Name Type Description Default
encoder Union[ConvolutionalNetwork, DenseNetwork]

The encoder network. Defaults to None.

None
bottleneck_encoder Optional[Union[Linear, DenseNetwork]]

The bottleneck encoder network. Defaults to None.

None
bottleneck_decoder Optional[Union[Linear, DenseNetwork]]

The bottleneck decoder network. Defaults to None.

None
decoder Union[ConvolutionalNetwork, DenseNetwork]

The decoder network. Defaults to None.

None
encoder_activation str

The activation function to use in the encoder. Defaults to "relu".

'relu'
input_dim Optional[Tuple[int, ...]]

The input dimension of the data. Defaults to None.

None
output_dim Optional[Tuple[int, ...]]

The output dimension of the data. Defaults to None.

None
latent_dim Optional[int]

The size of the bottleneck layer. Defaults to None.

None
activation Optional[Union[list, str]]

The activation function to use in the networks. Defaults to None.

None
channels Optional[int]

The number of channels in the input data. Defaults to None.

None
kernel_size Optional[int]

Convolutional kernel size. (Default value = None)

None
case Optional[str]

The name of the autoencoder variant. Defaults to None.

None
architecture Optional[str]

The architecture of the networks. Defaults to None.

None
use_batch_norm Optional[bool]

(Default value = False)

False
shallow Optional[bool]

Whether to use a shallow network architecture. Defaults to False.

False
scale float

The scale of the initialization. Defaults to 1e-3.

0.001
devices Union[str, list]

The device(s) to use for computation. Defaults to "cpu".

'cpu'
name str

The name of the autoencoder. Defaults to None.

None
Source code in simulai/models/_pytorch_models/_autoencoder.py
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def __init__(
    self,
    encoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
    bottleneck_encoder: Optional[Union[Linear, DenseNetwork]] = None,
    bottleneck_decoder: Optional[Union[Linear, DenseNetwork]] = None,
    decoder: Union[ConvolutionalNetwork, DenseNetwork] = None,
    encoder_activation: str = "relu",
    input_dim: Optional[Tuple[int, ...]] = None,
    output_dim: Optional[Tuple[int, ...]] = None,
    latent_dim: Optional[int] = None,
    activation: Optional[Union[list, str]] = None,
    channels: Optional[int] = None,
    kernel_size: Optional[int] = None,
    case: Optional[str] = None,
    architecture: Optional[str] = None,
    use_batch_norm: Optional[bool] = False,
    shallow: Optional[bool] = False,
    scale: float = 1e-3,
    devices: Union[str, list] = "cpu",
    name: str = None,
    **kwargs,
) -> None:
    r"""Constructor method.

    Args:
        encoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The encoder network. Defaults to None.
        bottleneck_encoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck encoder network. Defaults to None.
        bottleneck_decoder (Optional[Union[Linear, DenseNetwork]], optional): The bottleneck decoder network. Defaults to None.
        decoder (Union[ConvolutionalNetwork, DenseNetwork], optional): The decoder network. Defaults to None.
        encoder_activation (str, optional): The activation function to use in the encoder. Defaults to "relu".
        input_dim (Optional[Tuple[int, ...]], optional): The input dimension of the data. Defaults to None.
        output_dim (Optional[Tuple[int, ...]], optional): The output dimension of the data. Defaults to None.
        latent_dim (Optional[int], optional): The size of the bottleneck layer. Defaults to None.
        activation (Optional[Union[list, str]], optional): The activation function to use in the networks. Defaults to None.
        channels (Optional[int], optional): The number of channels in the input data. Defaults to None.
        kernel_size (Optional[int], optional): Convolutional kernel size. (Default value = None)
        case (Optional[str], optional): The name of the autoencoder variant. Defaults to None.
        architecture (Optional[str], optional): The architecture of the networks. Defaults to None.
        use_batch_norm (Optional[bool], optional):  (Default value = False)
        shallow (Optional[bool], optional): Whether to use a shallow network architecture. Defaults to False.
        scale (float, optional): The scale of the initialization. Defaults to 1e-3.
        devices (Union[str, list], optional): The device(s) to use for computation. Defaults to "cpu".
        name (str, optional): The name of the autoencoder. Defaults to None.
        **kwargs

    """
    super(AutoencoderVariational, self).__init__(name=name)

    self.weights = list()

    # Determining the kind of device to be used for allocating the
    # subnetworks
    self.device = self._set_device(devices=devices)

    self.input_dim = None

    # If not network is provided, the automatic generation
    # pipeline is activated.
    if all(
        [
            isn == None
            for isn in [encoder, decoder, bottleneck_encoder, bottleneck_decoder]
        ]
    ):
        self.input_dim = input_dim

        encoder, decoder, bottleneck_encoder, bottleneck_decoder = autoencoder_auto(
            input_dim=input_dim,
            latent_dim=latent_dim,
            output_dim=output_dim,
            activation=activation,
            channels=channels,
            kernel_size=kernel_size,
            architecture=architecture,
            case=case,
            shallow=shallow,
            use_batch_norm=use_batch_norm,
            name=self.name,
            **kwargs,
        )

    self.encoder = self.to_wrap(entity=encoder, device=self.device)
    self.decoder = decoder.to(self.device)

    self.add_module("encoder", self.encoder)
    self.add_module("decoder", self.decoder)

    self.weights += self.encoder.weights
    self.weights += self.decoder.weights

    self.there_is_bottleneck = False

    # These subnetworks are optional
    if bottleneck_encoder is not None and bottleneck_decoder is not None:
        self.bottleneck_encoder = self.to_wrap(
            entity=bottleneck_encoder, device=self.device
        )
        self.bottleneck_decoder = self.to_wrap(
            entity=bottleneck_decoder, device=self.device
        )

        self.add_module("bottleneck_encoder", self.bottleneck_encoder)
        self.add_module("bottleneck_decoder", self.bottleneck_decoder)

        self.weights += self.bottleneck_encoder.weights
        self.weights += self.bottleneck_decoder.weights

        self.projection = self._projection_with_bottleneck
        self.reconstruction = self._reconstruction_with_bottleneck

        self.there_is_bottleneck = True

    else:
        self.projection = self._projection
        self.reconstruction = self._reconstruction

    self.last_encoder_channels = None
    self.before_flatten_dimension = None

    self.latent_dimension = None

    if bottleneck_encoder is not None:
        self.latent_dimension = bottleneck_encoder.output_size
    else:
        self.latent_dimension = self.encoder.output_size

    self.z_mean = self.to_wrap(
        entity=torch.nn.Linear(self.latent_dimension, self.latent_dimension),
        device=self.device,
    )

    self.z_log_var = self.to_wrap(
        entity=torch.nn.Linear(self.latent_dimension, self.latent_dimension),
        device=self.device,
    )

    self.add_module("z_mean", self.z_mean)
    self.add_module("z_log_var", self.z_log_var)

    self.weights += [self.z_mean.weight]
    self.weights += [self.z_log_var.weight]

    self.mu = None
    self.log_v = None
    self.scale = scale

    self.encoder_activation = self._get_operation(operation=encoder_activation)

    self.shapes_dict = dict()

eval(input_data=None) #

Reconstructs the input data using the mean of the encoded data.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to reconstruct, by default None

None

Returns:

Type Description
ndarray

np.ndarray: The reconstructed data.

Example:

>>> autoencoder = Autoencoder(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> reconstructed_data = autoencoder.eval(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
def eval(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
    r"""Reconstructs the input data using the mean of the encoded data.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to reconstruct, by default None

    Returns:
        np.ndarray: The reconstructed data.
    Example:

        >>> autoencoder = Autoencoder(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> reconstructed_data = autoencoder.eval(input_data=input_data)
    """

    if isinstance(input_data, np.ndarray):
        input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

    input_data = input_data.to(self.device)

    return self.reconstruction_eval(input_data=input_data).cpu().detach().numpy()

latent_gaussian_noisy(input_data=None) #

Generates a noisy latent representation of the input data.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to encode and generate a noisy latent representation, by default None

None

Returns:

Type Description
Tensor

torch.Tensor: A noisy latent representation of the input data.

Note: This function adds Gaussian noise to the mean and standard deviation of the encoded input data to generate a noisy latent representation. Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> noisy_latent = autoencoder.latent_gaussian_noisy(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
def latent_gaussian_noisy(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    r"""Generates a noisy latent representation of the input data.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to encode and generate a noisy latent representation, by default None

    Returns:
        torch.Tensor: A noisy latent representation of the input data.
    Note:
        This function adds Gaussian noise to the mean and standard deviation of the encoded input data to generate a noisy latent representation.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> noisy_latent = autoencoder.latent_gaussian_noisy(input_data=input_data)
    """
    self.mu = self.z_mean(input_data)
    self.log_v = self.z_log_var(input_data)
    eps = self.scale * torch.autograd.Variable(
        torch.randn(*self.log_v.size())
    ).type_as(self.log_v)

    return self.mu + torch.exp(self.log_v / 2.0) * eps

project(input_data=None) #

Projects the input data onto the autoencoder's latent space.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to project onto the autoencoder's latent space, by default None

None

Returns:

Type Description
ndarray

np.ndarray: The input data projected onto the autoencoder's latent space.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> projected_data = autoencoder.project(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
def project(self, input_data: Union[np.ndarray, torch.Tensor] = None) -> np.ndarray:
    r"""Projects the input data onto the autoencoder's latent space.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to project onto the autoencoder's latent space, by default None

    Returns:
        np.ndarray: The input data projected onto the autoencoder's latent space.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> projected_data = autoencoder.project(input_data=input_data)
    """
    if isinstance(input_data, np.ndarray):
        input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

    input_data = input_data.to(self.device)

    projected_data_latent = self.Mu(input_data=input_data)

    return projected_data_latent.cpu().detach().numpy()

reconstruct(input_data=None) #

Reconstructs the input data using the trained autoencoder.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to reconstruct, by default None

None

Returns:

Type Description
ndarray

np.ndarray: The reconstructed data.

Example:

>>> autoencoder = Autoencoder(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> reconstructed_data = autoencoder.reconstruct(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
def reconstruct(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> np.ndarray:
    r"""Reconstructs the input data using the trained autoencoder.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to reconstruct, by default None

    Returns:
        np.ndarray: The reconstructed data.
    Example:

        >>> autoencoder = Autoencoder(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> reconstructed_data = autoencoder.reconstruct(input_data=input_data)
    """
    if isinstance(input_data, np.ndarray):
        input_data = torch.from_numpy(input_data.astype(ARRAY_DTYPE))

    input_data = input_data.to(self.device)

    reconstructed_data = self.reconstruction(input_data=input_data)

    return reconstructed_data.cpu().detach().numpy()

reconstruction_eval(input_data=None) #

Applies the encoder, computes the mean of the encoded data, and then applies the decoder to generate a reconstructed output.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to pass through the autoencoder, by default None

None

Returns:

Type Description
Tensor

torch.Tensor: The reconstructed output of the autoencoder.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> reconstructed_data = autoencoder.reconstruction_eval(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
def reconstruction_eval(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    r"""Applies the encoder, computes the mean of the encoded data, and then applies the decoder to generate a reconstructed output.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to pass through the autoencoder, by default None

    Returns:
        torch.Tensor: The reconstructed output of the autoencoder.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> reconstructed_data = autoencoder.reconstruction_eval(input_data=input_data)
    """
    encoder_output = self.projection(input_data=input_data)
    latent = self.z_mean(encoder_output)
    reconstructed = self.reconstruction(input_data=latent)

    return reconstructed

reconstruction_forward(input_data=None) #

Applies the encoder, adds Gaussian noise to the encoded data, and then applies the decoder to generate a reconstructed output.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

The input data to pass through the autoencoder, by default None

None

Returns:

Type Description
Tensor

torch.Tensor: The reconstructed output of the autoencoder.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> reconstructed_data = autoencoder.reconstruction_forward(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
def reconstruction_forward(
    self, input_data: Union[np.ndarray, torch.Tensor] = None
) -> torch.Tensor:
    r"""Applies the encoder, adds Gaussian noise to the encoded data, and then applies the decoder to generate a reconstructed output.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): The input data to pass through the autoencoder, by default None

    Returns:
        torch.Tensor: The reconstructed output of the autoencoder.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> reconstructed_data = autoencoder.reconstruction_forward(input_data=input_data)
    """
    latent = self.projection(input_data=input_data)
    latent_noisy = self.latent_gaussian_noisy(input_data=latent)
    reconstructed = self.reconstruction(input_data=latent_noisy)

    return reconstructed

summary(input_data=None, input_shape=None, verbose=True, display=True) #

Summarizes the overall architecture of the autoencoder and saves the content of the subnetworks to a dictionary.

Parameters:

Name Type Description Default
input_data Union[ndarray, Tensor]

Input data to pass through the encoder, by default None

None
input_shape list

The shape of the input data if input_data is None, by default None

None
verbose bool

(Default value = True)

True
display bool

(Default value = True)

True

Returns:

Type Description
Tensor

torch.Tensor: The output of the autoencoder's decoder applied to the input data.

Raises:

Type Description
Exception

If self.input_dim is not a tuple or an integer.

AssertionError

If input_shape is None when input_data is None.

Note

The summary method calls the summary method of each of the subnetworks and saves the content of the subnetworks to the overall architecture dictionary. If there is a bottleneck network, it is also summarized and saved to the architecture dictionary.

Example:

>>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
>>> input_data = np.random.rand(1, 28, 28, 1)
>>> output_data = autoencoder.summary(input_data=input_data)
Source code in simulai/models/_pytorch_models/_autoencoder.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
def summary(
    self,
    input_data: Union[np.ndarray, torch.Tensor] = None,
    input_shape: list = None,
    verbose: bool = True,
    display: bool = True,
) -> torch.Tensor:
    r"""Summarizes the overall architecture of the autoencoder and saves the content of the subnetworks to a dictionary.

    Args:
        input_data (Union[np.ndarray, torch.Tensor], optional): Input data to pass through the encoder, by default None
        input_shape (list, optional): The shape of the input data if input_data is None, by default None
        verbose (bool, optional):  (Default value = True)
        display (bool, optional):  (Default value = True)

    Returns:
        torch.Tensor: The output of the autoencoder's decoder applied to the input data.

    Raises:
        Exception: If self.input_dim is not a tuple or an integer.
        AssertionError: If input_shape is None when input_data is None.

    Note:
        The summary method calls the `summary` method of each of the subnetworks and saves the content of the subnetworks to the overall architecture dictionary. If there is a bottleneck network, it is also summarized and saved to the architecture dictionary.
    Example:

        >>> autoencoder = AutoencoderVariational(input_dim=(28, 28, 1))
        >>> input_data = np.random.rand(1, 28, 28, 1)
        >>> output_data = autoencoder.summary(input_data=input_data)
    """

    if verbose == True:
        if self.input_dim != None:
            if type(self.input_dim) == tuple:
                input_shape = list(self.input_dim)
            elif type(self.input_dim) == int:
                input_shape = [None, self.input_dim]
            else:
                raise Exception(
                    f"input_dim is expected to be tuple or int, but received {type(self.input_dim)}"
                )
        else:
            pass

        self.encoder.summary(
            input_data=input_data,
            input_shape=input_shape,
            device=self.device,
            display=display,
        )

        if type(self.encoder.output_size) == tuple:
            self.before_flatten_dimension = tuple(self.encoder.output_size[1:])
            input_shape = self.encoder.input_size
        elif type(self.encoder.output_size) == int:
            input_shape = [None, self.encoder.input_size]
        else:
            pass

        if isinstance(input_data, np.ndarray):
            btnk_input = self.encoder.forward(input_data=input_data)
        else:
            assert (
                input_shape
            ), "It is necessary to have input_shape when input_data is None."

            input_shape[0] = 1

            input_data = self.to_wrap(
                entity=torch.ones(input_shape), device=self.device
            )

            btnk_input = self.encoder.forward(input_data=input_data)

        before_flatten_dimension = tuple(btnk_input.shape[1:])
        btnk_input = btnk_input.reshape((-1, np.prod(btnk_input.shape[1:])))

        # Bottleneck networks is are optional
        if self.there_is_bottleneck:
            latent = self.bottleneck_encoder.forward(input_data=btnk_input)

            self.bottleneck_encoder.summary(display=display)
            self.bottleneck_decoder.summary(display=display)

            bottleneck_output = self.encoder_activation(
                self.bottleneck_decoder.forward(input_data=latent)
            )

            bottleneck_output = bottleneck_output.reshape(
                (-1, *before_flatten_dimension)
            )
        else:
            bottleneck_output = btnk_input

        self.decoder.summary(
            input_data=bottleneck_output, device=self.device, display=display
        )

        # Saving the content of the subnetworks to the overall architecture dictionary
        self.shapes_dict.update({"encoder": self.encoder.shapes_dict})

        # Bottleneck networks is are optional
        if self.there_is_bottleneck:
            self.shapes_dict.update(
                {"bottleneck_encoder": self.bottleneck_encoder.shapes_dict}
            )
            self.shapes_dict.update(
                {"bottleneck_decoder": self.bottleneck_decoder.shapes_dict}
            )

        self.shapes_dict.update({"decoder": self.decoder.shapes_dict})

    else:
        print(self)