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.
Files changed (37) hide show
  1. alchemist_core/__init__.py +14 -7
  2. alchemist_core/acquisition/botorch_acquisition.py +14 -6
  3. alchemist_core/audit_log.py +594 -0
  4. alchemist_core/data/experiment_manager.py +69 -5
  5. alchemist_core/models/botorch_model.py +6 -4
  6. alchemist_core/models/sklearn_model.py +44 -6
  7. alchemist_core/session.py +600 -8
  8. alchemist_core/utils/doe.py +200 -0
  9. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/METADATA +57 -40
  10. alchemist_nrel-0.3.0.dist-info/RECORD +66 -0
  11. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/entry_points.txt +1 -0
  12. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/top_level.txt +1 -0
  13. api/main.py +19 -3
  14. api/models/requests.py +71 -0
  15. api/models/responses.py +144 -0
  16. api/routers/experiments.py +117 -5
  17. api/routers/sessions.py +329 -10
  18. api/routers/visualizations.py +10 -5
  19. api/services/session_store.py +210 -54
  20. api/static/NEW_ICON.ico +0 -0
  21. api/static/NEW_ICON.png +0 -0
  22. api/static/NEW_LOGO_DARK.png +0 -0
  23. api/static/NEW_LOGO_LIGHT.png +0 -0
  24. api/static/assets/api-vcoXEqyq.js +1 -0
  25. api/static/assets/index-C0_glioA.js +4084 -0
  26. api/static/assets/index-CB4V1LI5.css +1 -0
  27. api/static/index.html +14 -0
  28. api/static/vite.svg +1 -0
  29. run_api.py +55 -0
  30. ui/gpr_panel.py +7 -2
  31. ui/notifications.py +197 -10
  32. ui/ui.py +1117 -68
  33. ui/variables_setup.py +47 -2
  34. ui/visualizations.py +60 -3
  35. alchemist_nrel-0.2.1.dist-info/RECORD +0 -54
  36. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/WHEEL +0 -0
  37. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.0.dist-info}/licenses/LICENSE +0 -0
api/models/responses.py CHANGED
@@ -88,6 +88,31 @@ class SessionInfoResponse(BaseModel):
88
88
  )
89
89
 
90
90
 
91
+ class SessionStateResponse(BaseModel):
92
+ """Current state of an optimization session for monitoring."""
93
+ session_id: str
94
+ n_variables: int
95
+ n_experiments: int
96
+ model_trained: bool
97
+ model_backend: Optional[str] = None
98
+ last_suggestion: Optional[Dict[str, Any]] = None
99
+ last_acquisition_value: Optional[float] = None
100
+
101
+ model_config = ConfigDict(
102
+ json_schema_extra={
103
+ "example": {
104
+ "session_id": "550e8400-e29b-41d4-a716-446655440000",
105
+ "n_variables": 2,
106
+ "n_experiments": 15,
107
+ "model_trained": True,
108
+ "model_backend": "botorch",
109
+ "last_suggestion": {"temperature": 385.2, "flow_rate": 4.3},
110
+ "last_acquisition_value": 0.025
111
+ }
112
+ }
113
+ )
114
+
115
+
91
116
  # ============================================================
92
117
  # Variable Models
93
118
  # ============================================================
@@ -112,6 +137,11 @@ class ExperimentResponse(BaseModel):
112
137
  """Response when adding an experiment."""
113
138
  message: str = "Experiment added successfully"
114
139
  n_experiments: int
140
+ model_trained: bool = Field(default=False, description="Whether model was auto-trained")
141
+ training_metrics: Optional[Dict[str, Any]] = Field(
142
+ None,
143
+ description="Training metrics if auto-train was enabled"
144
+ )
115
145
 
116
146
 
117
147
  class ExperimentsListResponse(BaseModel):
@@ -146,6 +176,30 @@ class ExperimentsSummaryResponse(BaseModel):
146
176
  )
147
177
 
148
178
 
179
+ # ============================================================
180
+ # Initial Design (DoE) Models
181
+ # ============================================================
182
+
183
+ class InitialDesignResponse(BaseModel):
184
+ """Response containing generated initial design points."""
185
+ points: List[Dict[str, Any]] = Field(..., description="Generated experimental points")
186
+ method: str = Field(..., description="Sampling method used")
187
+ n_points: int = Field(..., description="Number of points generated")
188
+
189
+ model_config = ConfigDict(
190
+ json_schema_extra={
191
+ "example": {
192
+ "points": [
193
+ {"temperature": 350.2, "flow_rate": 2.47},
194
+ {"temperature": 421.8, "flow_rate": 7.92}
195
+ ],
196
+ "method": "lhs",
197
+ "n_points": 2
198
+ }
199
+ }
200
+ )
201
+
202
+
149
203
  # ============================================================
150
204
  # Model Training Models
151
205
  # ============================================================
@@ -290,3 +344,93 @@ class ErrorResponse(BaseModel):
290
344
  }
291
345
  }
292
346
  )
347
+
348
+
349
+ # ============================================================
350
+ # Audit Log & Session Management Responses
351
+ # ============================================================
352
+
353
+ class SessionMetadataResponse(BaseModel):
354
+ """Response containing session metadata."""
355
+ session_id: str
356
+ name: str
357
+ created_at: str
358
+ last_modified: str
359
+ description: str
360
+ tags: List[str]
361
+
362
+ model_config = ConfigDict(
363
+ json_schema_extra={
364
+ "example": {
365
+ "session_id": "550e8400-e29b-41d4-a716-446655440000",
366
+ "name": "Catalyst_Screening_Nov2025",
367
+ "created_at": "2025-11-19T09:00:00",
368
+ "last_modified": "2025-11-19T14:30:00",
369
+ "description": "Pt/Pd ratio optimization",
370
+ "tags": ["catalyst", "CO2"]
371
+ }
372
+ }
373
+ )
374
+
375
+
376
+ class AuditEntryResponse(BaseModel):
377
+ """Response containing a single audit log entry."""
378
+ timestamp: str
379
+ entry_type: str
380
+ parameters: Dict[str, Any]
381
+ hash: str
382
+ notes: str
383
+
384
+ model_config = ConfigDict(
385
+ json_schema_extra={
386
+ "example": {
387
+ "timestamp": "2025-11-19T09:15:00",
388
+ "entry_type": "data_locked",
389
+ "parameters": {
390
+ "n_experiments": 15,
391
+ "variables": [],
392
+ "data_hash": "abc123"
393
+ },
394
+ "hash": "a1b2c3d4",
395
+ "notes": "Initial screening dataset"
396
+ }
397
+ }
398
+ )
399
+
400
+
401
+ class AuditLogResponse(BaseModel):
402
+ """Response containing complete audit log."""
403
+ entries: List[AuditEntryResponse]
404
+ n_entries: int
405
+
406
+ model_config = ConfigDict(
407
+ json_schema_extra={
408
+ "example": {
409
+ "entries": [],
410
+ "n_entries": 0
411
+ }
412
+ }
413
+ )
414
+
415
+
416
+ class LockDecisionResponse(BaseModel):
417
+ """Response after locking a decision."""
418
+ success: bool
419
+ entry: AuditEntryResponse
420
+ message: str
421
+
422
+ model_config = ConfigDict(
423
+ json_schema_extra={
424
+ "example": {
425
+ "success": True,
426
+ "entry": {
427
+ "timestamp": "2025-11-19T09:15:00",
428
+ "entry_type": "data_locked",
429
+ "parameters": {},
430
+ "hash": "a1b2c3d4",
431
+ "notes": ""
432
+ },
433
+ "message": "Data decision locked successfully"
434
+ }
435
+ }
436
+ )
@@ -2,9 +2,14 @@
2
2
  Experiments router - Experimental data management.
3
3
  """
4
4
 
5
- from fastapi import APIRouter, Depends, UploadFile, File
6
- from ..models.requests import AddExperimentRequest, AddExperimentsBatchRequest
7
- from ..models.responses import ExperimentResponse, ExperimentsListResponse, ExperimentsSummaryResponse
5
+ from fastapi import APIRouter, Depends, UploadFile, File, Query
6
+ from ..models.requests import AddExperimentRequest, AddExperimentsBatchRequest, InitialDesignRequest
7
+ from ..models.responses import (
8
+ ExperimentResponse,
9
+ ExperimentsListResponse,
10
+ ExperimentsSummaryResponse,
11
+ InitialDesignResponse
12
+ )
8
13
  from ..dependencies import get_session
9
14
  from ..middleware.error_handlers import NoVariablesError
10
15
  from alchemist_core.session import OptimizationSession
@@ -12,6 +17,7 @@ import logging
12
17
  import pandas as pd
13
18
  import tempfile
14
19
  import os
20
+ from typing import Optional
15
21
 
16
22
  logger = logging.getLogger(__name__)
17
23
 
@@ -22,6 +28,9 @@ router = APIRouter()
22
28
  async def add_experiment(
23
29
  session_id: str,
24
30
  experiment: AddExperimentRequest,
31
+ auto_train: bool = Query(False, description="Auto-train model after adding data"),
32
+ training_backend: Optional[str] = Query(None, description="Model backend (sklearn/botorch)"),
33
+ training_kernel: Optional[str] = Query(None, description="Kernel type (rbf/matern)"),
25
34
  session: OptimizationSession = Depends(get_session)
26
35
  ):
27
36
  """
@@ -29,6 +38,11 @@ async def add_experiment(
29
38
 
30
39
  The experiment must include values for all defined variables.
31
40
  Output value is optional for candidate experiments.
41
+
42
+ Args:
43
+ auto_train: If True, retrain model after adding data
44
+ training_backend: Model backend (uses last if None)
45
+ training_kernel: Kernel type (uses last or 'rbf' if None)
32
46
  """
33
47
  # Check if variables are defined
34
48
  if len(session.search_space.variables) == 0:
@@ -43,9 +57,34 @@ async def add_experiment(
43
57
  n_experiments = len(session.experiment_manager.df)
44
58
  logger.info(f"Added experiment to session {session_id}. Total: {n_experiments}")
45
59
 
60
+ # Auto-train if requested
61
+ model_trained = False
62
+ training_metrics = None
63
+
64
+ if auto_train and n_experiments >= 5: # Minimum data for training
65
+ try:
66
+ # Use previous config or provided config
67
+ backend = training_backend or (session.model_backend if session.model else "sklearn")
68
+ kernel = training_kernel or "rbf"
69
+
70
+ result = session.train_model(backend=backend, kernel=kernel)
71
+ model_trained = True
72
+ metrics = result.get("metrics", {})
73
+ training_metrics = {
74
+ "rmse": metrics.get("rmse"),
75
+ "r2": metrics.get("r2"),
76
+ "backend": backend
77
+ }
78
+ logger.info(f"Auto-trained model for session {session_id}: {training_metrics}")
79
+ except Exception as e:
80
+ logger.error(f"Auto-train failed for session {session_id}: {e}")
81
+ # Don't fail the whole request, just log it
82
+
46
83
  return ExperimentResponse(
47
84
  message="Experiment added successfully",
48
- n_experiments=n_experiments
85
+ n_experiments=n_experiments,
86
+ model_trained=model_trained,
87
+ training_metrics=training_metrics
49
88
  )
50
89
 
51
90
 
@@ -53,12 +92,20 @@ async def add_experiment(
53
92
  async def add_experiments_batch(
54
93
  session_id: str,
55
94
  batch: AddExperimentsBatchRequest,
95
+ auto_train: bool = Query(False, description="Auto-train model after adding data"),
96
+ training_backend: Optional[str] = Query(None, description="Model backend (sklearn/botorch)"),
97
+ training_kernel: Optional[str] = Query(None, description="Kernel type (rbf/matern)"),
56
98
  session: OptimizationSession = Depends(get_session)
57
99
  ):
58
100
  """
59
101
  Add multiple experiments at once.
60
102
 
61
103
  Useful for bulk data import or initialization.
104
+
105
+ Args:
106
+ auto_train: If True, retrain model after adding data
107
+ training_backend: Model backend (uses last if None)
108
+ training_kernel: Kernel type (uses last or 'rbf' if None)
62
109
  """
63
110
  # Check if variables are defined
64
111
  if len(session.search_space.variables) == 0:
@@ -74,9 +121,74 @@ async def add_experiments_batch(
74
121
  n_experiments = len(session.experiment_manager.df)
75
122
  logger.info(f"Added {len(batch.experiments)} experiments to session {session_id}. Total: {n_experiments}")
76
123
 
124
+ # Auto-train if requested
125
+ model_trained = False
126
+ training_metrics = None
127
+
128
+ if auto_train and n_experiments >= 5: # Minimum data for training
129
+ try:
130
+ backend = training_backend or (session.model_backend if session.model else "sklearn")
131
+ kernel = training_kernel or "rbf"
132
+
133
+ result = session.train_model(backend=backend, kernel=kernel)
134
+ model_trained = True
135
+ metrics = result.get("metrics", {})
136
+ training_metrics = {
137
+ "rmse": metrics.get("rmse"),
138
+ "r2": metrics.get("r2"),
139
+ "backend": backend
140
+ }
141
+ logger.info(f"Auto-trained model for session {session_id}: {training_metrics}")
142
+ except Exception as e:
143
+ logger.error(f"Auto-train failed for session {session_id}: {e}")
144
+
77
145
  return ExperimentResponse(
78
146
  message=f"Added {len(batch.experiments)} experiments successfully",
79
- n_experiments=n_experiments
147
+ n_experiments=n_experiments,
148
+ model_trained=model_trained,
149
+ training_metrics=training_metrics
150
+ )
151
+
152
+
153
+ @router.post("/{session_id}/initial-design", response_model=InitialDesignResponse)
154
+ async def generate_initial_design(
155
+ session_id: str,
156
+ request: InitialDesignRequest,
157
+ session: OptimizationSession = Depends(get_session)
158
+ ):
159
+ """
160
+ Generate initial experimental design (DoE) for autonomous operation.
161
+
162
+ Generates space-filling experimental designs before Bayesian optimization begins.
163
+ Useful for autonomous controllers to get initial points to evaluate.
164
+
165
+ Methods:
166
+ - random: Random sampling
167
+ - lhs: Latin Hypercube Sampling (space-filling)
168
+ - sobol: Sobol sequence (quasi-random)
169
+ - halton: Halton sequence (quasi-random)
170
+ - hammersly: Hammersly sequence (quasi-random)
171
+
172
+ Returns list of experiments (input combinations) to evaluate.
173
+ """
174
+ # Check if variables are defined
175
+ if len(session.search_space.variables) == 0:
176
+ raise NoVariablesError("No variables defined. Add variables to search space first.")
177
+
178
+ # Generate design
179
+ design_points = session.generate_initial_design(
180
+ method=request.method,
181
+ n_points=request.n_points,
182
+ random_seed=request.random_seed,
183
+ lhs_criterion=request.lhs_criterion
184
+ )
185
+
186
+ logger.info(f"Generated {len(design_points)} initial design points using {request.method} for session {session_id}")
187
+
188
+ return InitialDesignResponse(
189
+ points=design_points,
190
+ method=request.method,
191
+ n_points=len(design_points)
80
192
  )
81
193
 
82
194