spotoptim 2.1.2__tar.gz → 3.1.0__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 (89) hide show
  1. {spotoptim-2.1.2 → spotoptim-3.1.0}/PKG-INFO +1 -2
  2. {spotoptim-2.1.2 → spotoptim-3.1.0}/pyproject.toml +1 -3
  3. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/SpotOptim.py +61 -8
  4. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/data/__init__.py +14 -1
  5. spotoptim-3.1.0/src/spotoptim/data/manydataset.py +236 -0
  6. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/__init__.py +5 -0
  7. spotoptim-3.1.0/src/spotoptim/function/sequence_cv_objective.py +278 -0
  8. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/nn/__init__.py +15 -1
  9. spotoptim-3.1.0/src/spotoptim/nn/many_to_many_rnn.py +234 -0
  10. spotoptim-3.1.0/src/spotoptim/nn/optimizer.py +202 -0
  11. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/optimizer/acquisition.py +33 -5
  12. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/kernels.py +3 -18
  13. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/kriging.py +0 -7
  14. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/transform.py +2 -1
  15. {spotoptim-2.1.2 → spotoptim-3.1.0}/README.md +0 -0
  16. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/__init__.py +0 -0
  17. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/core/__init__.py +0 -0
  18. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/core/data.py +0 -0
  19. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/core/experiment.py +0 -0
  20. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/core/protocol.py +0 -0
  21. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/core/storage.py +0 -0
  22. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/data/base.py +0 -0
  23. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/data/diabetes.py +0 -0
  24. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/datasets/__init__.py +0 -0
  25. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/datasets/py.typed +0 -0
  26. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/datasets/test01.csv +0 -0
  27. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/datasets/test02.csv +0 -0
  28. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/datasets/test11.csv +0 -0
  29. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/eda/__init__.py +0 -0
  30. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/eda/plots.py +0 -0
  31. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/__init__.py +0 -0
  32. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py +0 -0
  33. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer.py +0 -0
  34. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py +0 -0
  35. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer_utils.py +0 -0
  36. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/cd_data.csv +0 -0
  37. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/forr08a.py +0 -0
  38. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/mo.py +0 -0
  39. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/remote.py +0 -0
  40. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/so.py +0 -0
  41. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/function/torch_objective.py +0 -0
  42. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/__init__.py +0 -0
  43. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/parameters.py +0 -0
  44. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/repr_helpers.py +0 -0
  45. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/inspection/__init__.py +0 -0
  46. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/inspection/importance.py +0 -0
  47. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/inspection/predictions.py +0 -0
  48. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/mo/__init__.py +0 -0
  49. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/mo/mo_mm.py +0 -0
  50. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/mo/pareto.py +0 -0
  51. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/nn/linear_regressor.py +0 -0
  52. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/nn/mlp.py +0 -0
  53. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/optimizer/__init__.py +0 -0
  54. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/optimizer/schedule_free.py +0 -0
  55. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/optimizer/wrapper.py +0 -0
  56. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/plot/__init__.py +0 -0
  57. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/plot/contour.py +0 -0
  58. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/plot/mo.py +0 -0
  59. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/plot/visualization.py +0 -0
  60. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/py.typed +0 -0
  61. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/reporting/__init__.py +0 -0
  62. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/reporting/analysis.py +0 -0
  63. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/reporting/results.py +0 -0
  64. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/sampling/__init__.py +0 -0
  65. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/sampling/design.py +0 -0
  66. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/sampling/effects.py +0 -0
  67. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/sampling/lhs.py +0 -0
  68. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/sampling/mm.py +0 -0
  69. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/__init__.py +0 -0
  70. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/mlp_surrogate.py +0 -0
  71. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/nystroem.py +0 -0
  72. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/pipeline.py +0 -0
  73. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/surrogate/simple_kriging.py +0 -0
  74. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/tricands/__init__.py +0 -0
  75. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/tricands/tricands.py +0 -0
  76. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/__init__.py +0 -0
  77. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/boundaries.py +0 -0
  78. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/convert.py +0 -0
  79. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/dimreduction.py +0 -0
  80. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/eval.py +0 -0
  81. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/file.py +0 -0
  82. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/mapping.py +0 -0
  83. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/ocba.py +0 -0
  84. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/pca.py +0 -0
  85. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/scaler.py +0 -0
  86. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/serialization.py +0 -0
  87. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/stats.py +0 -0
  88. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/tensorboard.py +0 -0
  89. {spotoptim-2.1.2 → spotoptim-3.1.0}/src/spotoptim/utils/variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spotoptim
3
- Version: 2.1.2
3
+ Version: 3.1.0
4
4
  Summary: Sequential Parameter Optimization Toolbox
5
5
  Author: bartzbeielstein
6
6
  Author-email: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com>
@@ -22,7 +22,6 @@ Requires-Dist: pytest-timeout>=2.3.0 ; extra == 'dev'
22
22
  Requires-Dist: black>=24.1.0 ; extra == 'dev'
23
23
  Requires-Dist: isort>=5.13.0 ; extra == 'dev'
24
24
  Requires-Dist: ruff>=0.3.0 ; extra == 'dev'
25
- Requires-Dist: safety>=3.0.0 ; extra == 'dev'
26
25
  Requires-Dist: bandit>=1.8.0 ; extra == 'dev'
27
26
  Requires-Dist: pre-commit>=3.7.0 ; extra == 'dev'
28
27
  Requires-Dist: ty>=0.0.29 ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spotoptim"
3
- version = "2.1.2"
3
+ version = "3.1.0"
4
4
  description = "Sequential Parameter Optimization Toolbox"
5
5
  readme = "README.md"
6
6
  license = { text = "AGPL-3.0-or-later" }
@@ -35,7 +35,6 @@ dev = [
35
35
  "black>=24.1.0",
36
36
  "isort>=5.13.0",
37
37
  "ruff>=0.3.0",
38
- "safety>=3.0.0",
39
38
  "bandit>=1.8.0",
40
39
  "pre-commit>=3.7.0",
41
40
  "ty>=0.0.29",
@@ -67,7 +66,6 @@ dev = [
67
66
  "black>=24.1.0",
68
67
  "isort>=5.13.0",
69
68
  "ruff>=0.3.0",
70
- "safety>=3.0.0",
71
69
  "bandit>=1.8.0",
72
70
  "pre-commit>=3.7.0",
73
71
  "ty>=0.0.29",
@@ -278,7 +278,13 @@ class SpotOptim(BaseEstimator):
278
278
  For example, max_iter=30 with n_initial=10 will perform 10 initial evaluations plus
279
279
  20 sequential optimization iterations. Defaults to 20.
280
280
  n_initial (int, optional):
281
- Number of initial design points. Defaults to 10.
281
+ Number of initial design points. Defaults to 10. A ``UserWarning`` is
282
+ raised when ``n_initial < 2 * n_dim``: a cold-start design that small
283
+ tends to under-sample the search space and gives the surrogate too
284
+ little signal to model dimension interactions. The warning is
285
+ guidance only (existing scripts with a deliberately small
286
+ ``n_initial`` keep working unchanged); consider
287
+ ``n_initial=max(10, 2 * n_dim)`` for cold starts.
282
288
  surrogate (object, optional):
283
289
  Surrogate model with scikit-learn interface (fit/predict methods).
284
290
  If None, uses a Gaussian Process Regressor with Matern kernel. Default configuration::
@@ -874,6 +880,26 @@ class SpotOptim(BaseEstimator):
874
880
  # Derived attribute dimension n_dim
875
881
  self.n_dim = len(self.bounds)
876
882
 
883
+ # Guidance for cold-start designs (ADR 2026-07-05, decision 4): a
884
+ # design with n_initial < 2*n_dim tends to under-sample the search
885
+ # space, giving the surrogate too little signal to model dimension
886
+ # interactions. Warning-only — existing small-budget scripts keep
887
+ # working unchanged. Nested catch_warnings + "always" mirrors
888
+ # _validate_factor_surrogate_compat: makes the warning visible even
889
+ # under the default warnings_filter="ignore".
890
+ if self.n_initial < 2 * self.n_dim:
891
+ with warnings.catch_warnings():
892
+ warnings.simplefilter("always")
893
+ warnings.warn(
894
+ f"n_initial ({self.n_initial}) is below 2 * n_dim "
895
+ f"({2 * self.n_dim}) for a {self.n_dim}-dimensional problem. "
896
+ f"Cold-start designs this small may under-sample the search "
897
+ f"space; consider n_initial=max(10, 2 * n_dim) = "
898
+ f"{max(10, 2 * self.n_dim)}.",
899
+ UserWarning,
900
+ stacklevel=2,
901
+ )
902
+
877
903
  # Default variable types
878
904
  if self.var_type is None:
879
905
  self.var_type = self.detect_var_type()
@@ -2503,6 +2529,8 @@ class SpotOptim(BaseEstimator):
2503
2529
  * message: termination message indicating reason for stopping, including statistics (function value, iterations, evaluations)
2504
2530
  * X: all evaluated points in Natural Space
2505
2531
  * y: all function values
2532
+ * x_encoded: best point in full-dimension natural-NUMERIC encoding (factor dimensions as integer level indices) — the representation restart injection and ``validate_x0`` consume
2533
+ * X_encoded: all evaluated points in the same numeric encoding (numeric dtype even when factor dimensions map to string labels in ``X``)
2506
2534
 
2507
2535
  Examples:
2508
2536
  ```{python}
@@ -2615,9 +2643,16 @@ class SpotOptim(BaseEstimator):
2615
2643
 
2616
2644
  if self.restart_inject_best:
2617
2645
  # Inject the current global best into the next run's initial design.
2618
- # best_res.x is in natural scale; validate_x0 converts to internal scale
2619
- # so the injected point can be mixed with LHS samples.
2620
- self.x0 = self.validate_x0(best_res.x)
2646
+ # Use the pre-string-map numeric encoding (x_encoded) when available:
2647
+ # best_res.x has factor dims mapped to string labels, which
2648
+ # validate_x0's `low <= val <= high` bounds check cannot compare
2649
+ # (UFuncTypeError). x_encoded carries the same full-dim natural-scale
2650
+ # point with factor dims still as integer codes.
2651
+ # validate_x0 converts to internal scale so the injected point can be
2652
+ # mixed with LHS samples.
2653
+ self.x0 = self.validate_x0(
2654
+ getattr(best_res, "x_encoded", best_res.x)
2655
+ )
2621
2656
  # Keep current_X0 unset so the initial design is rebuilt around the injected x0.
2622
2657
  current_X0 = None
2623
2658
 
@@ -2641,8 +2676,13 @@ class SpotOptim(BaseEstimator):
2641
2676
  # Find best result based on 'fun'
2642
2677
  best_result = min(self.restarts_results_, key=lambda r: r.fun)
2643
2678
 
2644
- # Merge results from all restart runs
2645
- X_all_list = [res.X for res in self.restarts_results_]
2679
+ # Merge results from all restart runs. Use the numeric encoding
2680
+ # (X_encoded) rather than the string-mapped X: on a factor problem the
2681
+ # latter has object dtype, which would leave self.X_ non-numeric after
2682
+ # a restart and break the post-restart surrogate fit.
2683
+ X_all_list = [
2684
+ getattr(res, "X_encoded", res.X) for res in self.restarts_results_
2685
+ ]
2646
2686
  y_all_list = [res.y for res in self.restarts_results_]
2647
2687
 
2648
2688
  # Concatenate all evaluations
@@ -2653,8 +2693,9 @@ class SpotOptim(BaseEstimator):
2653
2693
  # Aggregated iterations (sum of all runs)
2654
2694
  self.n_iter_ = sum(getattr(res, "nit", 0) for res in self.restarts_results_)
2655
2695
 
2656
- # Update best solution found
2657
- self.best_x_ = best_result.x
2696
+ # Update best solution found. Same numeric-encoding rationale as X_
2697
+ # above, for cross-path (x0 injection vs. best_x_) consistency.
2698
+ self.best_x_ = getattr(best_result, "x_encoded", best_result.x)
2658
2699
  self.best_y_ = best_result.fun
2659
2700
 
2660
2701
  # If the patience-based early-stopping rule fired, rewrite the result
@@ -3563,6 +3604,14 @@ class SpotOptim(BaseEstimator):
3563
3604
  message=status_message,
3564
3605
  X=X_result,
3565
3606
  y=self.y_,
3607
+ # Pre-string-map numeric form of x/X (full-dim, natural
3608
+ # scale, with factor dims as integer codes rather than
3609
+ # string labels). optimize()'s restart-inject path and the
3610
+ # cross-restart X_ merge use these instead of x/X, so a
3611
+ # factor problem's restart never feeds a string into
3612
+ # validate_x0()/the surrogate (UFuncTypeError).
3613
+ x_encoded=best_x_full,
3614
+ X_encoded=X_full,
3566
3615
  )
3567
3616
  return "RESTART", res
3568
3617
 
@@ -3621,6 +3670,10 @@ class SpotOptim(BaseEstimator):
3621
3670
  message=message,
3622
3671
  X=X_result,
3623
3672
  y=self.y_,
3673
+ # See the RESTART branch above: pre-string-map numeric x/X, used
3674
+ # by optimize()'s restart-inject path and cross-restart X_ merge.
3675
+ x_encoded=best_x_full,
3676
+ X_encoded=X_full,
3624
3677
  )
3625
3678
 
3626
3679
  def determine_termination(self, timeout_start: float) -> str:
@@ -10,11 +10,24 @@ machine learning tasks.
10
10
 
11
11
  from .base import Config, FileConfig
12
12
 
13
- __all__ = ["DiabetesDataset", "get_diabetes_dataloaders", "Config", "FileConfig"]
13
+ __all__ = [
14
+ "DiabetesDataset",
15
+ "get_diabetes_dataloaders",
16
+ "Config",
17
+ "FileConfig",
18
+ "ManyToManyDataset",
19
+ "ManyToOneDataset",
20
+ "PadSequenceManyToMany",
21
+ "PadSequenceManyToOne",
22
+ ]
14
23
 
15
24
  _lazy_map = {
16
25
  "DiabetesDataset": ("spotoptim.data.diabetes", "DiabetesDataset"),
17
26
  "get_diabetes_dataloaders": ("spotoptim.data.diabetes", "get_diabetes_dataloaders"),
27
+ "ManyToManyDataset": ("spotoptim.data.manydataset", "ManyToManyDataset"),
28
+ "ManyToOneDataset": ("spotoptim.data.manydataset", "ManyToOneDataset"),
29
+ "PadSequenceManyToMany": ("spotoptim.data.manydataset", "PadSequenceManyToMany"),
30
+ "PadSequenceManyToOne": ("spotoptim.data.manydataset", "PadSequenceManyToOne"),
18
31
  }
19
32
 
20
33
 
@@ -0,0 +1,236 @@
1
+ # SPDX-FileCopyrightText: 2026 bartzbeielstein
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ """Variable-length sequence datasets and padding collates for recurrent models.
6
+
7
+ Ported from spotPython (``spotpython.data.manydataset`` and the padding
8
+ collates from ``spotpython.data.lightdatamodule``) without any Lightning
9
+ dependency. Each dataset item is one complete sequence (e.g. one operating
10
+ curve of a compressor map), so batches must be padded with the collate
11
+ classes provided here.
12
+
13
+ Requires the ``torch`` optional extra (``pip install 'spotoptim[torch]'``).
14
+ """
15
+
16
+ from typing import List, Optional, Union
17
+
18
+ import pandas as pd
19
+ import torch
20
+ from torch.nn.utils.rnn import pad_sequence
21
+ from torch.utils.data import Dataset
22
+
23
+
24
+ class ManyToManyDataset(Dataset):
25
+ """Sequence dataset with one target value per time step.
26
+
27
+ Each element of ``df_list`` is one variable-length sequence; item ``i``
28
+ is the pair ``(features_i, targets_i)`` with shapes ``(T_i, n_features)``
29
+ and ``(T_i,)``.
30
+
31
+ Args:
32
+ df_list (List[pd.DataFrame]): List of pandas DataFrames, one per sequence.
33
+ target (str): The target column name.
34
+ drop (Optional[Union[str, List[str]]]): Column(s) to drop from the
35
+ DataFrames before extracting features. If a listed column is
36
+ missing, no column is dropped. Defaults to None.
37
+ dtype (torch.dtype): Data type for the tensors. Defaults to ``torch.float32``.
38
+
39
+ Attributes:
40
+ data (List[pd.DataFrame]): DataFrames with the ``drop`` columns removed.
41
+ target (List[torch.Tensor]): Per-sequence target tensors, shape ``(T_i,)``.
42
+ features (List[torch.Tensor]): Per-sequence feature tensors, shape
43
+ ``(T_i, n_features)``.
44
+
45
+ Examples:
46
+ ```{python}
47
+ import pandas as pd
48
+ from spotoptim.data.manydataset import ManyToManyDataset
49
+
50
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [2.0, 4.0, 6.0]})
51
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [8.0, 10.0]})
52
+ ds = ManyToManyDataset([df1, df2], target="y")
53
+ print(len(ds))
54
+ features, targets = ds[0]
55
+ print(features.shape, targets.shape)
56
+ ```
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ df_list: List[pd.DataFrame],
62
+ target: str,
63
+ drop: Optional[Union[str, List[str]]] = None,
64
+ dtype: torch.dtype = torch.float32,
65
+ ):
66
+ if drop is None:
67
+ self.data = list(df_list)
68
+ else:
69
+ try:
70
+ self.data = [df.drop(drop, axis=1) for df in df_list]
71
+ except KeyError:
72
+ self.data = df_list
73
+ self.target = [
74
+ torch.tensor(df[target].to_numpy(), dtype=dtype) for df in self.data
75
+ ]
76
+ self.features = [
77
+ torch.tensor(df.drop([target], axis=1).to_numpy(), dtype=dtype)
78
+ for df in self.data
79
+ ]
80
+
81
+ def __getitem__(self, index: int):
82
+ """Return the ``(features, targets)`` pair of sequence ``index``."""
83
+ x = self.features[index]
84
+ y = self.target[index]
85
+ return x, y
86
+
87
+ def __len__(self) -> int:
88
+ """Return the number of sequences."""
89
+ return len(self.data)
90
+
91
+
92
+ class ManyToOneDataset(Dataset):
93
+ """Sequence dataset with a single target value per sequence.
94
+
95
+ Like `ManyToManyDataset`, but item ``i`` pairs the full feature sequence
96
+ with the scalar target taken from the first row of sequence ``i``.
97
+
98
+ Args:
99
+ df_list (List[pd.DataFrame]): List of pandas DataFrames, one per sequence.
100
+ target (str): The target column name.
101
+ drop (Optional[Union[str, List[str]]]): Column(s) to drop from the
102
+ DataFrames before extracting features. If a listed column is
103
+ missing, no column is dropped. Defaults to None.
104
+ dtype (torch.dtype): Data type for the tensors. Defaults to ``torch.float32``.
105
+
106
+ Attributes:
107
+ data (List[pd.DataFrame]): DataFrames with the ``drop`` columns removed.
108
+ target (List[torch.Tensor]): Per-sequence scalar target tensors.
109
+ features (List[torch.Tensor]): Per-sequence feature tensors, shape
110
+ ``(T_i, n_features)``.
111
+
112
+ Examples:
113
+ ```{python}
114
+ import pandas as pd
115
+ from spotoptim.data.manydataset import ManyToOneDataset
116
+
117
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [5.0, 5.0, 5.0]})
118
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [7.0, 7.0]})
119
+ ds = ManyToOneDataset([df1, df2], target="y")
120
+ features, target = ds[1]
121
+ print(features.shape, target)
122
+ ```
123
+ """
124
+
125
+ def __init__(
126
+ self,
127
+ df_list: List[pd.DataFrame],
128
+ target: str,
129
+ drop: Optional[Union[str, List[str]]] = None,
130
+ dtype: torch.dtype = torch.float32,
131
+ ):
132
+ if drop is None:
133
+ self.data = list(df_list)
134
+ else:
135
+ try:
136
+ self.data = [df.drop(drop, axis=1) for df in df_list]
137
+ except KeyError:
138
+ self.data = df_list
139
+ self.target = [
140
+ torch.tensor(df[target].to_numpy()[0], dtype=dtype) for df in self.data
141
+ ]
142
+ self.features = [
143
+ torch.tensor(df.drop([target], axis=1).to_numpy(), dtype=dtype)
144
+ for df in self.data
145
+ ]
146
+
147
+ def __getitem__(self, index: int):
148
+ """Return the ``(features, target)`` pair of sequence ``index``."""
149
+ x = self.features[index]
150
+ y = self.target[index]
151
+ return x, y
152
+
153
+ def __len__(self) -> int:
154
+ """Return the number of sequences."""
155
+ return len(self.data)
156
+
157
+
158
+ class PadSequenceManyToMany:
159
+ """Padding collate for `ManyToManyDataset` batches.
160
+
161
+ Pads features and targets of a batch of variable-length sequences with
162
+ zeros to the longest sequence in the batch and records the true lengths,
163
+ as required by ``torch.nn.utils.rnn.pack_padded_sequence``.
164
+
165
+ Examples:
166
+ ```{python}
167
+ import pandas as pd
168
+ from torch.utils.data import DataLoader
169
+ from spotoptim.data.manydataset import ManyToManyDataset, PadSequenceManyToMany
170
+
171
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [2.0, 4.0, 6.0]})
172
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [8.0, 10.0]})
173
+ ds = ManyToManyDataset([df1, df2], target="y")
174
+ dl = DataLoader(ds, batch_size=2, shuffle=False, collate_fn=PadSequenceManyToMany())
175
+ x, lengths, y = next(iter(dl))
176
+ print(x.shape, lengths.tolist(), y.shape)
177
+ ```
178
+ """
179
+
180
+ def __call__(self, batch):
181
+ """Collate a batch into ``(padded_x, lengths, padded_y)``.
182
+
183
+ Args:
184
+ batch: Sequence of ``(features, targets)`` pairs as produced by
185
+ `ManyToManyDataset`.
186
+
187
+ Returns:
188
+ tuple: ``(padded_x, lengths, padded_y)`` where ``padded_x`` has
189
+ shape ``(B, T_max, n_features)``, ``lengths`` is an int tensor of
190
+ shape ``(B,)``, and ``padded_y`` has shape ``(B, T_max)``.
191
+ """
192
+ batch_x, batch_y = zip(*batch)
193
+ padded_batch_x = pad_sequence(list(batch_x), batch_first=True)
194
+ padded_batch_y = pad_sequence(list(batch_y), batch_first=True)
195
+ lengths = torch.tensor([len(x) for x in batch_x])
196
+
197
+ return padded_batch_x, lengths, padded_batch_y
198
+
199
+
200
+ class PadSequenceManyToOne:
201
+ """Padding collate for `ManyToOneDataset` batches.
202
+
203
+ Pads the feature sequences with zeros and stacks the scalar targets.
204
+
205
+ Examples:
206
+ ```{python}
207
+ import pandas as pd
208
+ from torch.utils.data import DataLoader
209
+ from spotoptim.data.manydataset import ManyToOneDataset, PadSequenceManyToOne
210
+
211
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [5.0, 5.0, 5.0]})
212
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [7.0, 7.0]})
213
+ ds = ManyToOneDataset([df1, df2], target="y")
214
+ dl = DataLoader(ds, batch_size=2, shuffle=False, collate_fn=PadSequenceManyToOne())
215
+ x, lengths, y = next(iter(dl))
216
+ print(x.shape, lengths.tolist(), y.shape)
217
+ ```
218
+ """
219
+
220
+ def __call__(self, batch):
221
+ """Collate a batch into ``(padded_x, lengths, y)``.
222
+
223
+ Args:
224
+ batch: Sequence of ``(features, target)`` pairs as produced by
225
+ `ManyToOneDataset`.
226
+
227
+ Returns:
228
+ tuple: ``(padded_x, lengths, y)`` where ``padded_x`` has shape
229
+ ``(B, T_max, n_features)``, ``lengths`` is an int tensor of shape
230
+ ``(B,)``, and ``y`` is a tensor of shape ``(B,)``.
231
+ """
232
+ batch_x, batch_y = zip(*batch)
233
+ padded_batch_x = pad_sequence(list(batch_x), batch_first=True)
234
+ lengths = torch.tensor([len(x) for x in batch_x])
235
+
236
+ return padded_batch_x, lengths, torch.tensor(batch_y)
@@ -69,11 +69,16 @@ __all__ = [
69
69
  "branin",
70
70
  "onevar",
71
71
  "TorchObjective",
72
+ "SequenceCVObjective",
72
73
  "objective_remote",
73
74
  ]
74
75
 
75
76
  _lazy_map = {
76
77
  "TorchObjective": ("spotoptim.function.torch_objective", "TorchObjective"),
78
+ "SequenceCVObjective": (
79
+ "spotoptim.function.sequence_cv_objective",
80
+ "SequenceCVObjective",
81
+ ),
77
82
  "objective_remote": ("spotoptim.function.remote", "objective_remote"),
78
83
  }
79
84