alchemist-nrel 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alchemist_core/__init__.py +14 -7
- alchemist_core/acquisition/botorch_acquisition.py +14 -6
- alchemist_core/audit_log.py +594 -0
- alchemist_core/data/experiment_manager.py +69 -5
- alchemist_core/models/botorch_model.py +6 -4
- alchemist_core/models/sklearn_model.py +44 -6
- alchemist_core/session.py +600 -8
- alchemist_core/utils/doe.py +200 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/METADATA +57 -40
- alchemist_nrel-0.3.0.dist-info/RECORD +66 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/entry_points.txt +1 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/top_level.txt +1 -0
- api/main.py +19 -3
- api/models/requests.py +71 -0
- api/models/responses.py +144 -0
- api/routers/experiments.py +117 -5
- api/routers/sessions.py +329 -10
- api/routers/visualizations.py +10 -5
- api/services/session_store.py +210 -54
- api/static/NEW_ICON.ico +0 -0
- api/static/NEW_ICON.png +0 -0
- api/static/NEW_LOGO_DARK.png +0 -0
- api/static/NEW_LOGO_LIGHT.png +0 -0
- api/static/assets/api-vcoXEqyq.js +1 -0
- api/static/assets/index-C0_glioA.js +4084 -0
- api/static/assets/index-CB4V1LI5.css +1 -0
- api/static/index.html +14 -0
- api/static/vite.svg +1 -0
- run_api.py +55 -0
- ui/gpr_panel.py +7 -2
- ui/notifications.py +197 -10
- ui/ui.py +1117 -68
- ui/variables_setup.py +47 -2
- ui/visualizations.py +60 -3
- alchemist_nrel-0.2.1.dist-info/RECORD +0 -54
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/WHEEL +0 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,13 +13,15 @@ class ExperimentManager:
|
|
|
13
13
|
self.df = pd.DataFrame() # Raw experimental data
|
|
14
14
|
self.search_space = search_space # Reference to the search space
|
|
15
15
|
self.filepath = None # Path to saved experiment file
|
|
16
|
+
self._current_iteration = 0 # Track current iteration for audit log
|
|
16
17
|
|
|
17
18
|
def set_search_space(self, search_space):
|
|
18
19
|
"""Set or update the search space reference."""
|
|
19
20
|
self.search_space = search_space
|
|
20
21
|
|
|
21
22
|
def add_experiment(self, point_dict: Dict[str, Union[float, str, int]], output_value: Optional[float] = None,
|
|
22
|
-
noise_value: Optional[float] = None
|
|
23
|
+
noise_value: Optional[float] = None, iteration: Optional[int] = None,
|
|
24
|
+
reason: Optional[str] = None):
|
|
23
25
|
"""
|
|
24
26
|
Add a single experiment point.
|
|
25
27
|
|
|
@@ -27,6 +29,8 @@ class ExperimentManager:
|
|
|
27
29
|
point_dict: Dictionary with variable names as keys and values
|
|
28
30
|
output_value: The experiment output/target value (if known)
|
|
29
31
|
noise_value: Optional observation noise/uncertainty value for regularization
|
|
32
|
+
iteration: Iteration number (auto-assigned if None)
|
|
33
|
+
reason: Reason for this experiment (e.g., 'Initial Design (LHS)', 'Expected Improvement')
|
|
30
34
|
"""
|
|
31
35
|
# Create a copy of the point_dict to avoid modifying the original
|
|
32
36
|
new_point = point_dict.copy()
|
|
@@ -38,6 +42,22 @@ class ExperimentManager:
|
|
|
38
42
|
# Add noise value if provided
|
|
39
43
|
if noise_value is not None:
|
|
40
44
|
new_point['Noise'] = noise_value
|
|
45
|
+
|
|
46
|
+
# Add iteration tracking
|
|
47
|
+
if iteration is not None:
|
|
48
|
+
# Use provided iteration and ensure _current_iteration reflects it
|
|
49
|
+
new_point['Iteration'] = int(iteration)
|
|
50
|
+
# Keep _current_iteration in sync with the latest explicit iteration
|
|
51
|
+
try:
|
|
52
|
+
self._current_iteration = int(iteration)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
else:
|
|
56
|
+
# Use current iteration (doesn't increment until lock_acquisition)
|
|
57
|
+
new_point['Iteration'] = int(self._current_iteration)
|
|
58
|
+
|
|
59
|
+
# Add reason
|
|
60
|
+
new_point['Reason'] = reason if reason is not None else 'Manual'
|
|
41
61
|
|
|
42
62
|
# Convert to DataFrame and append
|
|
43
63
|
new_df = pd.DataFrame([new_point])
|
|
@@ -52,6 +72,20 @@ class ExperimentManager:
|
|
|
52
72
|
if missing_cols:
|
|
53
73
|
raise ValueError(f"DataFrame is missing required columns: {missing_cols}")
|
|
54
74
|
|
|
75
|
+
# Ensure each row has an Iteration value; default to current iteration
|
|
76
|
+
if 'Iteration' not in data_df.columns:
|
|
77
|
+
data_df = data_df.copy()
|
|
78
|
+
data_df['Iteration'] = int(self._current_iteration)
|
|
79
|
+
else:
|
|
80
|
+
# Fill missing iterations with current iteration
|
|
81
|
+
data_df = data_df.copy()
|
|
82
|
+
data_df['Iteration'] = pd.to_numeric(data_df['Iteration'], errors='coerce').fillna(self._current_iteration).astype(int)
|
|
83
|
+
# Update _current_iteration to the max iteration present
|
|
84
|
+
if len(data_df) > 0:
|
|
85
|
+
max_iter = int(data_df['Iteration'].max())
|
|
86
|
+
if max_iter > self._current_iteration:
|
|
87
|
+
self._current_iteration = max_iter
|
|
88
|
+
|
|
55
89
|
# Append the data
|
|
56
90
|
self.df = pd.concat([self.df, data_df], ignore_index=True)
|
|
57
91
|
|
|
@@ -69,8 +103,17 @@ class ExperimentManager:
|
|
|
69
103
|
"""
|
|
70
104
|
if 'Output' not in self.df.columns:
|
|
71
105
|
raise ValueError("DataFrame doesn't contain 'Output' column")
|
|
72
|
-
|
|
73
|
-
|
|
106
|
+
|
|
107
|
+
# Drop metadata columns (Output, Noise, Iteration, Reason)
|
|
108
|
+
metadata_cols = ['Output']
|
|
109
|
+
if 'Noise' in self.df.columns:
|
|
110
|
+
metadata_cols.append('Noise')
|
|
111
|
+
if 'Iteration' in self.df.columns:
|
|
112
|
+
metadata_cols.append('Iteration')
|
|
113
|
+
if 'Reason' in self.df.columns:
|
|
114
|
+
metadata_cols.append('Reason')
|
|
115
|
+
|
|
116
|
+
X = self.df.drop(columns=metadata_cols)
|
|
74
117
|
y = self.df['Output']
|
|
75
118
|
return X, y
|
|
76
119
|
|
|
@@ -85,8 +128,17 @@ class ExperimentManager:
|
|
|
85
128
|
"""
|
|
86
129
|
if 'Output' not in self.df.columns:
|
|
87
130
|
raise ValueError("DataFrame doesn't contain 'Output' column")
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
|
|
132
|
+
# Drop metadata columns
|
|
133
|
+
metadata_cols = ['Output']
|
|
134
|
+
if 'Noise' in self.df.columns:
|
|
135
|
+
metadata_cols.append('Noise')
|
|
136
|
+
if 'Iteration' in self.df.columns:
|
|
137
|
+
metadata_cols.append('Iteration')
|
|
138
|
+
if 'Reason' in self.df.columns:
|
|
139
|
+
metadata_cols.append('Reason')
|
|
140
|
+
|
|
141
|
+
X = self.df.drop(columns=metadata_cols)
|
|
90
142
|
y = self.df['Output']
|
|
91
143
|
noise = self.df['Noise'] if 'Noise' in self.df.columns else None
|
|
92
144
|
return X, y, noise
|
|
@@ -129,6 +181,18 @@ class ExperimentManager:
|
|
|
129
181
|
print("Warning: Noise column contains non-numeric values. Converting to default noise level.")
|
|
130
182
|
self.df['Noise'] = 1e-10 # Default small noise
|
|
131
183
|
|
|
184
|
+
# Initialize iteration tracking from data
|
|
185
|
+
if 'Iteration' in self.df.columns:
|
|
186
|
+
self._current_iteration = int(self.df['Iteration'].max())
|
|
187
|
+
else:
|
|
188
|
+
# Add iteration column if missing (legacy data)
|
|
189
|
+
self.df['Iteration'] = 0
|
|
190
|
+
self._current_iteration = 0
|
|
191
|
+
|
|
192
|
+
# Add reason column if missing (legacy data)
|
|
193
|
+
if 'Reason' not in self.df.columns:
|
|
194
|
+
self.df['Reason'] = 'Initial Design'
|
|
195
|
+
|
|
132
196
|
return self
|
|
133
197
|
|
|
134
198
|
@classmethod
|
|
@@ -485,8 +485,10 @@ class BoTorchModel(BaseModel):
|
|
|
485
485
|
outcome_transform=fold_outcome_transform
|
|
486
486
|
)
|
|
487
487
|
|
|
488
|
-
#
|
|
489
|
-
|
|
488
|
+
# Train the fold model from scratch (don't load state_dict to avoid dimension mismatches)
|
|
489
|
+
# This is necessary because folds may have different categorical values or data shapes
|
|
490
|
+
mll = ExactMarginalLogLikelihood(fold_model.likelihood, fold_model)
|
|
491
|
+
fit_gpytorch_mll(mll)
|
|
490
492
|
|
|
491
493
|
# Make predictions on test fold
|
|
492
494
|
fold_model.eval()
|
|
@@ -720,8 +722,8 @@ class BoTorchModel(BaseModel):
|
|
|
720
722
|
y_vals = torch.linspace(y_range[0], y_range[1], 100)
|
|
721
723
|
X, Y = torch.meshgrid(x_vals, y_vals, indexing='ij')
|
|
722
724
|
|
|
723
|
-
# Total dimensions in the model
|
|
724
|
-
input_dim = len(self.
|
|
725
|
+
# Total dimensions in the model (use original_feature_names to match actual input dimensions)
|
|
726
|
+
input_dim = len(self.original_feature_names) if self.original_feature_names else 2
|
|
725
727
|
|
|
726
728
|
# Create placeholder tensors for all dimensions
|
|
727
729
|
grid_tensors = []
|
|
@@ -85,9 +85,30 @@ class SklearnModel(BaseModel):
|
|
|
85
85
|
def _build_kernel(self, X):
|
|
86
86
|
"""Build the kernel using training data X to initialize length scales."""
|
|
87
87
|
kernel_type = self.kernel_options.get("kernel_type", "RBF")
|
|
88
|
-
# Compute initial length scales
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
# Compute initial length scales from the data.
|
|
89
|
+
# Use standard deviation (positive) as a robust length-scale initializer.
|
|
90
|
+
try:
|
|
91
|
+
ls_init = np.std(X, axis=0)
|
|
92
|
+
ls_init = np.array(ls_init, dtype=float)
|
|
93
|
+
# Replace non-finite or non-positive values with sensible defaults
|
|
94
|
+
bad_mask = ~np.isfinite(ls_init) | (ls_init <= 0)
|
|
95
|
+
if np.any(bad_mask):
|
|
96
|
+
logger.debug("Replacing non-finite or non-positive length-scales with 1.0")
|
|
97
|
+
ls_init[bad_mask] = 1.0
|
|
98
|
+
|
|
99
|
+
# Build finite, positive bounds for each length-scale
|
|
100
|
+
ls_bounds = []
|
|
101
|
+
for l in ls_init:
|
|
102
|
+
# Protect against extremely small or non-finite upper bounds
|
|
103
|
+
upper = float(l * 1e5) if np.isfinite(l) else 1e5
|
|
104
|
+
if not np.isfinite(upper) or upper <= 1e-8:
|
|
105
|
+
upper = 1e3
|
|
106
|
+
ls_bounds.append((1e-5, upper))
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"Failed to compute sensible length-scales from data: {e}. Using safe defaults.")
|
|
109
|
+
n_dims = X.shape[1] if hasattr(X, 'shape') else 1
|
|
110
|
+
ls_init = np.ones(n_dims, dtype=float)
|
|
111
|
+
ls_bounds = [(1e-5, 1e5) for _ in range(n_dims)]
|
|
91
112
|
constant = C()
|
|
92
113
|
if kernel_type == "RBF":
|
|
93
114
|
kernel = constant * RBF(length_scale=ls_init, length_scale_bounds=ls_bounds)
|
|
@@ -317,12 +338,29 @@ class SklearnModel(BaseModel):
|
|
|
317
338
|
|
|
318
339
|
# Create model with appropriate parameters
|
|
319
340
|
self.model = GaussianProcessRegressor(**params)
|
|
320
|
-
|
|
341
|
+
|
|
321
342
|
# Store the raw training data for possible reuse with skopt
|
|
322
343
|
self.X_train_ = X
|
|
323
344
|
self.y_train_ = y
|
|
324
|
-
|
|
325
|
-
|
|
345
|
+
|
|
346
|
+
# Fit the model, but be defensive: if sklearn complains about non-finite
|
|
347
|
+
# bounds when n_restarts_optimizer>0, retry with no restarts.
|
|
348
|
+
try:
|
|
349
|
+
self.model.fit(X, y)
|
|
350
|
+
except ValueError as e:
|
|
351
|
+
msg = str(e)
|
|
352
|
+
if 'requires that all bounds are finite' in msg or 'bounds' in msg.lower():
|
|
353
|
+
logger.warning("GaussianProcessRegressor failed due to non-finite bounds. "
|
|
354
|
+
"Retrying without optimizer restarts (n_restarts_optimizer=0).")
|
|
355
|
+
# Retry with safer parameters
|
|
356
|
+
safe_params = params.copy()
|
|
357
|
+
safe_params['n_restarts_optimizer'] = 0
|
|
358
|
+
safe_params['optimizer'] = None
|
|
359
|
+
self.model = GaussianProcessRegressor(**safe_params)
|
|
360
|
+
self.model.fit(X, y)
|
|
361
|
+
else:
|
|
362
|
+
# Re-raise other value errors
|
|
363
|
+
raise
|
|
326
364
|
self.optimized_kernel = self.model.kernel_
|
|
327
365
|
self._is_trained = True
|
|
328
366
|
|