alchemist-nrel 0.2.1__py3-none-any.whl → 0.3.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.
- alchemist_core/__init__.py +14 -7
- alchemist_core/acquisition/botorch_acquisition.py +15 -6
- alchemist_core/audit_log.py +594 -0
- alchemist_core/data/experiment_manager.py +76 -5
- alchemist_core/models/botorch_model.py +6 -4
- alchemist_core/models/sklearn_model.py +74 -8
- alchemist_core/session.py +788 -39
- alchemist_core/utils/doe.py +200 -0
- alchemist_nrel-0.3.1.dist-info/METADATA +185 -0
- alchemist_nrel-0.3.1.dist-info/RECORD +66 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/entry_points.txt +1 -0
- api/example_client.py +7 -2
- api/main.py +21 -4
- api/models/requests.py +95 -1
- api/models/responses.py +167 -0
- api/routers/acquisition.py +25 -0
- api/routers/experiments.py +134 -6
- api/routers/sessions.py +438 -10
- api/routers/visualizations.py +10 -5
- api/routers/websocket.py +132 -0
- api/run_api.py +56 -0
- api/services/session_store.py +285 -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-DWfIKU9j.js +4094 -0
- api/static/assets/index-sMIa_1hV.css +1 -0
- api/static/index.html +14 -0
- api/static/vite.svg +1 -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_core/models/ax_model.py +0 -159
- alchemist_nrel-0.2.1.dist-info/METADATA +0 -206
- alchemist_nrel-0.2.1.dist-info/RECORD +0 -54
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/WHEEL +0 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Design of Experiments (DoE) - Initial sampling strategies for Bayesian optimization.
|
|
3
|
+
|
|
4
|
+
This module provides methods for generating initial experimental designs before
|
|
5
|
+
starting the optimization loop. Supported methods:
|
|
6
|
+
- Random sampling
|
|
7
|
+
- Latin Hypercube Sampling (LHS)
|
|
8
|
+
- Sobol sequences
|
|
9
|
+
- Halton sequences
|
|
10
|
+
- Hammersly sequences
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import List, Dict, Optional, Literal, Any
|
|
14
|
+
import numpy as np
|
|
15
|
+
from skopt.sampler import Lhs, Sobol, Hammersly
|
|
16
|
+
from skopt.space import Real, Integer, Categorical
|
|
17
|
+
from alchemist_core.data.search_space import SearchSpace
|
|
18
|
+
from alchemist_core.config import get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_initial_design(
|
|
24
|
+
search_space: SearchSpace,
|
|
25
|
+
method: Literal["random", "lhs", "sobol", "halton", "hammersly"] = "lhs",
|
|
26
|
+
n_points: int = 10,
|
|
27
|
+
random_seed: Optional[int] = None,
|
|
28
|
+
lhs_criterion: str = "maximin"
|
|
29
|
+
) -> List[Dict[str, Any]]:
|
|
30
|
+
"""
|
|
31
|
+
Generate initial experimental design using specified sampling strategy.
|
|
32
|
+
|
|
33
|
+
This function creates a set of experimental conditions to evaluate before
|
|
34
|
+
starting Bayesian optimization. Different methods provide different
|
|
35
|
+
space-filling properties:
|
|
36
|
+
|
|
37
|
+
- **random**: Uniform random sampling
|
|
38
|
+
- **lhs**: Latin Hypercube Sampling (recommended for most cases)
|
|
39
|
+
- **sobol**: Sobol quasi-random sequences (low discrepancy)
|
|
40
|
+
- **halton**: Halton sequences (via Hammersly sampler)
|
|
41
|
+
- **hammersly**: Hammersly sequences (low discrepancy)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
search_space: SearchSpace object with defined variables
|
|
45
|
+
method: Sampling method to use
|
|
46
|
+
n_points: Number of points to generate
|
|
47
|
+
random_seed: Random seed for reproducibility (applies to random and lhs)
|
|
48
|
+
lhs_criterion: Criterion for LHS optimization ("maximin", "correlation", "ratio")
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of dictionaries, each containing variable names and values.
|
|
52
|
+
Does NOT include 'Output' column - experiments need to be evaluated.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If search_space has no variables or method is unknown
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> from alchemist_core import SearchSpace
|
|
59
|
+
>>> from alchemist_core.utils.doe import generate_initial_design
|
|
60
|
+
>>>
|
|
61
|
+
>>> # Define search space
|
|
62
|
+
>>> space = SearchSpace()
|
|
63
|
+
>>> space.add_variable('temperature', 'real', min=300, max=500)
|
|
64
|
+
>>> space.add_variable('pressure', 'real', min=1, max=10)
|
|
65
|
+
>>>
|
|
66
|
+
>>> # Generate 10 LHS points
|
|
67
|
+
>>> points = generate_initial_design(space, method='lhs', n_points=10)
|
|
68
|
+
>>>
|
|
69
|
+
>>> # Points ready for experimentation
|
|
70
|
+
>>> for point in points:
|
|
71
|
+
>>> print(point) # {'temperature': 350.2, 'pressure': 4.7}
|
|
72
|
+
"""
|
|
73
|
+
# Validate inputs
|
|
74
|
+
if len(search_space.variables) == 0:
|
|
75
|
+
raise ValueError("SearchSpace has no variables. Define variables before generating initial design.")
|
|
76
|
+
|
|
77
|
+
if n_points < 1:
|
|
78
|
+
raise ValueError(f"n_points must be >= 1, got {n_points}")
|
|
79
|
+
|
|
80
|
+
# Set random seed if provided
|
|
81
|
+
if random_seed is not None:
|
|
82
|
+
np.random.seed(random_seed)
|
|
83
|
+
logger.info(f"Set random seed to {random_seed} for reproducibility")
|
|
84
|
+
|
|
85
|
+
# Get skopt dimensions from SearchSpace
|
|
86
|
+
skopt_space = search_space.skopt_dimensions
|
|
87
|
+
|
|
88
|
+
# Generate samples based on method
|
|
89
|
+
if method == "random":
|
|
90
|
+
samples = _random_sampling(skopt_space, n_points)
|
|
91
|
+
|
|
92
|
+
elif method == "lhs":
|
|
93
|
+
samples = _lhs_sampling(skopt_space, n_points, lhs_criterion)
|
|
94
|
+
|
|
95
|
+
elif method == "sobol":
|
|
96
|
+
samples = _sobol_sampling(skopt_space, n_points)
|
|
97
|
+
|
|
98
|
+
elif method in ["halton", "hammersly"]:
|
|
99
|
+
samples = _hammersly_sampling(skopt_space, n_points)
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Unknown sampling method: {method}. "
|
|
104
|
+
f"Choose from: random, lhs, sobol, halton, hammersly"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Convert samples to list of dicts
|
|
108
|
+
variable_names = [v['name'] for v in search_space.variables]
|
|
109
|
+
points = []
|
|
110
|
+
|
|
111
|
+
for sample in samples:
|
|
112
|
+
point = {name: value for name, value in zip(variable_names, sample)}
|
|
113
|
+
points.append(point)
|
|
114
|
+
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Generated {len(points)} initial points using {method} method "
|
|
117
|
+
f"for {len(variable_names)} variables"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return points
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _random_sampling(skopt_space, n_points: int) -> list:
|
|
124
|
+
"""
|
|
125
|
+
Generate random samples respecting variable types.
|
|
126
|
+
|
|
127
|
+
Handles Real, Integer, and Categorical dimensions appropriately.
|
|
128
|
+
Returns list of lists to preserve mixed types.
|
|
129
|
+
"""
|
|
130
|
+
samples_list = []
|
|
131
|
+
|
|
132
|
+
for dim in skopt_space:
|
|
133
|
+
if isinstance(dim, Categorical):
|
|
134
|
+
# Random choice from categories
|
|
135
|
+
samples = np.random.choice(dim.categories, size=n_points)
|
|
136
|
+
|
|
137
|
+
elif isinstance(dim, Integer):
|
|
138
|
+
# Random integers in [low, high] (inclusive)
|
|
139
|
+
# np.random.randint is [low, high), so add 1 to include upper bound
|
|
140
|
+
samples = np.random.randint(dim.low, dim.high + 1, size=n_points)
|
|
141
|
+
|
|
142
|
+
elif isinstance(dim, Real):
|
|
143
|
+
# Random floats in [low, high]
|
|
144
|
+
samples = np.random.uniform(dim.low, dim.high, size=n_points)
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(f"Unknown dimension type: {type(dim)}")
|
|
148
|
+
|
|
149
|
+
samples_list.append(samples)
|
|
150
|
+
|
|
151
|
+
# Transpose to get list of samples (each sample is a list of values)
|
|
152
|
+
# Don't use column_stack as it converts everything to same dtype
|
|
153
|
+
samples = [[samples_list[j][i] for j in range(len(samples_list))]
|
|
154
|
+
for i in range(n_points)]
|
|
155
|
+
return samples
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _lhs_sampling(skopt_space, n_points: int, criterion: str = "maximin") -> list:
|
|
159
|
+
"""
|
|
160
|
+
Generate Latin Hypercube Sampling points.
|
|
161
|
+
|
|
162
|
+
LHS provides good space-filling properties and is generally recommended
|
|
163
|
+
for initial designs in Bayesian optimization.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
criterion: Optimization criterion
|
|
167
|
+
- "maximin": maximize minimum distance between points (default)
|
|
168
|
+
- "correlation": minimize correlations between dimensions
|
|
169
|
+
- "ratio": minimize ratio of max to min distance
|
|
170
|
+
"""
|
|
171
|
+
sampler = Lhs(lhs_type="classic", criterion=criterion)
|
|
172
|
+
samples = sampler.generate(skopt_space, n_points)
|
|
173
|
+
# skopt returns list of samples already
|
|
174
|
+
return samples
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _sobol_sampling(skopt_space, n_points: int) -> list:
|
|
178
|
+
"""
|
|
179
|
+
Generate Sobol quasi-random sequence points.
|
|
180
|
+
|
|
181
|
+
Sobol sequences have low discrepancy properties, meaning they cover
|
|
182
|
+
the space more uniformly than random sampling.
|
|
183
|
+
"""
|
|
184
|
+
sampler = Sobol()
|
|
185
|
+
samples = sampler.generate(skopt_space, n_points)
|
|
186
|
+
# skopt returns list of samples already
|
|
187
|
+
return samples
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _hammersly_sampling(skopt_space, n_points: int) -> list:
|
|
191
|
+
"""
|
|
192
|
+
Generate Hammersly sequence points.
|
|
193
|
+
|
|
194
|
+
Hammersly and Halton sequences are low-discrepancy sequences similar
|
|
195
|
+
to Sobol, providing good space coverage.
|
|
196
|
+
"""
|
|
197
|
+
sampler = Hammersly()
|
|
198
|
+
samples = sampler.generate(skopt_space, n_points)
|
|
199
|
+
# skopt returns list of samples already
|
|
200
|
+
return samples
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alchemist-nrel
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Active learning and optimization toolkit for chemical and materials research
|
|
5
|
+
Author-email: Caleb Coatney <caleb.coatney@nrel.gov>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/NREL/ALchemist
|
|
8
|
+
Project-URL: Documentation, https://nrel.github.io/ALchemist/
|
|
9
|
+
Project-URL: Source, https://github.com/NREL/ALchemist
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/NREL/ALchemist/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/NREL/ALchemist/releases
|
|
12
|
+
Keywords: active learning,bayesian optimization,gaussian processes,materials science,chemistry
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy
|
|
26
|
+
Requires-Dist: pandas
|
|
27
|
+
Requires-Dist: scipy
|
|
28
|
+
Requires-Dist: matplotlib
|
|
29
|
+
Requires-Dist: mplcursors
|
|
30
|
+
Requires-Dist: scikit-learn
|
|
31
|
+
Requires-Dist: scikit-optimize
|
|
32
|
+
Requires-Dist: botorch
|
|
33
|
+
Requires-Dist: torch
|
|
34
|
+
Requires-Dist: gpytorch
|
|
35
|
+
Requires-Dist: ax-platform
|
|
36
|
+
Requires-Dist: customtkinter
|
|
37
|
+
Requires-Dist: tksheet
|
|
38
|
+
Requires-Dist: tabulate
|
|
39
|
+
Requires-Dist: ctkmessagebox
|
|
40
|
+
Requires-Dist: joblib
|
|
41
|
+
Requires-Dist: fastapi>=0.109.0
|
|
42
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
43
|
+
Requires-Dist: pydantic>=2.5.0
|
|
44
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
45
|
+
Requires-Dist: requests
|
|
46
|
+
Provides-Extra: test
|
|
47
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
48
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
49
|
+
Requires-Dist: pytest-anyio>=0.0.0; extra == "test"
|
|
50
|
+
Requires-Dist: httpx>=0.25.0; extra == "test"
|
|
51
|
+
Requires-Dist: requests>=2.31.0; extra == "test"
|
|
52
|
+
Provides-Extra: dev
|
|
53
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
54
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
55
|
+
Requires-Dist: pytest-anyio>=0.0.0; extra == "dev"
|
|
56
|
+
Requires-Dist: httpx>=0.25.0; extra == "dev"
|
|
57
|
+
Requires-Dist: requests>=2.31.0; extra == "dev"
|
|
58
|
+
Dynamic: license-file
|
|
59
|
+
|
|
60
|
+
<img src="docs/assets/NEW_LOGO_LIGHT.png" alt="ALchemist" width="50%" />
|
|
61
|
+
|
|
62
|
+
**ALchemist: Active Learning Toolkit for Chemical and Materials Research**
|
|
63
|
+
|
|
64
|
+
ALchemist is a modular Python toolkit that brings active learning and Bayesian optimization to experimental design in chemical and materials research. It is designed for scientists and engineers who want to efficiently explore or optimize high-dimensional variable spaces—using intuitive graphical interfaces, programmatic APIs, or autonomous optimization workflows.
|
|
65
|
+
|
|
66
|
+
**NLR Software Record:** SWR-25-102
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
Full user guide and documentation:
|
|
73
|
+
[https://nrel.github.io/ALchemist/](https://nrel.github.io/ALchemist/)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Overview
|
|
78
|
+
|
|
79
|
+
**Key Features:**
|
|
80
|
+
|
|
81
|
+
- **Flexible variable space definition**: Real, integer, and categorical variables with bounds or discrete values
|
|
82
|
+
- **Probabilistic surrogate modeling**: Gaussian process regression via BoTorch or scikit-learn backends
|
|
83
|
+
- **Advanced acquisition strategies**: Efficient sampling using qEI, qPI, qUCB, and qNegIntegratedPosteriorVariance
|
|
84
|
+
- **Modern web interface**: React-based UI with FastAPI backend for seamless active learning workflows
|
|
85
|
+
- **Desktop GUI**: CustomTkinter desktop application for offline optimization
|
|
86
|
+
- **Session management**: Save/load optimization sessions with audit logs for reproducibility
|
|
87
|
+
- **Multiple interfaces**: No-code GUI, Python Session API, or REST API for different use cases
|
|
88
|
+
- **Autonomous optimization**: Human-out-of-the-loop operation for real-time process control
|
|
89
|
+
- **Experiment tracking**: CSV logging, reproducible random seeds, and comprehensive audit trails
|
|
90
|
+
- **Extensibility**: Abstract interfaces for models and acquisition functions enable future backend and workflow expansion
|
|
91
|
+
**Architecture:**
|
|
92
|
+
|
|
93
|
+
ALchemist is built on a clean, modular architecture:
|
|
94
|
+
|
|
95
|
+
- **Core Session API**: Headless Bayesian optimization engine (`alchemist_core`) that powers all interfaces
|
|
96
|
+
- **Desktop Application**: CustomTkinter GUI using the Core Session API, designed for human-in-the-loop and offline optimization
|
|
97
|
+
- **REST API**: FastAPI server providing a thin wrapper around the Core Session API for remote access
|
|
98
|
+
- **Web Application**: React UI consuming the REST API, supporting both interactive and autonomous optimization workflows
|
|
99
|
+
|
|
100
|
+
Session files (JSON format) are fully interoperable between desktop and web applications, enabling seamless workflow transitions.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Use Cases
|
|
105
|
+
|
|
106
|
+
- **Interactive Optimization**: Use desktop or web GUI for manual experiment design and human-in-the-loop optimization
|
|
107
|
+
- **Programmatic Workflows**: Import the Session API in Python scripts or Jupyter notebooks for batch processing
|
|
108
|
+
- **Autonomous Optimization**: Use the REST API to integrate ALchemist with automated laboratory equipment for real-time process control
|
|
109
|
+
- **Remote Monitoring**: Web dashboard provides read-only monitoring mode when ALchemist is being remote-controlled
|
|
110
|
+
|
|
111
|
+
For detailed application examples, see [Use Cases](https://nrel.github.io/ALchemist/use_cases/) in the documentation.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Installation
|
|
116
|
+
|
|
117
|
+
**Requirements:** Python 3.11 or higher
|
|
118
|
+
|
|
119
|
+
**Recommended (Optional):** We recommend using [Anaconda](https://www.anaconda.com/products/distribution) to manage Python environments:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
conda create -n alchemist-env python=3.11
|
|
123
|
+
conda activate alchemist-env
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Basic Installation:**
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install alchemist-nrel
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**From GitHub:**
|
|
133
|
+
> *Note: This installs the latest unreleased version. The web application is not pre-built with this method because static build files are not included in the repository.*
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install git+https://github.com/NREL/ALchemist.git
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For advanced installation options, Docker deployment, and development setup, see the [Advanced Installation Guide](https://nrel.github.io/ALchemist/#advanced-installation) in the documentation.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Running ALchemist
|
|
144
|
+
|
|
145
|
+
**Web Application:**
|
|
146
|
+
```bash
|
|
147
|
+
alchemist-web
|
|
148
|
+
```
|
|
149
|
+
Opens at [http://localhost:8000/app](http://localhost:8000/app)
|
|
150
|
+
|
|
151
|
+
**Desktop Application:**
|
|
152
|
+
```bash
|
|
153
|
+
alchemist
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For detailed usage instructions, see [Getting Started](https://nrel.github.io/ALchemist/) in the documentation.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Development Status
|
|
161
|
+
|
|
162
|
+
ALchemist is under active development at NLR as part of the DataHub project within the ChemCatBio consortium.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Issues & Troubleshooting
|
|
167
|
+
|
|
168
|
+
If you encounter any issues or have questions, please [open an issue on GitHub](https://github.com/NREL/ALchemist/issues) or contact ccoatney@nrel.gov.
|
|
169
|
+
|
|
170
|
+
For the latest known issues and troubleshooting tips, see the [Issues & Troubleshooting Log](docs/ISSUES_LOG.md).
|
|
171
|
+
|
|
172
|
+
We appreciate your feedback and bug reports to help improve ALchemist!
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
This project is licensed under the BSD 3-Clause License. See the [LICENSE](LICENSE) file for details.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Repository
|
|
183
|
+
|
|
184
|
+
[https://github.com/NREL/ALchemist](https://github.com/NREL/ALchemist)
|
|
185
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
main.py,sha256=3sAO2QZxxibs4WRT82i2w6KVBBFmYEMNUoGiMYFowOw,126
|
|
2
|
+
alchemist_core/__init__.py,sha256=jYIygJyhCXUmX3oAaxw687uLLQcRSuxNRQrMeuWuuuI,2023
|
|
3
|
+
alchemist_core/audit_log.py,sha256=s8h3YKBgvcu_tgIrjP69rNr6yOnbks5J2RR_m2bwB4Q,22531
|
|
4
|
+
alchemist_core/config.py,sha256=Sk5eM1okktO5bUMlMPv9yzF2fpuiyGr9LUtlCWIBDc8,3366
|
|
5
|
+
alchemist_core/events.py,sha256=ty9nRzfZGHzk6b09dALIwrMY_5PYSv0wMaw94JLDjSk,6717
|
|
6
|
+
alchemist_core/session.py,sha256=OraGE2y78s9cbhfpxr4jllfet-GclCfTj0yV1D9S_2M,55330
|
|
7
|
+
alchemist_core/acquisition/__init__.py,sha256=3CYGI24OTBS66ETrlGFyHCNpfS6DBMP41MZDhvjFEzg,32
|
|
8
|
+
alchemist_core/acquisition/base_acquisition.py,sha256=s51vGx0b0Nt91lSCiVwYP9IClugVg2VJ21dn2n_4LIs,483
|
|
9
|
+
alchemist_core/acquisition/botorch_acquisition.py,sha256=r2Z510UIrPZCS-uXWZzgtaNhmCUh0CsTrAJQCWdevxA,31401
|
|
10
|
+
alchemist_core/acquisition/skopt_acquisition.py,sha256=YRdANqgiN3GWd4sn16oruN6jVnI4RLmvLhBMUfYyLp4,13115
|
|
11
|
+
alchemist_core/data/__init__.py,sha256=wgEb03x0RzVCi0uJXOzEKXkbA2oNHom5EgSB3tKgl1E,256
|
|
12
|
+
alchemist_core/data/experiment_manager.py,sha256=LujWfFRlOSP7rok8sxTc6NTjetN7u7RnsOo5W2I5o-w,9013
|
|
13
|
+
alchemist_core/data/search_space.py,sha256=oA9YEF3JRWpRklHzSo_Uxlmfy7bHwZfLZFDf4_nl4ew,6230
|
|
14
|
+
alchemist_core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
alchemist_core/models/base_model.py,sha256=gIpC2eoTZcp4ozI0Rctcxt_4bLhgaE6ALYCzvuIrMZw,3145
|
|
16
|
+
alchemist_core/models/botorch_model.py,sha256=VbZjggt8PszTJeG0x1A1vGGM1kT95Bjset3HWT6PQEM,42263
|
|
17
|
+
alchemist_core/models/sklearn_model.py,sha256=XEr2yAQtVQd3beGAoQnuNmt_inzEsKAwpGo-Gfdp2vY,38059
|
|
18
|
+
alchemist_core/utils/__init__.py,sha256=oQsvUqukRng8GgiZSPMM-xmB-Lv46XveJzYQr2MdkOc,99
|
|
19
|
+
alchemist_core/utils/doe.py,sha256=hnrhIzm3Nz0qZBW4PXaNQ6pTDgiLn_lUhbMPmWmk_6Y,7110
|
|
20
|
+
alchemist_nrel-0.3.1.dist-info/licenses/LICENSE,sha256=wdIWWEj59ztfQViDuT_9wG3L1K8afUpRSygimXw36wY,1511
|
|
21
|
+
api/__init__.py,sha256=ODc6pq4OSImgK4xvhX_zMhqUjIc7JvLfqxKF_-Ubw7g,49
|
|
22
|
+
api/dependencies.py,sha256=sF1YYjnFRaw7nj7Y7URKLF2Ek-EfaXqjOyV1Zbktz2g,1090
|
|
23
|
+
api/example_client.py,sha256=RjEOvZItzgmGdP6V5j106LQqmQg0WIEr72xf4oMJZHo,6924
|
|
24
|
+
api/main.py,sha256=Fee_u_dvNcLzNE-kRhoinfEYnIf5e-i71ZwdNOHu7hY,4301
|
|
25
|
+
api/run_api.py,sha256=YGyLUJNbdsDGEKsd_EsrA1gGheGW53etn8F3ku44q1g,1993
|
|
26
|
+
api/middleware/__init__.py,sha256=WM4JEg3DibymvEvQ6hx_FJkv8lKLjHD48qNouSORGxA,313
|
|
27
|
+
api/middleware/error_handlers.py,sha256=k5hNo6W5QDjGRHUY8Le-t7ubWkwSZBFpsTKcEF0nweI,4545
|
|
28
|
+
api/models/__init__.py,sha256=YFtp8989mH9Zjzvd8W6pXklQJXTf8zWj4I2YWnLegDQ,1204
|
|
29
|
+
api/models/requests.py,sha256=SvAL36-WcuaibKkI2hsNPDEpptZHw88_Oj961dD37vE,10853
|
|
30
|
+
api/models/responses.py,sha256=58oWYfKcQpUpa3oF_VY-QfNTaLxGu_O1cgBHB-Banjs,13704
|
|
31
|
+
api/routers/__init__.py,sha256=Mhg62NdA6iaPy-L5HLVp_dd9aUHmJ72KtMSRyO2kusA,180
|
|
32
|
+
api/routers/acquisition.py,sha256=GzncAXsQzhldB-gngpMcUa5choaoAqMqLrpqszRjMFc,6683
|
|
33
|
+
api/routers/experiments.py,sha256=h4UI96MwSVL5VmibI-ebdsOl0f332edcifmdNL8OVFg,10329
|
|
34
|
+
api/routers/models.py,sha256=32Ln0MtlnCEjfN3Q6Io_EBwwwGoJXb73UbQEMIcVGjI,3651
|
|
35
|
+
api/routers/sessions.py,sha256=3h9veIWxx4F1bP_YgWwOCtuKIDe2e8VcaBCjQxmZT7c,19073
|
|
36
|
+
api/routers/variables.py,sha256=TiByX1ITabBDdTSMGAPa1lGd0LBipNgDfmPsbvTEAdE,10108
|
|
37
|
+
api/routers/visualizations.py,sha256=QPe1PkgGtzb4Oe4YYgFoi7J1oHJGS5pa1g75KKBDqUM,24180
|
|
38
|
+
api/routers/websocket.py,sha256=AnENOTSk8LOZ5XD_5O29srOfdQ8U136Yzw6dwMca9t4,4361
|
|
39
|
+
api/services/__init__.py,sha256=0jw0tkL-8CtChv5ytdXRFeIz1OTVz7Vw1UaaDo86PQs,106
|
|
40
|
+
api/services/session_store.py,sha256=BIGJUAiOEQ3uH40wUXuzqd7djy60SE6PQDKVBquJS9Y,18989
|
|
41
|
+
api/static/NEW_ICON.ico,sha256=V4zY86qhPT24SSYK8VL5Ax5AezWxOfvfeHWBXisudOU,247870
|
|
42
|
+
api/static/NEW_ICON.png,sha256=7UUPRgQ6-Ncv1xvB_57QMfrMY8xxHn16mLHc_zUGmCE,62788
|
|
43
|
+
api/static/NEW_LOGO_DARK.png,sha256=O4p2tfTBuChSSPRl-Fzue1qoQdLkqXHBofNyiEzRNLs,128146
|
|
44
|
+
api/static/NEW_LOGO_LIGHT.png,sha256=XBcv5-snGDTpjCrk7UfuJiFbSqrsGRafk7vNBj9eJnM,131686
|
|
45
|
+
api/static/index.html,sha256=ttWE08A9TQ0ai63juHHo0SvcPWczYg4ANob2GB4ZQ5Q,499
|
|
46
|
+
api/static/vite.svg,sha256=SnSK_UQ5GLsWWRyDTEAdrjPoeGGrXbrQgRw6O0qSFPs,1497
|
|
47
|
+
api/static/assets/api-vcoXEqyq.js,sha256=LDSOiSvi1Zc7SRry3ldpA__egc6GAFbzQt9QzBzbTIQ,303
|
|
48
|
+
api/static/assets/index-DWfIKU9j.js,sha256=raGrwAEmx2gqVz8E7HlQ8KiqOFPYOQuHpKI4rqYdydw,5745850
|
|
49
|
+
api/static/assets/index-sMIa_1hV.css,sha256=1-xNuB1JVE5hK0YzdpYb6ys1dMNJMDgx6IMv712yryI,20007
|
|
50
|
+
ui/__init__.py,sha256=H4kWlVey7KKf3iPQi74zuM7FSOg5Gh-ii3UwSTuIp8A,1203
|
|
51
|
+
ui/acquisition_panel.py,sha256=zF-mQDrs-Y7sf2GXYF-bPlO9UXZMTzYRMDN-Wn5FyWw,39647
|
|
52
|
+
ui/custom_widgets.py,sha256=UXNv4DiTw3tFC0VaN1Qtcf_-9umX34uDn46-cEA6cs0,3812
|
|
53
|
+
ui/experiment_logger.py,sha256=dP3IGaQ31sURyz7awd_VrZBWaKLH2xXEeRalWZpvVcQ,8366
|
|
54
|
+
ui/gpr_panel.py,sha256=rQYCKZr8UlXVL3DVXTq4ArlIodzvDOtZXkYrdotERtw,26465
|
|
55
|
+
ui/notifications.py,sha256=hpUDo52_cQd7e8j7lOZikVRu1yLqRwlNR9vYNZdF5VM,35301
|
|
56
|
+
ui/pool_viz.py,sha256=RwjggEfRSSEe-4nGjxc-I-1e5_aH64DgypT_YoubLIU,8765
|
|
57
|
+
ui/ui.py,sha256=Lvg2H0_LrsrxJYFYFHr2LQsnbJWW5Q2fYqk63qVUJv4,101727
|
|
58
|
+
ui/ui_utils.py,sha256=yud2-9LvT4XBcjTyfwUX5tYGNZRlAUVlu2YpcNY1HKA,658
|
|
59
|
+
ui/utils.py,sha256=m19YFFkEUAY46YSj6S5RBmfUFjIWOk7F8CB4oKDRRZw,1078
|
|
60
|
+
ui/variables_setup.py,sha256=6hphCy66uLsjIX7FjFtY6-fBfZ6cgfpviXXX9JBhuc4,23618
|
|
61
|
+
ui/visualizations.py,sha256=FCpuehMi2Cf3Jpuycqoj43oJoCU-QAr8-6Sp6LCO4hE,70371
|
|
62
|
+
alchemist_nrel-0.3.1.dist-info/METADATA,sha256=SxYr-5UGvB6fL_O_OvopkBMkM4QsBSWBhi_t88t0nFw,7122
|
|
63
|
+
alchemist_nrel-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
64
|
+
alchemist_nrel-0.3.1.dist-info/entry_points.txt,sha256=e2QcTxh-pidX_eJlQRk9yE3hLMYA0YnUzSh3sxqvdzY,73
|
|
65
|
+
alchemist_nrel-0.3.1.dist-info/top_level.txt,sha256=dwh-oxj7H6oAGYchcUDyfiu9UxxzCn4hUDx1oeM2k-8,27
|
|
66
|
+
alchemist_nrel-0.3.1.dist-info/RECORD,,
|
api/example_client.py
CHANGED
|
@@ -90,8 +90,13 @@ def main():
|
|
|
90
90
|
json={"experiments": experiments}
|
|
91
91
|
)
|
|
92
92
|
response.raise_for_status()
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
batch_result = response.json()
|
|
94
|
+
n_added = batch_result.get("n_added", len(experiments))
|
|
95
|
+
total_experiments = batch_result.get("n_experiments")
|
|
96
|
+
if total_experiments is not None and total_experiments >= n_added:
|
|
97
|
+
print(f" ✓ Added {n_added} experiments (total: {total_experiments})")
|
|
98
|
+
else:
|
|
99
|
+
print(f" ✓ Added {n_added} experiments")
|
|
95
100
|
|
|
96
101
|
# Get data summary
|
|
97
102
|
response = requests.get(f"{BASE_URL}/sessions/{session_id}/experiments/summary")
|
api/main.py
CHANGED
|
@@ -16,7 +16,7 @@ from fastapi import FastAPI
|
|
|
16
16
|
from fastapi.middleware.cors import CORSMiddleware
|
|
17
17
|
from fastapi.staticfiles import StaticFiles
|
|
18
18
|
from fastapi.responses import FileResponse
|
|
19
|
-
from .routers import sessions, variables, experiments, models, acquisition, visualizations
|
|
19
|
+
from .routers import sessions, variables, experiments, models, acquisition, visualizations, websocket
|
|
20
20
|
from .middleware.error_handlers import add_exception_handlers
|
|
21
21
|
import logging
|
|
22
22
|
|
|
@@ -32,14 +32,19 @@ logger = logging.getLogger(__name__)
|
|
|
32
32
|
app = FastAPI(
|
|
33
33
|
title="ALchemist API",
|
|
34
34
|
description="REST API for Bayesian optimization and active learning",
|
|
35
|
-
version="0.
|
|
35
|
+
version="0.3.0",
|
|
36
36
|
docs_url="/api/docs",
|
|
37
37
|
redoc_url="/api/redoc",
|
|
38
38
|
openapi_url="/api/openapi.json"
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
# CORS configuration - allows frontend in both dev and production
|
|
42
|
-
|
|
42
|
+
# Default origins include dev servers and common production patterns
|
|
43
|
+
# Override with ALLOWED_ORIGINS environment variable for specific deployments
|
|
44
|
+
ALLOWED_ORIGINS = os.getenv(
|
|
45
|
+
"ALLOWED_ORIGINS",
|
|
46
|
+
"http://localhost:3000,http://localhost:5173,http://localhost:5174,http://localhost:8000,http://127.0.0.1:8000"
|
|
47
|
+
).split(",")
|
|
43
48
|
|
|
44
49
|
app.add_middleware(
|
|
45
50
|
CORSMiddleware,
|
|
@@ -59,6 +64,7 @@ app.include_router(experiments.router, prefix="/api/v1/sessions", tags=["Experim
|
|
|
59
64
|
app.include_router(models.router, prefix="/api/v1/sessions", tags=["Models"])
|
|
60
65
|
app.include_router(acquisition.router, prefix="/api/v1/sessions", tags=["Acquisition"])
|
|
61
66
|
app.include_router(visualizations.router, prefix="/api/v1/sessions", tags=["Visualizations"])
|
|
67
|
+
app.include_router(websocket.router, prefix="/api/v1", tags=["WebSocket"])
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
@app.get("/")
|
|
@@ -82,8 +88,17 @@ async def health_check():
|
|
|
82
88
|
|
|
83
89
|
|
|
84
90
|
# Mount static files for production (if they exist)
|
|
85
|
-
|
|
91
|
+
# Priority order:
|
|
92
|
+
# 1. api/static/ - Production (pip installed or built package)
|
|
93
|
+
# 2. alchemist-web/dist/ - Development (after manual npm run build)
|
|
94
|
+
api_static_dir = Path(__file__).parent / "static"
|
|
95
|
+
dev_static_dir = Path(__file__).parent.parent / "alchemist-web" / "dist"
|
|
96
|
+
|
|
97
|
+
# Use api/static if it exists (production), otherwise fall back to dev build
|
|
98
|
+
static_dir = api_static_dir if api_static_dir.exists() else dev_static_dir
|
|
99
|
+
|
|
86
100
|
if static_dir.exists():
|
|
101
|
+
logger.info(f"Serving static files from: {static_dir}")
|
|
87
102
|
app.mount("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets")
|
|
88
103
|
|
|
89
104
|
@app.get("/{full_path:path}")
|
|
@@ -104,6 +119,8 @@ if static_dir.exists():
|
|
|
104
119
|
return FileResponse(index_path)
|
|
105
120
|
|
|
106
121
|
return {"detail": "Not Found"}
|
|
122
|
+
else:
|
|
123
|
+
logger.warning("Static files not found. Web UI will not be available. Run 'npm run build' in alchemist-web/ or install from built wheel.")
|
|
107
124
|
|
|
108
125
|
|
|
109
126
|
if __name__ == "__main__":
|
api/models/requests.py
CHANGED
|
@@ -93,13 +93,17 @@ class AddExperimentRequest(BaseModel):
|
|
|
93
93
|
inputs: Dict[str, Union[float, int, str]] = Field(..., description="Variable values")
|
|
94
94
|
output: Optional[float] = Field(None, description="Target/output value")
|
|
95
95
|
noise: Optional[float] = Field(None, description="Measurement uncertainty")
|
|
96
|
+
iteration: Optional[int] = Field(None, description="Iteration number (auto-assigned if None)")
|
|
97
|
+
reason: Optional[str] = Field(None, description="Reason for this experiment")
|
|
96
98
|
|
|
97
99
|
model_config = ConfigDict(
|
|
98
100
|
json_schema_extra={
|
|
99
101
|
"example": {
|
|
100
102
|
"inputs": {"temperature": 350, "catalyst": "A"},
|
|
101
103
|
"output": 0.85,
|
|
102
|
-
"noise": 0.02
|
|
104
|
+
"noise": 0.02,
|
|
105
|
+
"iteration": 1,
|
|
106
|
+
"reason": "Initial Design"
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
)
|
|
@@ -182,6 +186,36 @@ class FindOptimumRequest(BaseModel):
|
|
|
182
186
|
)
|
|
183
187
|
|
|
184
188
|
|
|
189
|
+
# ============================================================
|
|
190
|
+
# Initial Design (DoE) Models
|
|
191
|
+
# ============================================================
|
|
192
|
+
|
|
193
|
+
class InitialDesignRequest(BaseModel):
|
|
194
|
+
"""Request for generating initial experimental design."""
|
|
195
|
+
method: Literal["random", "lhs", "sobol", "halton", "hammersly"] = Field(
|
|
196
|
+
default="lhs",
|
|
197
|
+
description="Sampling method"
|
|
198
|
+
)
|
|
199
|
+
n_points: int = Field(default=10, ge=1, le=1000, description="Number of points to generate")
|
|
200
|
+
random_seed: Optional[int] = Field(None, description="Random seed for reproducibility")
|
|
201
|
+
lhs_criterion: str = Field(
|
|
202
|
+
default="maximin",
|
|
203
|
+
pattern="^(maximin|correlation|ratio)$",
|
|
204
|
+
description="Criterion for LHS method"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
model_config = ConfigDict(
|
|
208
|
+
json_schema_extra={
|
|
209
|
+
"example": {
|
|
210
|
+
"method": "lhs",
|
|
211
|
+
"n_points": 10,
|
|
212
|
+
"random_seed": 42,
|
|
213
|
+
"lhs_criterion": "maximin"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
185
219
|
# ============================================================
|
|
186
220
|
# Prediction Models
|
|
187
221
|
# ============================================================
|
|
@@ -200,3 +234,63 @@ class PredictionRequest(BaseModel):
|
|
|
200
234
|
}
|
|
201
235
|
}
|
|
202
236
|
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ============================================================
|
|
240
|
+
# Audit Log & Session Management Models
|
|
241
|
+
# ============================================================
|
|
242
|
+
|
|
243
|
+
class UpdateMetadataRequest(BaseModel):
|
|
244
|
+
"""Request to update session metadata."""
|
|
245
|
+
name: Optional[str] = Field(None, description="Session name")
|
|
246
|
+
description: Optional[str] = Field(None, description="Session description")
|
|
247
|
+
tags: Optional[List[str]] = Field(None, description="Session tags")
|
|
248
|
+
|
|
249
|
+
model_config = ConfigDict(
|
|
250
|
+
json_schema_extra={
|
|
251
|
+
"example": {
|
|
252
|
+
"name": "Catalyst_Screening_Nov2025",
|
|
253
|
+
"description": "Pt/Pd ratio optimization for CO2 reduction",
|
|
254
|
+
"tags": ["catalyst", "CO2", "electrochemistry"]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class LockDecisionRequest(BaseModel):
|
|
261
|
+
"""Request to lock in a decision to the audit log."""
|
|
262
|
+
lock_type: Literal["data", "model", "acquisition"] = Field(..., description="Type of decision to lock")
|
|
263
|
+
notes: Optional[str] = Field(None, description="Optional notes about this decision")
|
|
264
|
+
|
|
265
|
+
# For acquisition lock
|
|
266
|
+
strategy: Optional[str] = Field(None, description="Acquisition strategy (required for acquisition lock)")
|
|
267
|
+
parameters: Optional[Dict[str, Any]] = Field(None, description="Acquisition parameters (required for acquisition lock)")
|
|
268
|
+
suggestions: Optional[List[Dict[str, Any]]] = Field(None, description="Suggested experiments (required for acquisition lock)")
|
|
269
|
+
|
|
270
|
+
model_config = ConfigDict(
|
|
271
|
+
json_schema_extra={
|
|
272
|
+
"example": {
|
|
273
|
+
"lock_type": "model",
|
|
274
|
+
"notes": "Best cross-validation performance: R²=0.93"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ============================================================
|
|
281
|
+
# Session Lock Models
|
|
282
|
+
# ============================================================
|
|
283
|
+
|
|
284
|
+
class SessionLockRequest(BaseModel):
|
|
285
|
+
"""Request to lock a session for programmatic control."""
|
|
286
|
+
locked_by: str = Field(..., description="Identifier of the client locking the session")
|
|
287
|
+
client_id: Optional[str] = Field(None, description="Optional unique client identifier")
|
|
288
|
+
|
|
289
|
+
model_config = ConfigDict(
|
|
290
|
+
json_schema_extra={
|
|
291
|
+
"example": {
|
|
292
|
+
"locked_by": "Reactor Controller v1.2",
|
|
293
|
+
"client_id": "lab-3-workstation"
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
)
|