autoemulate 0.3.0__tar.gz → 0.3.2__tar.gz

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.
Files changed (85) hide show
  1. {autoemulate-0.3.0 → autoemulate-0.3.2}/PKG-INFO +1 -1
  2. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/compare.py +41 -14
  3. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/compare.py +8 -4
  4. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/data/preprocessors.py +10 -12
  5. autoemulate-0.3.2/autoemulate/experimental/data/utils.py +483 -0
  6. autoemulate-0.3.2/autoemulate/experimental/device.py +180 -0
  7. autoemulate-0.3.2/autoemulate/experimental/emulators/__init__.py +13 -0
  8. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/base.py +95 -47
  9. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/gaussian_process/exact.py +30 -37
  10. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/lightgbm.py +32 -48
  11. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/neural_processes/conditional_neural_process.py +49 -31
  12. autoemulate-0.3.2/autoemulate/experimental/emulators/random_forest.py +69 -0
  13. autoemulate-0.3.2/autoemulate/experimental/emulators/svm.py +85 -0
  14. autoemulate-0.3.2/autoemulate/experimental/exploratory/active_matter/README.md +26 -0
  15. autoemulate-0.3.2/autoemulate/experimental/exploratory/active_matter/active_matter.ipynb +231318 -0
  16. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/base.py +3 -3
  17. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/stream.py +12 -2
  18. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/model_selection.py +20 -10
  19. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/tuner.py +34 -14
  20. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/types.py +4 -0
  21. autoemulate-0.3.2/autoemulate/history_matching.py +450 -0
  22. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/history_matching_dashboard.py +16 -41
  23. autoemulate-0.3.2/autoemulate/sensitivity_analysis.py +623 -0
  24. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/base.py +35 -23
  25. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/naghavi_cardiac_ModularCirc.py +0 -24
  26. {autoemulate-0.3.0 → autoemulate-0.3.2}/pyproject.toml +1 -1
  27. autoemulate-0.3.0/autoemulate/experimental/data/utils.py +0 -176
  28. autoemulate-0.3.0/autoemulate/experimental/data/validation.py +0 -221
  29. autoemulate-0.3.0/autoemulate/experimental/emulators/__init__.py +0 -5
  30. autoemulate-0.3.0/autoemulate/history_matching.py +0 -419
  31. autoemulate-0.3.0/autoemulate/sensitivity_analysis.py +0 -310
  32. {autoemulate-0.3.0 → autoemulate-0.3.2}/LICENSE +0 -0
  33. {autoemulate-0.3.0 → autoemulate-0.3.2}/README.md +0 -0
  34. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/__init__.py +0 -0
  35. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/cross_validate.py +0 -0
  36. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/data_splitting.py +0 -0
  37. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/datasets.py +0 -0
  38. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/__init__.py +0 -0
  39. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/conditional_neural_process.py +0 -0
  40. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/conditional_neural_process_attn.py +0 -0
  41. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process.py +0 -0
  42. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_mogp.py +0 -0
  43. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_mt.py +0 -0
  44. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_sklearn.py +0 -0
  45. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/__init__.py +0 -0
  46. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/early_stopping_criterion.py +0 -0
  47. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/poly_mean.py +0 -0
  48. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/polynomial_features.py +0 -0
  49. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gradient_boosting.py +0 -0
  50. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/light_gbm.py +0 -0
  51. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_net_sk.py +0 -0
  52. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/__init__.py +0 -0
  53. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/cnp_module.py +0 -0
  54. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/cnp_module_attn.py +0 -0
  55. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/datasets.py +0 -0
  56. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/gp_module.py +0 -0
  57. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/losses.py +0 -0
  58. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/polynomials.py +0 -0
  59. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/radial_basis_functions.py +0 -0
  60. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/random_forest.py +0 -0
  61. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/support_vector_machines.py +0 -0
  62. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/__init__.py +0 -0
  63. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/gaussian_process/__init__.py +0 -0
  64. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/__init__.py +0 -0
  65. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/membership.py +0 -0
  66. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/pool.py +0 -0
  67. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/simulations/__init__.py +0 -0
  68. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental_design.py +0 -0
  69. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/hyperparam_searching.py +0 -0
  70. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/logging_config.py +0 -0
  71. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/metrics.py +0 -0
  72. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/model_processing.py +0 -0
  73. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/model_registry.py +0 -0
  74. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/plotting.py +1 -1
  75. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/preprocess_target.py +0 -0
  76. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/printing.py +0 -0
  77. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/save.py +0 -0
  78. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/__init__.py +0 -0
  79. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/circ_utils.py +0 -0
  80. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/epidemic.py +0 -0
  81. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/flow_functions.py +0 -0
  82. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/projectile.py +0 -0
  83. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/reaction_diffusion.py +0 -0
  84. {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/utils.py +0 -0
  85. {autoemulate-0.3.0 → autoemulate-0.3.2}/misc/AE_logo_final.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: autoemulate
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A python package for semi-automated emulation
5
5
  License: MIT
6
6
  Author: AutoEmulate contributors (see our GitHub page)
@@ -24,7 +24,8 @@ from autoemulate.preprocess_target import get_dim_reducer
24
24
  from autoemulate.preprocess_target import NonTrainableTransformer
25
25
  from autoemulate.printing import _print_setup
26
26
  from autoemulate.save import ModelSerialiser
27
- from autoemulate.sensitivity_analysis import _plot_sensitivity_analysis
27
+ from autoemulate.sensitivity_analysis import _plot_morris_analysis
28
+ from autoemulate.sensitivity_analysis import _plot_sobol_analysis
28
29
  from autoemulate.sensitivity_analysis import _sensitivity_analysis
29
30
  from autoemulate.utils import _check_cv
30
31
  from autoemulate.utils import _ensure_2d
@@ -473,15 +474,15 @@ class AutoEmulate:
473
474
  means = _sum_cvs({full_name: cv_results[full_name]}, metric)
474
475
  current_score = means.iloc[0][f"{metric}"]
475
476
 
476
- # Compare with current best
477
- if (metric == "r2" and current_score > best_score) or (
478
- metric != "r2" and current_score < best_score
479
- ):
480
- best_score = current_score
481
- for model in prep_data["models"]:
482
- if get_model_name(model) == full_name:
477
+ # Find the model with the correct name that has the best score
478
+ for model in prep_data["models"]:
479
+ if get_model_name(model) == full_name:
480
+ if (metric == "r2" and current_score > best_score) or (
481
+ metric != "r2" and current_score < best_score
482
+ ):
483
+ best_score = current_score
483
484
  best_model = model
484
- break
485
+ break
485
486
 
486
487
  if best_model is None:
487
488
  raise ValueError(
@@ -979,7 +980,14 @@ class AutoEmulate:
979
980
  return fig
980
981
 
981
982
  def sensitivity_analysis(
982
- self, model=None, problem=None, N=1024, conf_level=0.95, as_df=True
983
+ self,
984
+ model=None,
985
+ method="sobol",
986
+ problem=None,
987
+ N=1024,
988
+ conf_level=0.95,
989
+ as_df=True,
990
+ **plot_kwargs,
983
991
  ):
984
992
  """Perform Sobol sensitivity analysis on a fitted emulator.
985
993
 
@@ -1038,6 +1046,10 @@ class AutoEmulate:
1038
1046
  The analysis requires N * (2D + 2) model evaluations, where D is the number of input
1039
1047
  parameters. For example, with N=1024 and 5 parameters, this requires 12,288 evaluations.
1040
1048
  """
1049
+ self.method = method
1050
+ if method not in ["sobol", "morris"]:
1051
+ raise ValueError(f"Unknown method: {method}. Must be 'sobol' or 'morris'.")
1052
+
1041
1053
  if model is None:
1042
1054
  if not hasattr(self, "best_model"):
1043
1055
  raise RuntimeError("Must run compare() before sensitivity_analysis()")
@@ -1045,10 +1057,22 @@ class AutoEmulate:
1045
1057
  self.logger.info(
1046
1058
  f"No model provided, using {get_model_name(model)}, which had the highest average cross-validation score, refitted on full data."
1047
1059
  )
1048
- Si = _sensitivity_analysis(model, problem, self.X, N, conf_level, as_df)
1049
- return Si
1050
1060
 
1051
- def plot_sensitivity_analysis(self, results, index="S1", n_cols=None, figsize=None):
1061
+ df_results = _sensitivity_analysis(
1062
+ model=model,
1063
+ method=method,
1064
+ problem=problem,
1065
+ X=self.X,
1066
+ N=N,
1067
+ conf_level=conf_level,
1068
+ as_df=as_df,
1069
+ )
1070
+
1071
+ return df_results
1072
+
1073
+ def plot_sensitivity_analysis(
1074
+ self, results, index="S1", param_groups=None, n_cols=None, figsize=None
1075
+ ):
1052
1076
  """
1053
1077
  Plot the sensitivity analysis results.
1054
1078
 
@@ -1068,4 +1092,7 @@ class AutoEmulate:
1068
1092
  Figure size as (width, height) in inches.If None, automatically calculated.
1069
1093
 
1070
1094
  """
1071
- return _plot_sensitivity_analysis(results, index, n_cols, figsize)
1095
+ if self.method == "sobol":
1096
+ return _plot_sobol_analysis(results, index, n_cols, figsize)
1097
+ elif self.method == "morris":
1098
+ return _plot_morris_analysis(results, param_groups, n_cols, figsize)
@@ -5,23 +5,27 @@ from typing import Any
5
5
  import numpy as np
6
6
  from sklearn.model_selection import BaseCrossValidator, KFold
7
7
 
8
- from autoemulate.experimental.data.utils import InputTypeMixin
8
+ from autoemulate.experimental.data.utils import ConversionMixin
9
+ from autoemulate.experimental.device import TorchDeviceMixin
9
10
  from autoemulate.experimental.emulators import ALL_EMULATORS
10
11
  from autoemulate.experimental.emulators.base import Emulator
11
12
  from autoemulate.experimental.model_selection import cross_validate
12
13
  from autoemulate.experimental.tuner import Tuner
13
- from autoemulate.experimental.types import InputLike
14
+ from autoemulate.experimental.types import DeviceLike, InputLike
14
15
 
15
16
 
16
- class AutoEmulate(InputTypeMixin):
17
+ class AutoEmulate(ConversionMixin, TorchDeviceMixin):
17
18
  def __init__(
18
19
  self,
19
20
  x: InputLike,
20
21
  y: InputLike,
21
22
  models: list[type[Emulator]] | None = None,
23
+ device: DeviceLike | None = None,
22
24
  ):
25
+ TorchDeviceMixin.__init__(self, device=device)
23
26
  # TODO: refactor in https://github.com/alan-turing-institute/autoemulate/issues/400
24
27
  x, y = self._convert_to_tensors(x, y)
28
+ x, y = self._move_tensors_to_device(x, y)
25
29
 
26
30
  # Set default models if None
27
31
  updated_models = self.get_models(models)
@@ -73,7 +77,7 @@ class AutoEmulate(InputTypeMixin):
73
77
  def compare(
74
78
  self, n_iter: int = 10, cv: type[BaseCrossValidator] = KFold
75
79
  ) -> dict[str, dict[str, Any]]:
76
- tuner = Tuner(self.train_val, y=None, n_iter=n_iter)
80
+ tuner = Tuner(self.train_val, y=None, n_iter=n_iter, device=self.device)
77
81
  models_evaluated = {}
78
82
  for model_cls in self.models:
79
83
  scores, configs = tuner.run(model_cls)
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
3
  import torch
4
- from autoemulate.experimental.types import InputLike
4
+ from autoemulate.experimental.types import TensorLike
5
5
 
6
6
 
7
7
  class Preprocessor(ABC):
@@ -9,14 +9,14 @@ class Preprocessor(ABC):
9
9
  def __init__(*args, **kwargs): ...
10
10
 
11
11
  @abstractmethod
12
- def preprocess(self, x: InputLike) -> InputLike: ...
12
+ def preprocess(self, x: TensorLike) -> TensorLike: ...
13
13
 
14
14
 
15
15
  class Standardizer(Preprocessor):
16
- mean: torch.Tensor
17
- std: torch.Tensor
16
+ mean: TensorLike
17
+ std: TensorLike
18
18
 
19
- def __init__(self, mean: torch.Tensor, std: torch.Tensor):
19
+ def __init__(self, mean: TensorLike, std: TensorLike):
20
20
  if len(mean.shape) != 2:
21
21
  raise ValueError(f"mean is expected to be 2D, shape passed ({mean.shape})")
22
22
  if len(std.shape) != 2:
@@ -28,19 +28,17 @@ class Standardizer(Preprocessor):
28
28
  self.mean = mean
29
29
  self.std = std
30
30
 
31
- def preprocess(self, x):
31
+ def preprocess(self, x: TensorLike) -> TensorLike:
32
32
  """
33
33
  Parameters
34
34
  ----------
35
35
 
36
- x : torch.Tensor
36
+ x : TensorLike
37
37
  The input tensor to be standardized.
38
38
 
39
39
  """
40
- if not isinstance(x, torch.Tensor):
41
- raise ValueError(f"Expected 2D torch.Tensor, actual type {type(x)}")
40
+ if not isinstance(x, TensorLike):
41
+ raise ValueError(f"Expected 2D TensorLike, actual type {type(x)}")
42
42
  if len(x.shape) != 2:
43
- raise ValueError(
44
- f"Expected 2D torch.Tensor, actual shape dim {len(x.shape)}"
45
- )
43
+ raise ValueError(f"Expected 2D TensorLike, actual shape dim {len(x.shape)}")
46
44
  return (x - self.mean) / self.std
@@ -0,0 +1,483 @@
1
+ import numpy as np
2
+ import torch
3
+ from autoemulate.experimental.types import (
4
+ InputLike,
5
+ OutputLike,
6
+ TensorLike,
7
+ TorchScalarDType,
8
+ )
9
+ from sklearn.utils.validation import check_X_y
10
+ from torch.utils.data import DataLoader, Dataset, Subset, TensorDataset, random_split
11
+
12
+
13
+ class ConversionMixin:
14
+ """
15
+ Mixin class to convert input data to pytorch Datasets and DataLoaders.
16
+ """
17
+
18
+ def _convert_to_dataset(
19
+ self,
20
+ x: InputLike,
21
+ y: InputLike | None = None,
22
+ ) -> Dataset:
23
+ """
24
+ Convert input data to pytorch Dataset.
25
+ """
26
+ # Convert input to Dataset if not already
27
+ if isinstance(x, np.ndarray):
28
+ x = torch.tensor(x, dtype=torch.float32)
29
+ if isinstance(y, np.ndarray):
30
+ y = torch.tensor(y, dtype=torch.float32)
31
+
32
+ if isinstance(x, (torch.Tensor, np.ndarray)) and isinstance(
33
+ y, (torch.Tensor, np.ndarray)
34
+ ):
35
+ dataset = TensorDataset(x, y)
36
+ elif isinstance(x, (torch.Tensor, np.ndarray)) and y is None:
37
+ dataset = TensorDataset(x)
38
+ elif isinstance(x, Dataset) and y is None:
39
+ dataset = x
40
+ elif isinstance(x, DataLoader) and y is None:
41
+ dataset = x.dataset
42
+ else:
43
+ raise ValueError(
44
+ f"Unsupported type for x ({type(x)}). Must be numpy array or PyTorch "
45
+ "tensor."
46
+ )
47
+
48
+ return dataset
49
+
50
+ def _convert_to_dataloader(
51
+ self,
52
+ x: InputLike,
53
+ y: InputLike | None = None,
54
+ batch_size: int = 16,
55
+ shuffle: bool = True,
56
+ ) -> DataLoader:
57
+ """
58
+ Convert input data to pytorch DataLoaders.
59
+ """
60
+ if isinstance(x, DataLoader) and y is None:
61
+ dataloader = x
62
+ elif isinstance(x, DataLoader) and y is not None:
63
+ raise ValueError(
64
+ f"Since x is already a DataLoader, expect y to be None, not {type(y)}."
65
+ )
66
+ else:
67
+ dataset = self._convert_to_dataset(x, y)
68
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
69
+ return dataloader
70
+
71
+ def _convert_to_tensors(
72
+ self,
73
+ x: InputLike,
74
+ y: InputLike | None = None,
75
+ dtype: torch.dtype = torch.float32,
76
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
77
+ """
78
+ Convert InputLike x, y to Tensor or tuple of Tensors.
79
+ """
80
+ dataset = self._convert_to_dataset(x, y)
81
+
82
+ # Handle Subset of TensorDataset
83
+ if isinstance(dataset, Subset):
84
+ if isinstance(dataset.dataset, TensorDataset):
85
+ tensors = dataset.dataset.tensors
86
+ indices = dataset.indices
87
+
88
+ # Use indexing to get subset tensors
89
+ subset_tensors = tuple(tensor[indices] for tensor in tensors)
90
+ dataset = TensorDataset(*subset_tensors)
91
+ else:
92
+ raise ValueError(
93
+ f"Subset must wrap a TensorDataset. Found {type(dataset.dataset)}."
94
+ )
95
+
96
+ if isinstance(dataset, TensorDataset):
97
+ if len(dataset.tensors) > 2:
98
+ raise ValueError(
99
+ f"Dataset must have 2 or fewer tensors. Found "
100
+ f"{len(dataset.tensors)}."
101
+ )
102
+ if len(dataset.tensors) == 2:
103
+ x, y = dataset.tensors
104
+ assert x.ndim == 2
105
+ assert y.ndim in (1, 2)
106
+ # Ensure always 2D tensors
107
+ if y.ndim == 1:
108
+ y = y.unsqueeze(1)
109
+ return x.to(dtype), y.to(dtype)
110
+ if len(dataset.tensors) == 1:
111
+ (x,) = dataset.tensors
112
+ assert x.ndim == 2
113
+ return x.to(dtype)
114
+ msg = "Number of tensors returned must be greater than zero."
115
+ raise ValueError(msg)
116
+ # Note: this error will never be raised, the same error is raised in
117
+ # _convert_to_dataset
118
+ raise ValueError(
119
+ f"Unsupported type for x ({type(x)}). Must be numpy array or PyTorch "
120
+ "tensor."
121
+ )
122
+
123
+ def _convert_to_numpy(
124
+ self,
125
+ x: InputLike,
126
+ y: InputLike | None = None,
127
+ ) -> tuple[np.ndarray, np.ndarray | None]:
128
+ """
129
+ Convert InputLike x, y to tuple of numpy arrays.
130
+ """
131
+ if isinstance(x, np.ndarray) and (y is None or isinstance(y, np.ndarray)):
132
+ return x, y
133
+
134
+ result = self._convert_to_tensors(x, y)
135
+ if isinstance(result, tuple):
136
+ x_tensor, y_tensor = result
137
+ x_np, y_np = x_tensor.cpu().numpy(), y_tensor.cpu().numpy()
138
+ if (y_np.ndim == 2 and y_np.shape[1] == 1) or y_np.ndim == 1:
139
+ y_np = y_np.ravel() # Ensure y is 1-dimensional
140
+ return check_X_y(x_np, y_np, multi_output=False, y_numeric=True)
141
+ return check_X_y(x_np, y_np, multi_output=True, y_numeric=True)
142
+
143
+ x_tensor = result
144
+ return x_tensor.cpu().numpy(), None
145
+
146
+ def _random_split(
147
+ self,
148
+ dataset: Dataset,
149
+ batch_size: int | None = None,
150
+ train_size: float = 0.8,
151
+ test_size: float = 0.2,
152
+ ) -> tuple[DataLoader, DataLoader]:
153
+ """
154
+ Split Dataset into train/test DataLoaders.
155
+
156
+ Parameters
157
+ ----------
158
+ dataset: Dataset
159
+ The data to split.
160
+ batch_size: int | None
161
+ The DataLoader batch_size. If None, sets batch_size to lenth of training
162
+ data. Defaults to None.
163
+ """
164
+ if train_size < 0.0 or train_size > 1.0 or test_size < 0.0 or test_size > 1.0:
165
+ raise ValueError(
166
+ f"Train size ({train_size}) and test size ({test_size}) must be "
167
+ "specified as a proportion between 0 and 1"
168
+ )
169
+ if test_size + train_size != 1.0:
170
+ raise ValueError(
171
+ f"Train size ({train_size}) and test size ({test_size}) must sum to 1"
172
+ )
173
+ train, test = tuple(random_split(dataset, [train_size, test_size]))
174
+ if batch_size is None:
175
+ batch_size = len(train)
176
+ train_loader = self._convert_to_dataloader(train, batch_size=batch_size)
177
+ test_loader = self._convert_to_dataloader(test, batch_size=batch_size)
178
+ return train_loader, test_loader
179
+
180
+ @staticmethod
181
+ def _normalize(x: TensorLike) -> tuple[TensorLike, TensorLike, TensorLike]:
182
+ x_mean = x.mean(0, keepdim=True)
183
+ x_std = x.std(0, keepdim=True)
184
+ return (x - x_mean) / x_std, x_mean, x_std
185
+
186
+ @staticmethod
187
+ def _denormalize(
188
+ x: TensorLike, x_mean: TensorLike, x_std: TensorLike
189
+ ) -> TensorLike:
190
+ return (x * x_std) + x_mean
191
+
192
+
193
+ class ValidationMixin:
194
+ """
195
+ Mixin class for validation methods.
196
+ This class provides static methods for checking the types and shapes of
197
+ input and output data, as well as validating specific tensor shapes.
198
+ """
199
+
200
+ @staticmethod
201
+ def _check(x: TensorLike, y: TensorLike | None):
202
+ """
203
+ Check the types and shape are correct for the input data.
204
+ Checks are equivalent to sklearn's check_array.
205
+ """
206
+
207
+ if not isinstance(x, TensorLike):
208
+ raise ValueError(f"Expected x to be TensorLike, got {type(x)}")
209
+
210
+ if y is not None and not isinstance(y, TensorLike):
211
+ raise ValueError(f"Expected y to be TensorLike, got {type(y)}")
212
+
213
+ # Check x
214
+ if not torch.isfinite(x).all():
215
+ msg = "Input tensor x contains non-finite values"
216
+ raise ValueError(msg)
217
+ if x.dtype not in TorchScalarDType:
218
+ msg = (
219
+ f"Input tensor x has unsupported dtype {x.dtype}. "
220
+ "Expected float32, float64, int32, or int64."
221
+ )
222
+ raise ValueError(msg)
223
+
224
+ # Check y if not None
225
+ if y is not None:
226
+ if not torch.isfinite(y).all():
227
+ msg = "Input tensor y contains non-finite values"
228
+ raise ValueError(msg)
229
+ if y.dtype not in TorchScalarDType:
230
+ msg = (
231
+ f"Input tensor y has unsupported dtype {y.dtype}. "
232
+ "Expected float32, float64, int32, or int64."
233
+ )
234
+ raise ValueError(msg)
235
+
236
+ return x, y
237
+
238
+ @staticmethod
239
+ def _check_output(output: OutputLike):
240
+ """
241
+ Check the types and shape are correct
242
+ for the output data.
243
+ """
244
+ if not isinstance(output, OutputLike):
245
+ raise ValueError(f"Expected OutputLike, got {type(output)}")
246
+
247
+ if isinstance(output, TensorLike) and output.ndim != 2:
248
+ raise ValueError(f"Expected output to be 2D tensor, got {output.ndim}D")
249
+
250
+ @staticmethod
251
+ def check_vector(X: TensorLike) -> TensorLike:
252
+ """
253
+ Validate that the input is a 1D TensorLike.
254
+
255
+ Parameters
256
+ ----------
257
+ X : TensorLike
258
+ Input tensor to validate.
259
+
260
+ Returns
261
+ -------
262
+ TensorLike
263
+ Validated 1D tensor.
264
+
265
+ Raises
266
+ ------
267
+ ValueError
268
+ If X is not a TensorLike or is not 1-dimensional.
269
+ """
270
+ if not isinstance(X, TensorLike):
271
+ raise ValueError(f"Expected TensorLike, got {type(X)}")
272
+ if X.ndim != 1:
273
+ raise ValueError(f"Expected 1D tensor, got {X.ndim}D")
274
+ return X
275
+
276
+ @staticmethod
277
+ def check_matrix(X: TensorLike) -> TensorLike:
278
+ """
279
+ Validate that the input is a 2D TensorLike.
280
+
281
+ Parameters
282
+ ----------
283
+ X : TensorLike
284
+ Input tensor to validate.
285
+
286
+ Returns
287
+ -------
288
+ TensorLike
289
+ Validated 2D tensor.
290
+
291
+ Raises
292
+ ------
293
+ ValueError
294
+ If X is not a TensorLike or is not 2-dimensional.
295
+ """
296
+ if not isinstance(X, TensorLike):
297
+ raise ValueError(f"Expected TensorLike, got {type(X)}")
298
+ if X.ndim != 2:
299
+ raise ValueError(f"Expected 2D tensor, got {X.ndim}D")
300
+ return X
301
+
302
+ @staticmethod
303
+ def check_pair(X: TensorLike, Y: TensorLike) -> tuple[TensorLike, TensorLike]:
304
+ """
305
+ Validate that two tensors have the same number of rows.
306
+
307
+ Parameters
308
+ ----------
309
+ X : TensorLike
310
+ First tensor.
311
+ Y : TensorLike
312
+ Second tensor.
313
+
314
+ Returns
315
+ -------
316
+ tuple[TensorLike, TensorLike]
317
+ The validated pair of tensors.
318
+
319
+ Raises
320
+ ------
321
+ ValueError
322
+ If X and Y do not have the same number of rows.
323
+ """
324
+ if X.shape[0] != Y.shape[0]:
325
+ msg = "X and Y must have the same number of rows"
326
+ raise ValueError(msg)
327
+ return X, Y
328
+
329
+ @staticmethod
330
+ def check_covariance(Y: TensorLike, Sigma: TensorLike) -> TensorLike:
331
+ """
332
+ Validate and return the covariance matrix.
333
+
334
+ Parameters
335
+ ----------
336
+ Y : TensorLike
337
+ Output tensor.
338
+ Sigma : TensorLike
339
+ Covariance matrix, which may be full, diagonal, or a scalar per sample.
340
+
341
+ Returns
342
+ -------
343
+ TensorLike
344
+ Validated covariance matrix.
345
+
346
+ Raises
347
+ ------
348
+ ValueError
349
+ If Sigma does not have a valid shape relative to Y.
350
+ """
351
+ if (
352
+ Sigma.shape == (Y.shape[0], Y.shape[1], Y.shape[1])
353
+ or Sigma.shape == (Y.shape[0], Y.shape[1])
354
+ or Sigma.shape == (Y.shape[0],)
355
+ ):
356
+ return Sigma
357
+ msg = "Invalid covariance matrix shape"
358
+ raise ValueError(msg)
359
+
360
+ @staticmethod
361
+ def trace(Sigma: TensorLike, d: int) -> TensorLike:
362
+ """
363
+ Compute the trace of the covariance matrix (A-optimal design criterion).
364
+
365
+ Parameters
366
+ ----------
367
+ Sigma : TensorLike
368
+ Covariance matrix (full, diagonal, or scalar).
369
+ d : int
370
+ Dimension of the output.
371
+
372
+ Returns
373
+ -------
374
+ TensorLike
375
+ The computed trace value.
376
+
377
+ Raises
378
+ ------
379
+ ValueError
380
+ If Sigma does not have a valid shape.
381
+ """
382
+ if Sigma.dim() == 3 and Sigma.shape[1:] == (d, d):
383
+ return torch.diagonal(Sigma, dim1=1, dim2=2).sum(dim=1).mean()
384
+ if Sigma.dim() == 2 and Sigma.shape[1] == d:
385
+ return Sigma.sum(dim=1).mean()
386
+ if Sigma.dim() == 1:
387
+ return d * Sigma.mean()
388
+ raise ValueError(f"Invalid covariance matrix shape: {Sigma.shape}")
389
+
390
+ @staticmethod
391
+ def logdet(Sigma: TensorLike, dim: int) -> TensorLike:
392
+ """
393
+ Compute the log-determinant of the covariance matrix (D-optimal design
394
+ criterion).
395
+
396
+ Parameters
397
+ ----------
398
+ Sigma : TensorLike
399
+ Covariance matrix (full, diagonal, or scalar).
400
+ dim : int
401
+ Dimension of the output.
402
+
403
+ Returns
404
+ -------
405
+ TensorLike
406
+ The computed log-determinant value.
407
+
408
+ Raises
409
+ ------
410
+ ValueError
411
+ If Sigma does not have a valid shape.
412
+ """
413
+ if len(Sigma.shape) == 3 and Sigma.shape[1:] == (dim, dim):
414
+ return torch.logdet(Sigma).mean()
415
+ if len(Sigma.shape) == 2 and Sigma.shape[1] == dim:
416
+ return torch.sum(torch.log(Sigma), dim=1).mean()
417
+ if len(Sigma.shape) == 1:
418
+ return dim * torch.log(Sigma).mean()
419
+ raise ValueError(f"Invalid covariance matrix shape: {Sigma.shape}")
420
+
421
+ @staticmethod
422
+ def max_eigval(Sigma: TensorLike) -> TensorLike:
423
+ """
424
+ Compute the maximum eigenvalue of the covariance matrix (E-optimal design
425
+ criterion).
426
+
427
+ Parameters
428
+ ----------
429
+ Sigma : TensorLike
430
+ Covariance matrix (full, diagonal, or scalar).
431
+
432
+ Returns
433
+ -------
434
+ TensorLike
435
+ The average maximum eigenvalue.
436
+
437
+ Raises
438
+ ------
439
+ ValueError
440
+ If Sigma does not have a valid shape.
441
+ """
442
+ if Sigma.dim() == 3 and Sigma.shape[1:] == (Sigma.shape[1], Sigma.shape[1]):
443
+ eigvals = torch.linalg.eigvalsh(Sigma)
444
+ return eigvals[:, -1].mean() # Eigenvalues are sorted in ascending order
445
+ if Sigma.dim() == 2:
446
+ return Sigma.max(dim=1).values.mean()
447
+ if Sigma.dim() == 1:
448
+ return Sigma.mean()
449
+ raise ValueError(f"Invalid covariance matrix shape: {Sigma.shape}")
450
+
451
+ ### Validation methods from old utils.py ###
452
+ ### Leave here in case want to restore/refactor later ###
453
+
454
+ # def _ensure_2d(arr):
455
+ # """Ensure that arr is a 2D."""
456
+ # if arr.ndim == 1:
457
+ # arr = arr.reshape(-1, 1)
458
+ # return arr
459
+
460
+ # def _ensure_1d_if_column_vec(arr):
461
+ # """Ensure that arr is 1D if shape is (n, 1)."""
462
+ # if arr.ndim == 2 and arr.shape[1] == 1:
463
+ # arr = arr.ravel()
464
+ # if arr.ndim > 2 or arr.ndim < 1:
465
+ # raise ValueError(
466
+ # f"arr should be 1D or 2D. Found {arr.ndim}D array with shape {arr.shape}" # noqa: E501
467
+ # )
468
+ # return arr
469
+
470
+ # def _check_cv(cv):
471
+ # """Ensure that cross-validation method is valid"""
472
+ # if cv is None:
473
+ # msg = "cross_validator cannot be None"
474
+ # raise ValueError(msg)
475
+ # if not isinstance(cv, KFold):
476
+ # msg = (
477
+ # "cross_validator should be an instance of KFold cross-validation. We do not " # noqa: E501
478
+ # "currently support other cross-validation methods."
479
+ # )
480
+ # raise ValueError(
481
+ # msg
482
+ # )
483
+ # return cv