netloader 3.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,847 @@
1
+ """
2
+ Transformations that can be inverted
3
+ """
4
+ from __future__ import annotations
5
+ from warnings import warn
6
+ from types import ModuleType
7
+ from abc import ABC, abstractmethod
8
+ from typing import Callable, Any, Generic, TypeVar, cast, overload
9
+
10
+ import torch
11
+ import numpy as np
12
+ from torch import Tensor
13
+ from numpy import ndarray
14
+
15
+ from netloader.data import Data
16
+ from netloader.utils.types import ArrayLike, ArrayT, ArrayCT
17
+
18
+
19
+ InArrayCT = TypeVar('InArrayCT', ndarray, Tensor)
20
+ OutArrayCT = TypeVar('OutArrayCT', ndarray, Tensor)
21
+
22
+
23
+ class BaseTransform(ABC, Generic[InArrayCT, OutArrayCT]):
24
+ """
25
+ Base transformation class that other types of transforms build from.
26
+ """
27
+ def __init__(self) -> None:
28
+ # Adds all transform classes to list of safe PyTorch classes when loading saved architectures
29
+ torch.serialization.add_safe_globals([self.__class__])
30
+
31
+ @overload
32
+ def __call__(self, x: InArrayCT, *, back: bool = ...) -> OutArrayCT: ...
33
+
34
+ @overload
35
+ def __call__(self, x: Data[InArrayCT], *, back: bool = ...) -> Data[OutArrayCT]: ...
36
+
37
+ @overload
38
+ def __call__(
39
+ self,
40
+ x: InArrayCT,
41
+ *,
42
+ back: bool = ...,
43
+ uncertainty: InArrayCT) -> tuple[OutArrayCT, OutArrayCT]: ...
44
+
45
+ def __call__(
46
+ self,
47
+ x: InArrayCT | Data[InArrayCT],
48
+ *,
49
+ back: bool = False,
50
+ uncertainty: InArrayCT | None = None) -> OutArrayCT | Data[OutArrayCT] | tuple[OutArrayCT, OutArrayCT]:
51
+ """
52
+ Calling function returns the forward, backwards or uncertainty propagation of the
53
+ transformation
54
+
55
+ Parameters
56
+ ----------
57
+ x : ArrayT | Data[ArrayCT]
58
+ Input array or tensor of shape (N,...), where N is the number of elements
59
+ back : bool, Optional
60
+ If the inverse transformation should be applied, default = False
61
+ uncertainty : ArrayT, Optional
62
+ Corresponding uncertainties for the input data for uncertainty propagation of shape
63
+ (N,...)
64
+
65
+ Returns
66
+ -------
67
+ ArrayT | Data[ArrayCT] | tuple[ArrayT, ArrayT]
68
+ Transformed array or tensor of shape (N,...) and propagated uncertainties of shape
69
+ (N,...) if provided
70
+ """
71
+ if isinstance(x, Data):
72
+ return Data(*self._call(x.data, back=back, uncertainty=x.uncertainty))
73
+ return self._call(x, back=back, uncertainty=uncertainty)
74
+
75
+ def __repr__(self) -> str:
76
+ """
77
+ Representation of the transformation
78
+
79
+ Returns
80
+ -------
81
+ str
82
+ Representation string
83
+ """
84
+ return f'{self.__class__.__name__}({self.extra_repr()})'
85
+
86
+ def __getstate__(self) -> dict[str, Any]:
87
+ """
88
+ Returns a dictionary containing the state of the transformation for pickling
89
+
90
+ Returns
91
+ -------
92
+ dict[str, Any]
93
+ Dictionary containing the state of the transformation
94
+ """
95
+ return {}
96
+
97
+ def __setstate__(self, state: dict[str, Any]) -> None:
98
+ """
99
+ Sets the state of the transformation for pickling
100
+
101
+ Parameters
102
+ ----------
103
+ state : dict[str, Any]
104
+ Dictionary containing the state of the transformation
105
+ """
106
+
107
+ def _call(
108
+ self,
109
+ x: InArrayCT,
110
+ back: bool = False,
111
+ uncertainty: InArrayCT | None = None) -> OutArrayCT | tuple[OutArrayCT, OutArrayCT]:
112
+ """
113
+ Internal calling function to handle the different types of calls
114
+
115
+ Parameters
116
+ ----------
117
+ x : ArrayT
118
+ Input array or tensor of shape (N,...) and type float, where N is the number of elements
119
+ back : bool, Optional
120
+ If the inverse transformation should be applied, default = False
121
+ uncertainty : ArrayT | None, Optional
122
+ Corresponding uncertainties for the input data for uncertainty propagation of shape
123
+ (N,...) and type float
124
+
125
+ Returns
126
+ -------
127
+ ArrayT | tuple[ArrayT]
128
+ Transformed array or tensor and propagated uncertainties if provided of shape
129
+ (N,...) and type float
130
+ """
131
+ if back and uncertainty is not None:
132
+ return self.backward_grad(x, uncertainty)
133
+ if back:
134
+ return self.backward(x)
135
+ if uncertainty is not None:
136
+ return self.forward_grad(x, uncertainty)
137
+ return self.forward(x)
138
+
139
+ @abstractmethod
140
+ def forward(self, x: InArrayCT) -> OutArrayCT:
141
+ """
142
+ Forward pass of the transformation
143
+
144
+ Parameters
145
+ ----------
146
+ x : ArrayT
147
+ Input array or tensor of shape (N,...), where N is the number of elements
148
+
149
+ Returns
150
+ -------
151
+ ArrayT
152
+ Transformed array or tensor of shape (N,...)
153
+ """
154
+
155
+ @abstractmethod
156
+ def backward(self, x: InArrayCT) -> OutArrayCT:
157
+ """
158
+ Backwards pass to invert the transformation
159
+
160
+ Parameters
161
+ ----------
162
+ x : ArrayT
163
+ Input array or tensor of shape (N,...), where N is the number of elements
164
+
165
+ Returns
166
+ -------
167
+ ArrayT
168
+ Untransformed array or tensor of shape (N,...)
169
+ """
170
+
171
+ @abstractmethod
172
+ def forward_grad(self, x: InArrayCT, uncertainty: InArrayCT) -> tuple[OutArrayCT, OutArrayCT]:
173
+ """
174
+ Forward pass of the transformation and uncertainty propagation
175
+
176
+ Parameters
177
+ ----------
178
+ x : ArrayT
179
+ Input array or tensor of shape (N,...), where N is the number of elements
180
+ uncertainty : ArrayT
181
+ Uncertainty of the input array or tensor of shape (N,...)
182
+
183
+ Returns
184
+ -------
185
+ tuple[ArrayT, ArrayT]
186
+ Transformed array or tensor of shape (N,...) and transformed uncertainty of shape
187
+ (N,...)
188
+ """
189
+
190
+ @abstractmethod
191
+ def backward_grad(self, x: InArrayCT, uncertainty: InArrayCT) -> tuple[OutArrayCT, OutArrayCT]:
192
+ """
193
+ Backwards pass to invert the transformation and uncertainty propagation
194
+
195
+ Parameters
196
+ ----------
197
+ x : ArrayT
198
+ Input array or tensor of shape (N,...), where N is the number of elements
199
+ uncertainty : ArrayT
200
+ Uncertainty of the input array or tensor of shape (N,...)
201
+
202
+ Returns
203
+ -------
204
+ tuple[ArrayT, ArrayT]
205
+ Untransformed array or tensor of shape (N,...) and untransformed uncertainty of shape
206
+ (N,...)
207
+ """
208
+
209
+ def extra_repr(self) -> str:
210
+ """
211
+ Additional representation of the transformation
212
+
213
+ Returns
214
+ -------
215
+ str
216
+ Transform specific representation
217
+ """
218
+ return ''
219
+
220
+
221
+ class BaseTypeTransform(BaseTransform[ArrayCT, ArrayCT]):
222
+ """
223
+ Base transformation class that other types of transforms build from, with the same input and
224
+ output type.
225
+ """
226
+ def forward(self, x: ArrayCT) -> ArrayCT:
227
+ """
228
+ Forward pass of the transformation
229
+
230
+ Parameters
231
+ ----------
232
+ x : ArrayT
233
+ Input array or tensor of shape (N,...), where N is the number of elements
234
+
235
+ Returns
236
+ -------
237
+ ArrayT
238
+ Transformed array or tensor of shape (N,...)
239
+ """
240
+ return x
241
+
242
+ def backward(self, x: ArrayCT) -> ArrayCT:
243
+ """
244
+ Backwards pass to invert the transformation
245
+
246
+ Parameters
247
+ ----------
248
+ x : ArrayT
249
+ Input array or tensor of shape (N,...), where N is the number of elements
250
+
251
+ Returns
252
+ -------
253
+ ArrayT
254
+ Untransformed array or tensor of shape (N,...)
255
+ """
256
+ return x
257
+
258
+ def forward_grad(self, x: ArrayCT, uncertainty: ArrayCT) -> tuple[ArrayCT, ArrayCT]:
259
+ """
260
+ Forward pass of the transformation and uncertainty propagation
261
+
262
+ Parameters
263
+ ----------
264
+ x : ArrayT
265
+ Input array or tensor of shape (N,...), where N is the number of elements
266
+ uncertainty : ArrayT
267
+ Uncertainty of the input array or tensor of shape (N,...)
268
+
269
+ Returns
270
+ -------
271
+ tuple[ArrayT, ArrayT]
272
+ Transformed array or tensor of shape (N,...) and transformed uncertainty of shape
273
+ (N,...)
274
+ """
275
+ return self(x), uncertainty
276
+
277
+ def backward_grad(self, x: ArrayCT, uncertainty: ArrayCT) -> tuple[ArrayCT, ArrayCT]:
278
+ """
279
+ Backwards pass to invert the transformation and uncertainty propagation
280
+
281
+ Parameters
282
+ ----------
283
+ x : ArrayT
284
+ Input array or tensor of shape (N,...), where N is the number of elements
285
+ uncertainty : ArrayT
286
+ Uncertainty of the input array or tensor of shape (N,...)
287
+
288
+ Returns
289
+ -------
290
+ tuple[ArrayT, ArrayT]
291
+ Untransformed array or tensor of shape (N,...) and untransformed uncertainty of shape
292
+ (N,...)
293
+ """
294
+ return self(x, back=True), uncertainty
295
+
296
+
297
+ class Index(BaseTypeTransform):
298
+ """
299
+ Slices the input along a given dimension assuming the input meets the required shape.
300
+ """
301
+ def __init__(
302
+ self,
303
+ *,
304
+ dim: int = -1,
305
+ in_shape: tuple[int, ...] | None = None,
306
+ slice_: slice = slice(None)) -> None:
307
+ """
308
+ Parameters
309
+ ----------
310
+ dim : int, Optional
311
+ Dimension to slice over, default = -1
312
+ in_shape : tuple[int, ...] | None, Optional
313
+ Target shape ignoring batch size so that the slice only occurs if the input has the
314
+ same shape to prevent repeated slicing, if any dimension has a shape of -1, then the
315
+ size of the dimension will be ignored
316
+ slice_ : slice, Optional
317
+ Slicing object, default = slice(None)
318
+ """
319
+ super().__init__()
320
+ self._shape: tuple[int, ...] = tuple(in_shape or ())
321
+ self._slice: list[slice] = [slice(None)] * (len(self._shape) or 1)
322
+ self._slice[dim] = slice_
323
+
324
+ def __getstate__(self) -> dict[str, Any]:
325
+ return {'in_shape': self._shape, 'slice': [(s.start, s.stop, s.step) for s in self._slice]}
326
+
327
+ def __setstate__(self, state: dict[str, Any]) -> None:
328
+ self._shape = state['in_shape']
329
+
330
+ if isinstance(state['slice'][0], slice):
331
+ warn(
332
+ f'{self.__class__.__name__} transform is saved in old non-weights safe '
333
+ 'format and is deprecated, please resave the transform in the new format using '
334
+ 'BaseArchitecture.save()',
335
+ DeprecationWarning,
336
+ stacklevel=2,
337
+ )
338
+ self._slice = state['slice']
339
+ else:
340
+ self._slice = [slice(*s) for s in state['slice']]
341
+
342
+ def forward(self, x: ArrayT) -> ArrayT:
343
+ idxs: ndarray = np.array(np.array(self._shape) != -1)
344
+
345
+ if np.array(np.array(x.shape[1:])[idxs] != np.array(self._shape)[idxs]).any():
346
+ return super().forward(x)
347
+ return cast(ArrayT, x[:, *self._slice])
348
+
349
+ def forward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
350
+ return self(x), self(uncertainty)
351
+
352
+ def extra_repr(self) -> str:
353
+ return f'in_shape: {self._shape}, slice: {self._slice}'
354
+
355
+
356
+ class Log(BaseTypeTransform):
357
+ """
358
+ Logarithm transform.
359
+ """
360
+ def __init__(self, *, base: float = 10, idxs: list[int] | None = None) -> None:
361
+ """
362
+ Parameters
363
+ ----------
364
+ base : float, Optional
365
+ Base of the logarithm, default = 10
366
+ idxs : list[int] | None, Optional
367
+ Indices to slice the last dimension to perform the log on
368
+ """
369
+ super().__init__()
370
+ self._base: float = base
371
+ self._idxs: list[int] | None = idxs
372
+
373
+ def __getstate__(self) -> dict[str, Any]:
374
+ return {'base': self._base, 'idxs': self._idxs}
375
+
376
+ def __setstate__(self, state: dict[str, Any]) -> None:
377
+ try:
378
+ self._base = state['base']
379
+ self._idxs = state['idxs']
380
+ except KeyError:
381
+ self._base = state['_base']
382
+ self._idxs = state['_idxs']
383
+ warn(f'{self.__class__.__name__} transform is saved in an old format and is '
384
+ f'deprecated, please resave the transform', DeprecationWarning, stacklevel=2)
385
+
386
+ def forward(self, x: ArrayT) -> ArrayT:
387
+ module: ModuleType = torch if isinstance(x, Tensor) else np
388
+ logs: dict[float, Callable] = {module.e: module.log, 10: module.log10, 2: module.log2}
389
+ x = x.clone() if isinstance(x, Tensor) else x.copy()
390
+
391
+ if self._base in logs and self._idxs is not None:
392
+ x[..., self._idxs] = logs[self._base](x[..., self._idxs])
393
+ elif self._base in logs:
394
+ x = logs[self._base](x)
395
+ elif self._idxs is not None:
396
+ x[..., self._idxs] = module.log(x[..., self._idxs]) / np.log(self._base)
397
+ else:
398
+ x = module.log(x) / np.log(self._base)
399
+ return x
400
+
401
+ def backward(self, x: ArrayT) -> ArrayT:
402
+ x = x.clone() if isinstance(x, Tensor) else x.copy()
403
+
404
+ if self._idxs is not None:
405
+ x[..., self._idxs] = self._base ** x[..., self._idxs] # type: ignore
406
+ else:
407
+ x = cast(ArrayT, self._base ** x)
408
+ return x
409
+
410
+ def forward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
411
+ module: ModuleType = torch if isinstance(x, Tensor) else np
412
+ uncertainty = uncertainty.clone() if isinstance(uncertainty, Tensor) else uncertainty.copy()
413
+
414
+ if self._idxs is not None:
415
+ uncertainty[..., self._idxs] /= x[..., self._idxs] * np.log(self._base)
416
+ else:
417
+ uncertainty /= x * np.log(self._base)
418
+ return self(x), module.abs(uncertainty)
419
+
420
+ def backward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
421
+ module: ModuleType = torch if isinstance(x, Tensor) else np
422
+ uncertainty = uncertainty.clone() if isinstance(uncertainty, Tensor) else uncertainty.copy()
423
+ x = self(x, back=True)
424
+
425
+ if self._idxs is not None:
426
+ uncertainty[..., self._idxs] *= x[..., self._idxs]
427
+ else:
428
+ uncertainty *= x
429
+
430
+ if self._base != module.e and self._idxs is not None:
431
+ uncertainty[..., self._idxs] *= np.log(self._base)
432
+ elif self._base != module.e:
433
+ uncertainty *= np.log(self._base)
434
+ return x, module.abs(uncertainty)
435
+
436
+ def extra_repr(self) -> str:
437
+ return f'base: {self._base}, idxs: {self._idxs}'
438
+
439
+
440
+ class MinClamp(BaseTypeTransform):
441
+ """
442
+ Clamps the minimum value to be the smallest positive value.
443
+ """
444
+ def __init__(self, *, dim: int | None = None, idxs: list[int] | None = None) -> None:
445
+ """
446
+ Parameters
447
+ ----------
448
+ dim : int | None, Optional
449
+ Dimension to take the minimum value over
450
+ idxs : list[int] | None, Optional
451
+ Indices to slice the last dimension to perform the min clamp on
452
+ """
453
+ super().__init__()
454
+ self._dim: int | None = dim
455
+ self._idxs: list[int] | None = idxs
456
+
457
+ def __getstate__(self) -> dict[str, Any]:
458
+ return {'dim': self._dim, 'idxs': self._idxs}
459
+
460
+ def __setstate__(self, state: dict[str, Any]) -> None:
461
+ try:
462
+ self._dim = state['dim']
463
+ self._idxs = state['idxs']
464
+ except KeyError:
465
+ self._dim = state['_dim']
466
+ self._idxs = state['_idxs']
467
+ warn(f'{self.__class__.__name__} transform is saved in an old format and is '
468
+ f'deprecated, please resave the transform', DeprecationWarning, stacklevel=2)
469
+
470
+ def forward(self, x: ArrayT) -> ArrayT:
471
+ kwargs: dict[str, Any]
472
+ module: ModuleType = torch if isinstance(x, Tensor) else np
473
+ x_clamp: ArrayT
474
+ min_count: ArrayT
475
+
476
+ if isinstance(x, Tensor):
477
+ kwargs = {'dim': self._dim, 'keepdim': True}
478
+ else:
479
+ kwargs = {'axis': self._dim, 'keepdims': True}
480
+
481
+ if self._idxs is None:
482
+ min_count = module.amin(module.where(x > 0, x, module.max(x)), **kwargs)
483
+ x = module.maximum(x, min_count)
484
+ else:
485
+ x_clamp = cast(ArrayT, x[..., self._idxs])
486
+ min_count = module.amin(module.where(
487
+ x_clamp > 0,
488
+ x_clamp,
489
+ module.max(x_clamp),
490
+ ), **kwargs)
491
+ x[..., self._idxs] = module.maximum(x_clamp, min_count)
492
+ return x
493
+
494
+ def extra_repr(self) -> str:
495
+ return f'dim: {self._dim}, idxs: {self._idxs}'
496
+
497
+
498
+ class MultiTransform(BaseTransform):
499
+ """
500
+ Applies multiple transformations.
501
+
502
+ Attributes
503
+ ----------
504
+ transforms : list[BaseTransform]
505
+ List of transformations
506
+ """
507
+ def __init__(self, *args: BaseTransform) -> None:
508
+ """
509
+ Parameters
510
+ ----------
511
+ *args : BaseTransform
512
+ Transformations
513
+ """
514
+ super().__init__()
515
+ self.transforms: list[BaseTransform]
516
+
517
+ if isinstance(args[0], list):
518
+ warn(
519
+ 'List of transforms is deprecated, pass transforms as arguments directly',
520
+ DeprecationWarning,
521
+ stacklevel=2,
522
+ )
523
+ self.transforms = args[0]
524
+ else:
525
+ self.transforms = list(args)
526
+
527
+ @overload
528
+ def __getitem__(self, item: int) -> BaseTransform: ...
529
+
530
+ @overload
531
+ def __getitem__(self, item: slice) -> MultiTransform: ...
532
+
533
+ def __getitem__(self, item: int | slice) -> BaseTransform:
534
+ """
535
+ Gets a transform or a slice of transforms from the list of transforms.
536
+
537
+ Parameters
538
+ ----------
539
+ item : int | slice
540
+ Index or slice of the transform(s) to get
541
+
542
+ Returns
543
+ -------
544
+ BaseTransform
545
+ Transform or MultiTransform containing the slice of transforms
546
+ """
547
+ if isinstance(item, int):
548
+ return self.transforms[item]
549
+ return MultiTransform(*self.transforms[item])
550
+
551
+ def __getstate__(self) -> dict[str, Any]:
552
+ return {'transforms': [(
553
+ transform.__class__.__name__,
554
+ transform.__getstate__(),
555
+ ) for transform in self.transforms]}
556
+
557
+ def __setstate__(self, state: dict[str, Any]) -> None:
558
+ self.transforms = []
559
+
560
+ if isinstance(state['transforms'][0], BaseTransform):
561
+ warn(
562
+ f'{self.__class__.__name__} transform is saved in old non-weights safe '
563
+ 'format and is deprecated, please resave the transform in the new format using '
564
+ 'BaseArchitecture.save()',
565
+ DeprecationWarning,
566
+ stacklevel=2,
567
+ )
568
+ self.transforms = state['transforms']
569
+ else:
570
+ for name, transform in state['transforms']:
571
+ self.transforms.append(globals()[name]())
572
+ self.transforms[-1].__setstate__(transform)
573
+
574
+ def forward(self, x: ArrayLike) -> ArrayLike:
575
+ transform: BaseTransform
576
+
577
+ for transform in self.transforms:
578
+ x = transform(x)
579
+ return x
580
+
581
+ def backward(self, x: ArrayLike) -> ArrayLike:
582
+ transform: BaseTransform
583
+
584
+ for transform in self.transforms[::-1]:
585
+ x = transform(x, back=True)
586
+ return x
587
+
588
+ def forward_grad(self, x: ArrayLike, uncertainty: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
589
+ transform: BaseTransform
590
+
591
+ for transform in self.transforms:
592
+ x, uncertainty = transform(x, uncertainty=uncertainty)
593
+ return x, uncertainty
594
+
595
+ def backward_grad(self, x: ArrayLike, uncertainty: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
596
+ transform: BaseTransform
597
+
598
+ for transform in self.transforms[::-1]:
599
+ x, uncertainty = transform(x, back=True, uncertainty=uncertainty)
600
+ return x, uncertainty
601
+
602
+ def append(self, transform: BaseTransform) -> None:
603
+ """
604
+ Appends a transform to the list of transforms
605
+
606
+ Parameters
607
+ ----------
608
+ transform : BaseTransform
609
+ Transform to append to the list of transforms
610
+ """
611
+ self.transforms.append(transform)
612
+
613
+ def extend(self, *args: BaseTransform) -> None:
614
+ """
615
+ Extends the list of transforms with another list of transforms
616
+
617
+ Parameters
618
+ ----------
619
+ *args : BaseTransform
620
+ Transformations to extend MultiTransform transforms list
621
+ """
622
+ self.transforms.extend([*args])
623
+
624
+ def extra_repr(self) -> str:
625
+ transform_repr: str
626
+ extra_repr: str = ''
627
+
628
+ for i, transform in enumerate(self.transforms):
629
+ transform_repr = repr(transform).replace('\n', '\n\t')
630
+ extra_repr += f"\n\t({i}): {transform_repr},"
631
+
632
+ return f'{extra_repr}\n'
633
+
634
+
635
+ class Normalise(BaseTypeTransform):
636
+ """
637
+ Normalises the data to zero mean and unit variance, or between 0 and 1.
638
+
639
+ Attributes
640
+ ----------
641
+ offset : ndarray
642
+ Offset to subtract from the data
643
+ scale : ndarray
644
+ Scale to divide the data by
645
+ """
646
+ @overload
647
+ def __init__(self, *, offset: ndarray, scale: ndarray) -> None:
648
+ ...
649
+
650
+ @overload
651
+ def __init__(
652
+ self,
653
+ *,
654
+ data: ArrayLike,
655
+ mean: bool = ...,
656
+ dim: int | tuple[int, ...] | None = ...) -> None:
657
+ ...
658
+
659
+ def __init__(
660
+ self,
661
+ *,
662
+ mean: bool = True,
663
+ dim: int | tuple[int, ...] | None = None,
664
+ offset: ndarray | None = None,
665
+ scale: ndarray | None = None,
666
+ data: ArrayLike | None = None) -> None:
667
+ """
668
+ Parameters
669
+ ----------
670
+ mean : bool, Optional
671
+ If data should be normalised to zero mean and unit variance, or between 0 and 1,
672
+ default = True
673
+ dim : int | tuple[int, ...] | None, Optional
674
+ Dimensions to normalise over, if None, all dimensions will be normalised over
675
+ offset : ndarray | None, Optional
676
+ Offset to subtract from the data if data argument is None
677
+ scale : ndarray | None, Optional
678
+ Scale to divide the data if data argument is None
679
+ data : ArrayLike | None, Optional
680
+ Data to normalise with shape (N,...), where N is the number of elements
681
+ """
682
+ super().__init__()
683
+ self.offset: ndarray
684
+ self.scale: ndarray
685
+
686
+ if isinstance(data, Tensor):
687
+ data = data.cpu().numpy()
688
+ elif data is None:
689
+ self.offset = np.array([0]) if offset is None else offset
690
+ self.scale = np.array([1]) if scale is None else scale
691
+ return
692
+
693
+ if mean:
694
+ self.offset = np.mean(data, axis=dim, keepdims=True)
695
+ self.scale = np.std(data, axis=dim, keepdims=True)
696
+ else:
697
+ self.offset = np.amin(data, axis=dim, keepdims=True)
698
+ self.scale = np.amax(data, axis=dim, keepdims=True) - self.offset
699
+
700
+ self.scale = np.where(self.scale == 0, 1, self.scale)
701
+
702
+ def __getstate__(self) -> dict[str, Any]:
703
+ return {'offset': self.offset.tolist(), 'scale': self.scale.tolist()}
704
+
705
+ def __setstate__(self, state: dict[str, Any]) -> None:
706
+ self.offset = np.array(state['offset'])
707
+ self.scale = np.array(state['scale'])
708
+
709
+ def forward(self, x: ArrayT) -> ArrayT:
710
+ if isinstance(x, Tensor):
711
+ return cast(ArrayT, (x - x.new_tensor(self.offset)) / x.new_tensor(self.scale))
712
+ return cast(ArrayT, (x - self.offset) / self.scale)
713
+
714
+ def backward(self, x: ArrayT) -> ArrayT:
715
+ if isinstance(x, Tensor):
716
+ return cast(ArrayT, x * x.new_tensor(self.scale) + x.new_tensor(self.offset))
717
+ return cast(ArrayT, x * self.scale + self.offset)
718
+
719
+ def forward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
720
+ return self(x), cast(ArrayT, uncertainty / (
721
+ uncertainty.new_tensor(self.scale) if isinstance(uncertainty, Tensor) else
722
+ self.scale
723
+ ))
724
+
725
+ def backward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
726
+ return self(x, back=True), cast(ArrayT, uncertainty * (
727
+ uncertainty.new_tensor(self.scale) if isinstance(uncertainty, Tensor) else
728
+ self.scale
729
+ ))
730
+
731
+ def extra_repr(self) -> str:
732
+ if 1 < self.offset.size <= 10:
733
+ return (f"\n\toffset: {np.vectorize(lambda x: f'{x:.2g}')(self.offset)},"
734
+ f"\n\tscale: {np.vectorize(lambda x: f'{x:.2g}')(self.scale)},\n")
735
+
736
+ if self.offset.size > 1:
737
+ return f'offset shape: {self.offset.shape}, scale shape: {self.scale.shape}'
738
+
739
+ return f'offset: {self.offset.item():.2g}, scale: {self.scale.item():.2g}'
740
+
741
+
742
+ class NumpyTensor(BaseTransform):
743
+ """
744
+ Converts Numpy arrays to PyTorch tensors.
745
+
746
+ Attributes
747
+ ----------
748
+ dtype : torch.dtype
749
+ Data type of the tensor
750
+ """
751
+ def __init__(self, *, dtype: torch.dtype = torch.float32) -> None:
752
+ """
753
+ Parameters
754
+ ----------
755
+ dtype : dtype, Optional
756
+ Data type of the tensor, default = torch.float32
757
+ """
758
+ super().__init__()
759
+ self.dtype: torch.dtype = dtype
760
+
761
+ def __getstate__(self) -> dict[str, Any]:
762
+ return {'dtype': self.dtype}
763
+
764
+ def __setstate__(self, state: dict[str, Any]) -> None:
765
+ try:
766
+ self.dtype = state['dtype']
767
+ except KeyError:
768
+ self.dtype = torch.float32
769
+
770
+ def forward(self, x: ArrayLike) -> Tensor:
771
+ if isinstance(x, ndarray):
772
+ return torch.from_numpy(x).type(self.dtype)
773
+ return x
774
+
775
+ def backward(self, x: ArrayLike) -> ndarray:
776
+ if isinstance(x, Tensor):
777
+ return x.detach().cpu().numpy()
778
+ return x
779
+
780
+ def forward_grad(self, x: ArrayLike, uncertainty: ArrayLike) -> tuple[Tensor, Tensor]:
781
+ return self(x), self(uncertainty)
782
+
783
+ def backward_grad(self, x: ArrayLike, uncertainty: ArrayLike) -> tuple[ndarray, ndarray]:
784
+ return self.backward(x), self.backward(uncertainty)
785
+
786
+
787
+ class Reshape(BaseTypeTransform):
788
+ """
789
+ Reshapes the data.
790
+ """
791
+ def __init__(
792
+ self,
793
+ *,
794
+ in_shape: list[int] | None = None,
795
+ out_shape: list[int] | None = None) -> None:
796
+ """
797
+ Parameters
798
+ ----------
799
+ in_shape : list[int] | None, Optional
800
+ Original shape of the data
801
+ out_shape : list[int] | None, Optional
802
+ Output shape of the data
803
+ """
804
+ super().__init__()
805
+ self._in_shape: list[int] | None = in_shape
806
+ self._out_shape: list[int] | None = out_shape
807
+
808
+ def __getstate__(self) -> dict[str, Any]:
809
+ return {'in_shape': self._in_shape, 'out_shape': self._out_shape}
810
+
811
+ def __setstate__(self, state: dict[str, Any]) -> None:
812
+ self._in_shape = state['in_shape']
813
+ self._out_shape = state['out_shape']
814
+
815
+ def forward(self, x: ArrayT) -> ArrayT:
816
+ if self._out_shape is None:
817
+ return super().forward(x)
818
+ return getattr(x, 'view' if isinstance(x, Tensor) else 'reshape')(len(x), *self._out_shape)
819
+
820
+ def backward(self, x: ArrayT) -> ArrayT:
821
+ if self._in_shape is None:
822
+ return super().backward(x)
823
+ return getattr(x, 'view' if isinstance(x, Tensor) else 'reshape')(len(x), *self._in_shape)
824
+
825
+ def forward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
826
+ return self(x), self(uncertainty)
827
+
828
+ def backward_grad(self, x: ArrayT, uncertainty: ArrayT) -> tuple[ArrayT, ArrayT]:
829
+ return self(x, back=True), self(uncertainty, back=True)
830
+
831
+ def extra_repr(self) -> str:
832
+ return f'in_shape: {self._in_shape}, out_shape: {self._out_shape}'
833
+
834
+
835
+ __all__ = [
836
+ 'BaseTransform',
837
+ 'BaseTypeTransform',
838
+ 'Index',
839
+ 'Log',
840
+ 'MinClamp',
841
+ 'MultiTransform',
842
+ 'Normalise',
843
+ 'NumpyTensor',
844
+ 'Reshape',
845
+ 'InArrayCT',
846
+ 'OutArrayCT',
847
+ ]