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.
- {autoemulate-0.3.0 → autoemulate-0.3.2}/PKG-INFO +1 -1
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/compare.py +41 -14
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/compare.py +8 -4
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/data/preprocessors.py +10 -12
- autoemulate-0.3.2/autoemulate/experimental/data/utils.py +483 -0
- autoemulate-0.3.2/autoemulate/experimental/device.py +180 -0
- autoemulate-0.3.2/autoemulate/experimental/emulators/__init__.py +13 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/base.py +95 -47
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/gaussian_process/exact.py +30 -37
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/lightgbm.py +32 -48
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/neural_processes/conditional_neural_process.py +49 -31
- autoemulate-0.3.2/autoemulate/experimental/emulators/random_forest.py +69 -0
- autoemulate-0.3.2/autoemulate/experimental/emulators/svm.py +85 -0
- autoemulate-0.3.2/autoemulate/experimental/exploratory/active_matter/README.md +26 -0
- autoemulate-0.3.2/autoemulate/experimental/exploratory/active_matter/active_matter.ipynb +231318 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/base.py +3 -3
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/stream.py +12 -2
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/model_selection.py +20 -10
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/tuner.py +34 -14
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/types.py +4 -0
- autoemulate-0.3.2/autoemulate/history_matching.py +450 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/history_matching_dashboard.py +16 -41
- autoemulate-0.3.2/autoemulate/sensitivity_analysis.py +623 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/base.py +35 -23
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/naghavi_cardiac_ModularCirc.py +0 -24
- {autoemulate-0.3.0 → autoemulate-0.3.2}/pyproject.toml +1 -1
- autoemulate-0.3.0/autoemulate/experimental/data/utils.py +0 -176
- autoemulate-0.3.0/autoemulate/experimental/data/validation.py +0 -221
- autoemulate-0.3.0/autoemulate/experimental/emulators/__init__.py +0 -5
- autoemulate-0.3.0/autoemulate/history_matching.py +0 -419
- autoemulate-0.3.0/autoemulate/sensitivity_analysis.py +0 -310
- {autoemulate-0.3.0 → autoemulate-0.3.2}/LICENSE +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/README.md +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/cross_validate.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/data_splitting.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/datasets.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/conditional_neural_process.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/conditional_neural_process_attn.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_mogp.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_mt.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_sklearn.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/early_stopping_criterion.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/poly_mean.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gaussian_process_utils/polynomial_features.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/gradient_boosting.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/light_gbm.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_net_sk.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/cnp_module.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/cnp_module_attn.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/datasets.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/gp_module.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/neural_networks/losses.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/polynomials.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/radial_basis_functions.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/random_forest.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/emulators/support_vector_machines.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/emulators/gaussian_process/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/membership.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/learners/pool.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental/simulations/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/experimental_design.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/hyperparam_searching.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/logging_config.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/metrics.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/model_processing.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/model_registry.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/plotting.py +1 -1
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/preprocess_target.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/printing.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/save.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/__init__.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/circ_utils.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/epidemic.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/flow_functions.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/projectile.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/simulations/reaction_diffusion.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/autoemulate/utils.py +0 -0
- {autoemulate-0.3.0 → autoemulate-0.3.2}/misc/AE_logo_final.png +0 -0
|
@@ -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
|
|
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
|
-
#
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
12
|
+
def preprocess(self, x: TensorLike) -> TensorLike: ...
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Standardizer(Preprocessor):
|
|
16
|
-
mean:
|
|
17
|
-
std:
|
|
16
|
+
mean: TensorLike
|
|
17
|
+
std: TensorLike
|
|
18
18
|
|
|
19
|
-
def __init__(self, mean:
|
|
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 :
|
|
36
|
+
x : TensorLike
|
|
37
37
|
The input tensor to be standardized.
|
|
38
38
|
|
|
39
39
|
"""
|
|
40
|
-
if not isinstance(x,
|
|
41
|
-
raise ValueError(f"Expected 2D
|
|
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
|