dragon-ml-toolbox 6.0.1__py3-none-any.whl → 6.1.1__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.

Potentially problematic release.


This version of dragon-ml-toolbox might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dragon-ml-toolbox
3
- Version: 6.0.1
3
+ Version: 6.1.1
4
4
  Summary: A collection of tools for data science and machine learning projects.
5
5
  Author-email: Karl Loza <luigiloza@gmail.com>
6
6
  License-Expression: MIT
@@ -1,14 +1,14 @@
1
- dragon_ml_toolbox-6.0.1.dist-info/licenses/LICENSE,sha256=2uUFNy7D0TLgHim1K5s3DIJ4q_KvxEXVilnU20cWliY,1066
2
- dragon_ml_toolbox-6.0.1.dist-info/licenses/LICENSE-THIRD-PARTY.md,sha256=lY4_rJPnLnMu7YBQaY-_iz1JRDcLdQzNCyeLAF1glJY,1837
1
+ dragon_ml_toolbox-6.1.1.dist-info/licenses/LICENSE,sha256=2uUFNy7D0TLgHim1K5s3DIJ4q_KvxEXVilnU20cWliY,1066
2
+ dragon_ml_toolbox-6.1.1.dist-info/licenses/LICENSE-THIRD-PARTY.md,sha256=lY4_rJPnLnMu7YBQaY-_iz1JRDcLdQzNCyeLAF1glJY,1837
3
3
  ml_tools/ETL_engineering.py,sha256=4wwZXi9_U7xfCY70jGBaKniOeZ0m75ppxWpQBd_DmLc,39369
4
4
  ml_tools/GUI_tools.py,sha256=n4ZZ5kEjwK5rkOCFJE41HeLFfjhpJVLUSzk9Kd9Kr_0,45410
5
5
  ml_tools/MICE_imputation.py,sha256=oFHg-OytOzPYTzBR_wIRHhP71cMn3aupDeT59ABsXlQ,11576
6
6
  ml_tools/ML_callbacks.py,sha256=FEJ80TSEtY0-hdnOsAWeVApQt1mdzTdOntqtoWmMAzE,13310
7
7
  ml_tools/ML_datasetmaster.py,sha256=bbKCNA_b_uDIfxP9YIYKZm-VSfUSD15LvegFxpE9DIQ,34315
8
8
  ml_tools/ML_evaluation.py,sha256=-Z5fXQi2ou6l5Oyir06bO90SZIZVrjQfgoVAqKgSjks,13800
9
- ml_tools/ML_inference.py,sha256=Fh-X2UQn3AznWBjf-7iPSxwE-EzkGQm1VEIRUAkURmE,5336
9
+ ml_tools/ML_inference.py,sha256=blEDgzvDqatxbfloBKsyNPacRwoq9g6WTpIKQ3zoTak,5758
10
10
  ml_tools/ML_models.py,sha256=SJhKHGAN2VTBqzcHUOpFWuVZ2Y7U1M4P_axG_LNYWcI,6460
11
- ml_tools/ML_optimization.py,sha256=zGKpWW4SL1-3iiHglDP-dkuADL73T0kxs3Dc-Lyishs,9671
11
+ ml_tools/ML_optimization.py,sha256=0kRkjcAHbbx6EINUjzKfibL5h0DV39wghjcjzN0syNI,13406
12
12
  ml_tools/ML_trainer.py,sha256=1q_CDXuMfndRsPuNofUn2mg2TlhG6MYuGqjWxTDgN9c,15112
13
13
  ml_tools/PSO_optimization.py,sha256=9Y074d-B5h4Wvp9YPiy6KAeXM-Yv6Il3gWalKvOLVgo,22705
14
14
  ml_tools/RNN_forecast.py,sha256=2CyjBLSYYc3xLHxwLXUmP5Qv8AmV1OB_EndETNX1IBk,1956
@@ -27,7 +27,7 @@ ml_tools/keys.py,sha256=HtPG8-MWh89C32A7eIlfuuA-DLwkxGkoDfwR2TGN9CQ,1074
27
27
  ml_tools/optimization_tools.py,sha256=MuT4OG7_r1QqLUti-yYix7QeCpglezD0oe9BDCq0QXk,5086
28
28
  ml_tools/path_manager.py,sha256=Z8e7w3MPqQaN8xmTnKuXZS6CIW59BFwwqGhGc00sdp4,13692
29
29
  ml_tools/utilities.py,sha256=LqXXTovaHbA5AOKRk6Ru6DgAPAM0wPfYU70kUjYBryo,19231
30
- dragon_ml_toolbox-6.0.1.dist-info/METADATA,sha256=SxZPqt9cAVNkerRZYCpZP_-v7feEx5MTK5lCXeA5dxc,6698
31
- dragon_ml_toolbox-6.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- dragon_ml_toolbox-6.0.1.dist-info/top_level.txt,sha256=wm-oxax3ciyez6VoO4zsFd-gSok2VipYXnbg3TH9PtU,9
33
- dragon_ml_toolbox-6.0.1.dist-info/RECORD,,
30
+ dragon_ml_toolbox-6.1.1.dist-info/METADATA,sha256=qrfNT_c9zH8iYfbe_QBoxpRJNLvrhm-ZKQyMeVwDu9w,6698
31
+ dragon_ml_toolbox-6.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ dragon_ml_toolbox-6.1.1.dist-info/top_level.txt,sha256=wm-oxax3ciyez6VoO4zsFd-gSok2VipYXnbg3TH9PtU,9
33
+ dragon_ml_toolbox-6.1.1.dist-info/RECORD,,
ml_tools/ML_inference.py CHANGED
@@ -66,47 +66,10 @@ class PyTorchInferenceHandler:
66
66
 
67
67
  # Ensure tensor is on the correct device
68
68
  return features.to(self.device)
69
-
70
- def predict(self, features: Union[np.ndarray, torch.Tensor]) -> Dict[str, Any]:
71
- """
72
- Predicts on a single feature vector.
73
-
74
- Args:
75
- features (np.ndarray | torch.Tensor): A 1D or 2D array/tensor for a single sample.
76
-
77
- Returns:
78
- Dict[str, Any]: A dictionary containing the prediction.
79
- - For regression: {'predictions': float}
80
- - For classification: {'labels': int, 'probabilities': np.ndarray}
69
+
70
+ def predict_batch(self, features: Union[np.ndarray, torch.Tensor]) -> Dict[str, torch.Tensor]:
81
71
  """
82
- if features.ndim == 1:
83
- features = features.reshape(1, -1)
84
-
85
- if features.shape[0] != 1:
86
- raise ValueError("The predict() method is for a single sample. Use predict_batch() for multiple samples.")
87
-
88
- results_batch = self.predict_batch(features)
89
-
90
- # Extract the single result from the batch
91
- if self.task == "regression":
92
- return {PyTorchInferenceKeys.PREDICTIONS: results_batch[PyTorchInferenceKeys.PREDICTIONS].item()}
93
- else: # classification
94
- return {
95
- PyTorchInferenceKeys.LABELS: results_batch[PyTorchInferenceKeys.LABELS].item(),
96
- PyTorchInferenceKeys.PROBABILITIES: results_batch[PyTorchInferenceKeys.PROBABILITIES][0]
97
- }
98
-
99
- def predict_batch(self, features: Union[np.ndarray, torch.Tensor]) -> Dict[str, Any]:
100
- """
101
- Predicts on a batch of feature vectors.
102
-
103
- Args:
104
- features (np.ndarray | torch.Tensor): A 2D array/tensor where each row is a sample.
105
-
106
- Returns:
107
- Dict[str, Any]: A dictionary containing the predictions.
108
- - For regression: {'predictions': np.ndarray}
109
- - For classification: {'labels': np.ndarray, 'probabilities': np.ndarray}
72
+ Core batch prediction method. Returns results as PyTorch tensors on the model's device.
110
73
  """
111
74
  if features.ndim != 2:
112
75
  raise ValueError("Input for batch prediction must be a 2D array or tensor.")
@@ -114,18 +77,61 @@ class PyTorchInferenceHandler:
114
77
  input_tensor = self._preprocess_input(features)
115
78
 
116
79
  with torch.no_grad():
117
- output = self.model(input_tensor).cpu()
80
+ # Output tensor remains on the model's device (e.g., 'mps' or 'cuda')
81
+ output = self.model(input_tensor)
118
82
 
119
83
  if self.task == "classification":
120
84
  probs = nn.functional.softmax(output, dim=1)
121
85
  labels = torch.argmax(probs, dim=1)
122
86
  return {
123
- PyTorchInferenceKeys.LABELS: labels.numpy(),
124
- PyTorchInferenceKeys.PROBABILITIES: probs.numpy()
87
+ PyTorchInferenceKeys.LABELS: labels,
88
+ PyTorchInferenceKeys.PROBABILITIES: probs
125
89
  }
126
90
  else: # regression
127
- return {PyTorchInferenceKeys.PREDICTIONS: output.numpy()}
91
+ return {PyTorchInferenceKeys.PREDICTIONS: output}
128
92
 
93
+ def predict(self, features: Union[np.ndarray, torch.Tensor]) -> Dict[str, torch.Tensor]:
94
+ """
95
+ Core single-sample prediction. Returns results as PyTorch tensors on the model's device.
96
+ """
97
+ if features.ndim == 1:
98
+ features = features.reshape(1, -1)
99
+
100
+ if features.shape[0] != 1:
101
+ raise ValueError("The predict() method is for a single sample. Use predict_batch() for multiple samples.")
102
+
103
+ batch_results = self.predict_batch(features)
104
+
105
+ single_results = {key: value[0] for key, value in batch_results.items()}
106
+ return single_results
107
+
108
+ # --- NumPy Convenience Wrappers (on CPU) ---
109
+
110
+ def predict_batch_numpy(self, features: Union[np.ndarray, torch.Tensor]) -> Dict[str, np.ndarray]:
111
+ """
112
+ Convenience wrapper for predict_batch that returns NumPy arrays.
113
+ """
114
+ tensor_results = self.predict_batch(features)
115
+ # Move tensor to CPU before converting to NumPy
116
+ numpy_results = {key: value.cpu().numpy() for key, value in tensor_results.items()}
117
+ return numpy_results
118
+
119
+ def predict_numpy(self, features: Union[np.ndarray, torch.Tensor]) -> Dict[str, Any]:
120
+ """
121
+ Convenience wrapper for predict that returns NumPy arrays or scalars.
122
+ """
123
+ tensor_results = self.predict(features)
124
+
125
+ if self.task == "regression":
126
+ # .item() implicitly moves to CPU
127
+ return {PyTorchInferenceKeys.PREDICTIONS: tensor_results[PyTorchInferenceKeys.PREDICTIONS].item()}
128
+ else: # classification
129
+ return {
130
+ PyTorchInferenceKeys.LABELS: tensor_results[PyTorchInferenceKeys.LABELS].item(),
131
+ # ✅ Move tensor to CPU before converting to NumPy
132
+ PyTorchInferenceKeys.PROBABILITIES: tensor_results[PyTorchInferenceKeys.PROBABILITIES].cpu().numpy()
133
+ }
134
+
129
135
 
130
136
  def info():
131
137
  _script_info(__all__)
@@ -1,12 +1,15 @@
1
+ import pandas # logger
1
2
  import torch
2
3
  import numpy #handling torch to numpy
3
4
  import evotorch
4
- from evotorch.algorithms import CMAES, SteadyStateGA
5
- from evotorch.logging import StdOutLogger
6
- from typing import Literal, Union, Tuple, List, Optional
5
+ from evotorch.algorithms import SNES, CEM, GeneticAlgorithm
6
+ from evotorch.logging import PandasLogger
7
+ from evotorch.operators import SimulatedBinaryCrossOver, GaussianMutation
8
+ from typing import Literal, Union, Tuple, List, Optional, Any, Callable
7
9
  from pathlib import Path
8
10
  from tqdm.auto import trange
9
11
  from contextlib import nullcontext
12
+ from functools import partial
10
13
 
11
14
  from .path_manager import make_fullpath, sanitize_filename
12
15
  from ._logger import _LOGGER
@@ -15,8 +18,7 @@ from .ML_inference import PyTorchInferenceHandler
15
18
  from .keys import PyTorchInferenceKeys
16
19
  from .SQL import DatabaseManager
17
20
  from .optimization_tools import _save_result
18
- from .utilities import threshold_binary_values
19
-
21
+ from .utilities import threshold_binary_values, save_dataframe
20
22
 
21
23
  __all__ = [
22
24
  "create_pytorch_problem",
@@ -25,34 +27,38 @@ __all__ = [
25
27
 
26
28
 
27
29
  def create_pytorch_problem(
28
- handler: PyTorchInferenceHandler,
30
+ inference_handler: PyTorchInferenceHandler,
29
31
  bounds: Tuple[List[float], List[float]],
30
32
  binary_features: int,
31
- task: Literal["minimize", "maximize"],
32
- algorithm: Literal["CMAES", "GA"] = "CMAES",
33
- verbose: bool = False,
33
+ task: Literal["min", "max"],
34
+ algorithm: Literal["SNES", "CEM", "Genetic"] = "Genetic",
35
+ population_size: int = 200,
34
36
  **searcher_kwargs
35
- ) -> Tuple[evotorch.Problem, evotorch.Searcher]: # type: ignore
37
+ ) -> Tuple[evotorch.Problem, Callable[[], Any]]:
36
38
  """
37
- Creates and configures an EvoTorch Problem and Searcher for a PyTorch model.
38
-
39
+ Creates and configures an EvoTorch Problem and a Searcher factory class for a PyTorch model.
40
+
41
+ SNES and CEM do not accept bounds, the given bounds will be used as initial bounds only.
42
+
43
+ The Genetic Algorithm works directly with the bounds, and operators such as SimulatedBinaryCrossOver and GaussianMutation.
44
+
39
45
  Args:
40
- handler (PyTorchInferenceHandler): An initialized inference handler
41
- containing the model and weights.
42
- bounds (tuple[list[float], list[float]]): A tuple containing the lower
43
- and upper bounds for the solution features.
46
+ inference_handler (PyTorchInferenceHandler): An initialized inference handler containing the model and weights.
47
+ bounds (tuple[list[float], list[float]]): A tuple containing the lower and upper bounds for the solution features.
44
48
  binary_features (int): Number of binary features located at the END of the feature vector. Will be automatically added to the bounds.
45
49
  task (str): The optimization goal, either "minimize" or "maximize".
46
- algorithm (str): The search algorithm to use, "CMAES" or "GA" (SteadyStateGA).
47
- verbose (bool): Add an Evotorch logger for real-time console updates.
50
+ algorithm (str): The search algorithm to use.
51
+ population_size (int): Used for CEM and GeneticAlgorithm.
48
52
  **searcher_kwargs: Additional keyword arguments to pass to the
49
53
  selected search algorithm's constructor (e.g., stdev_init=0.5 for CMAES).
50
54
 
51
55
  Returns:
52
56
  Tuple:
53
- A tuple containing the configured evotorch.Problem and evotorch.Searcher.
57
+ A tuple containing the configured Problem and Searcher.
54
58
  """
55
- lower_bounds, upper_bounds = bounds
59
+ # Create copies to avoid modifying the original lists passed in the `bounds` tuple
60
+ lower_bounds = list(bounds[0])
61
+ upper_bounds = list(bounds[1])
56
62
 
57
63
  # add binary bounds
58
64
  if binary_features > 0:
@@ -60,51 +66,86 @@ def create_pytorch_problem(
60
66
  upper_bounds.extend([0.55] * binary_features)
61
67
 
62
68
  solution_length = len(lower_bounds)
63
- device = handler.device
69
+ device = inference_handler.device
64
70
 
65
71
  # Define the fitness function that EvoTorch will call.
66
- @evotorch.decorators.to_tensor # type: ignore
67
- @evotorch.decorators.on_aux_device(device)
68
72
  def fitness_func(solution_tensor: torch.Tensor) -> torch.Tensor:
69
73
  # Directly use the continuous-valued tensor from the optimizer for prediction
70
- predictions = handler.predict_batch(solution_tensor)[PyTorchInferenceKeys.PREDICTIONS]
74
+ predictions = inference_handler.predict_batch(solution_tensor)[PyTorchInferenceKeys.PREDICTIONS]
71
75
  return predictions.flatten()
72
-
76
+
77
+
73
78
  # Create the Problem instance.
74
- problem = evotorch.Problem(
75
- objective_sense=task,
76
- objective_func=fitness_func,
77
- solution_length=solution_length,
78
- initial_bounds=(lower_bounds, upper_bounds),
79
- device=device,
80
- )
81
-
82
- # Create the selected searcher instance.
83
- if algorithm == "CMAES":
84
- searcher = CMAES(problem, **searcher_kwargs)
85
- elif algorithm == "GA":
86
- searcher = SteadyStateGA(problem, **searcher_kwargs)
87
- else:
88
- raise ValueError(f"Unknown algorithm '{algorithm}'. Choose 'CMAES' or 'GA'.")
79
+ if algorithm == "CEM" or algorithm == "SNES":
80
+ problem = evotorch.Problem(
81
+ objective_sense=task,
82
+ objective_func=fitness_func,
83
+ solution_length=solution_length,
84
+ initial_bounds=(lower_bounds, upper_bounds),
85
+ device=device,
86
+ vectorized=True #Use batches
87
+ )
88
+
89
+ # If stdev_init is not provided, calculate it based on the bounds (used for SNES and CEM)
90
+ if 'stdev_init' not in searcher_kwargs:
91
+ # Calculate stdev for each parameter as 25% of its search range
92
+ stdevs = [abs(up - low) * 0.25 for low, up in zip(lower_bounds, upper_bounds)]
93
+ searcher_kwargs['stdev_init'] = torch.tensor(stdevs, dtype=torch.float32, requires_grad=False)
94
+
95
+ if algorithm == "SNES":
96
+ SearcherClass = SNES
97
+ elif algorithm == "CEM":
98
+ SearcherClass = CEM
99
+ # Set a defaults for CEM if not provided
100
+ if 'popsize' not in searcher_kwargs:
101
+ searcher_kwargs['popsize'] = population_size
102
+ if 'parenthood_ratio' not in searcher_kwargs:
103
+ searcher_kwargs['parenthood_ratio'] = 0.2 #float 0.0 - 1.0
104
+
105
+ elif algorithm == "Genetic":
106
+ problem = evotorch.Problem(
107
+ objective_sense=task,
108
+ objective_func=fitness_func,
109
+ solution_length=solution_length,
110
+ bounds=(lower_bounds, upper_bounds),
111
+ device=device,
112
+ vectorized=True #Use batches
113
+ )
89
114
 
90
- # Add a logger for real-time console updates.
91
- # This gives the user immediate feedback on the optimization progress.
92
- if verbose:
93
- _ = StdOutLogger(searcher)
115
+ operators = [
116
+ SimulatedBinaryCrossOver(problem,
117
+ tournament_size=4,
118
+ eta=0.8),
119
+ GaussianMutation(problem,
120
+ stdev=0.1)
121
+ ]
122
+
123
+ searcher_kwargs["operators"] = operators
124
+ if 'popsize' not in searcher_kwargs:
125
+ searcher_kwargs['popsize'] = population_size
126
+
127
+ SearcherClass = GeneticAlgorithm
128
+
129
+ else:
130
+ raise ValueError(f"Unknown algorithm '{algorithm}'.")
131
+
132
+ # Create a factory function with all arguments pre-filled
133
+ searcher_factory = partial(SearcherClass, problem, **searcher_kwargs)
94
134
 
95
- return problem, searcher
135
+ return problem, searcher_factory
96
136
 
97
137
 
98
138
  def run_optimization(
99
139
  problem: evotorch.Problem,
100
- searcher: evotorch.Searcher, # type: ignore
140
+ searcher_factory: Callable[[],Any],
101
141
  num_generations: int,
102
142
  target_name: str,
103
143
  binary_features: int,
104
144
  save_dir: Union[str, Path],
105
145
  save_format: Literal['csv', 'sqlite', 'both'],
106
146
  feature_names: Optional[List[str]],
107
- repetitions: int = 1
147
+ repetitions: int = 1,
148
+ verbose: bool = True
108
149
  ) -> Optional[dict]:
109
150
  """
110
151
  Runs the evolutionary optimization process, with support for multiple repetitions.
@@ -124,20 +165,19 @@ def run_optimization(
124
165
  Args:
125
166
  problem (evotorch.Problem): The configured problem instance, which defines
126
167
  the objective function, solution space, and optimization sense.
127
- searcher (evotorch.Searcher): The configured searcher instance, which
128
- contains the evolutionary algorithm (e.g., CMAES, GA).
129
- num_generations (int): The total number of generations to run the
130
- search algorithm for in each repetition.
168
+ searcher_factory (Callable): The searcher factory to generate fresh evolutionary algorithms.
169
+ num_generations (int): The total number of generations to run the search algorithm for in each repetition.
131
170
  target_name (str): Target name that will also be used for the CSV filename and SQL table.
132
171
  binary_features (int): Number of binary features located at the END of the feature vector.
133
172
  save_dir (str | Path): The directory where the result file(s) will be saved.
134
173
  save_format (Literal['csv', 'sqlite', 'both'], optional): The format for
135
- saving results during iterative analysis. Defaults to 'both'.
174
+ saving results during iterative analysis.
136
175
  feature_names (List[str], optional): Names of the solution features for
137
176
  labeling the output files. If None, generic names like 'feature_0',
138
- 'feature_1', etc., will be created. Defaults to None.
177
+ 'feature_1', etc., will be created.
139
178
  repetitions (int, optional): The number of independent times to run the
140
- entire optimization process. Defaults to 1.
179
+ entire optimization process.
180
+ verbose (bool): Add an Evotorch Pandas logger saved as a csv. Only for the first repetition.
141
181
 
142
182
  Returns:
143
183
  Optional[dict]: A dictionary containing the best feature values and the
@@ -162,11 +202,29 @@ def run_optimization(
162
202
 
163
203
  # --- SINGLE RUN LOGIC ---
164
204
  if repetitions <= 1:
165
- _LOGGER.info(f"🤖 Starting optimization with {searcher.__class__.__name__} for {num_generations} generations...")
166
- for _ in trange(num_generations, desc="Optimizing"):
167
- searcher.step()
205
+ searcher = searcher_factory()
206
+ _LOGGER.info(f"🤖 Starting optimization with {searcher.__class__.__name__} Algorithm for {num_generations} generations...")
207
+ # for _ in trange(num_generations, desc="Optimizing"):
208
+ # searcher.step()
209
+
210
+ # Attach logger if requested
211
+ if verbose:
212
+ pandas_logger = PandasLogger(searcher)
213
+
214
+ searcher.run(num_generations) # Use the built-in run method for simplicity
215
+
216
+ # # DEBUG new searcher objects
217
+ # for status_key in searcher.iter_status_keys():
218
+ # print("===", status_key, "===")
219
+ # print(searcher.status[status_key])
220
+ # print()
221
+
222
+ # Get results from the .status dictionary
223
+ # SNES and CEM use the key 'center' to get mean values if needed best_solution_tensor = searcher.status["center"]
224
+ best_solution_container = searcher.status["pop_best"]
225
+ best_solution_tensor = best_solution_container.values
226
+ best_fitness = best_solution_container.evals
168
227
 
169
- best_solution_tensor, best_fitness = searcher.best
170
228
  best_solution_np = best_solution_tensor.cpu().numpy()
171
229
 
172
230
  # threshold binary features
@@ -179,6 +237,11 @@ def run_optimization(
179
237
  result_dict[target_name] = best_fitness.item()
180
238
 
181
239
  _save_result(result_dict, 'csv', csv_path) # Single run defaults to CSV
240
+
241
+ # Process logger
242
+ if verbose:
243
+ _handle_pandas_log(pandas_logger, save_path=save_path)
244
+
182
245
  _LOGGER.info(f"✅ Optimization complete. Best solution saved to '{csv_path.name}'")
183
246
  return result_dict
184
247
 
@@ -193,17 +256,26 @@ def run_optimization(
193
256
  schema = {name: "REAL" for name in feature_names}
194
257
  schema[target_name] = "REAL"
195
258
  db_manager.create_table(db_table_name, schema)
196
-
259
+
260
+ print("")
261
+ # Repetitions loop
262
+ pandas_logger = None
197
263
  for i in trange(repetitions, desc="Repetitions"):
198
- _LOGGER.info(f"--- Starting Repetition {i+1}/{repetitions} ---")
264
+ # CRITICAL: Create a fresh searcher for each run using the factory
265
+ searcher = searcher_factory()
199
266
 
200
- # CRITICAL: Re-initialize the searcher to ensure each run is independent
201
- searcher.reset()
202
-
203
- for _ in range(num_generations): # Inner loop does not need a progress bar
204
- searcher.step()
205
-
206
- best_solution_tensor, best_fitness = searcher.best
267
+ # Attach logger if requested
268
+ if verbose and i==0:
269
+ pandas_logger = PandasLogger(searcher)
270
+
271
+ searcher.run(num_generations) # Use the built-in run method for simplicity
272
+
273
+ # Get results from the .status dictionary
274
+ # SNES and CEM use the key 'center' to get mean values if needed best_solution_tensor = searcher.status["center"]
275
+ best_solution_container = searcher.status["pop_best"]
276
+ best_solution_tensor = best_solution_container.values
277
+ best_fitness = best_solution_container.evals
278
+
207
279
  best_solution_np = best_solution_tensor.cpu().numpy()
208
280
 
209
281
  # threshold binary features
@@ -212,15 +284,25 @@ def run_optimization(
212
284
  else:
213
285
  best_solution_thresholded = best_solution_np
214
286
 
287
+ # make results dictionary
215
288
  result_dict = {name: value for name, value in zip(feature_names, best_solution_thresholded)}
216
289
  result_dict[target_name] = best_fitness.item()
217
290
 
218
291
  # Save each result incrementally
219
292
  _save_result(result_dict, save_format, csv_path, db_manager, db_table_name)
293
+
294
+ # Process logger
295
+ if pandas_logger is not None:
296
+ _handle_pandas_log(pandas_logger, save_path=save_path)
220
297
 
221
298
  _LOGGER.info(f"✅ Optimal solution space complete. Results saved to '{save_path}'")
222
299
  return None
223
300
 
224
301
 
302
+ def _handle_pandas_log(logger: PandasLogger, save_path: Path):
303
+ log_dataframe = logger.to_dataframe()
304
+ save_dataframe(df=log_dataframe, save_dir=save_path / "EvolutionLog", filename="evolution")
305
+
306
+
225
307
  def info():
226
308
  _script_info(__all__)