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
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alchemist-nrel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Active learning and optimization toolkit for chemical and materials research
|
|
5
5
|
Author-email: Caleb Coatney <caleb.coatney@nrel.gov>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -42,6 +42,7 @@ Requires-Dist: fastapi>=0.109.0
|
|
|
42
42
|
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
43
43
|
Requires-Dist: pydantic>=2.5.0
|
|
44
44
|
Requires-Dist: python-multipart>=0.0.6
|
|
45
|
+
Requires-Dist: requests
|
|
45
46
|
Provides-Extra: test
|
|
46
47
|
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
47
48
|
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
@@ -56,7 +57,7 @@ Requires-Dist: httpx>=0.25.0; extra == "dev"
|
|
|
56
57
|
Requires-Dist: requests>=2.31.0; extra == "dev"
|
|
57
58
|
Dynamic: license-file
|
|
58
59
|
|
|
59
|
-
<img src="docs/assets/
|
|
60
|
+
<img src="docs/assets/NEW_LOGO_LIGHT.png" alt="ALchemist" width="50%" />
|
|
60
61
|
|
|
61
62
|
**ALchemist: Active Learning Toolkit for Chemical and Materials Research**
|
|
62
63
|
|
|
@@ -81,75 +82,91 @@ ALchemist accelerates discovery and optimization by combining:
|
|
|
81
82
|
- **Probabilistic surrogate modeling:** Gaussian process regression via BoTorch or scikit-optimize backends.
|
|
82
83
|
- **Advanced acquisition strategies:** Efficient sampling using qEI, qPI, qUCB, and qNegIntegratedPosteriorVariance.
|
|
83
84
|
- **Modern web interface:** React-based UI with FastAPI backend for seamless active learning workflows.
|
|
85
|
+
- **Autonomous optimization:** Human-out-of-the-loop optimization for real-time process control.
|
|
84
86
|
- **Experiment tracking:** CSV logging, reproducible random seeds, and error tracking.
|
|
85
87
|
- **Extensibility:** Abstract interfaces for models and acquisition functions enable future backend and workflow expansion.
|
|
86
88
|
|
|
89
|
+
### Use Cases
|
|
90
|
+
|
|
91
|
+
- **Interactive Optimization**: Desktop GUI or web UI for manual experiment design
|
|
92
|
+
- **Programmatic Workflows**: Python Session API for scripts and notebooks
|
|
93
|
+
- **Autonomous Operation**: REST API for real-time process control (reactors, synthesis, etc.)
|
|
94
|
+
- **Remote Collaboration**: Web-based interface accessible from any device
|
|
95
|
+
|
|
87
96
|
---
|
|
88
97
|
|
|
89
98
|
## 🧭 Quick Start
|
|
90
99
|
|
|
91
|
-
###
|
|
100
|
+
### Installation
|
|
92
101
|
|
|
93
|
-
**
|
|
94
|
-
```bash
|
|
95
|
-
# Option 1: Manual start
|
|
96
|
-
python run_api.py # Terminal 1: Backend (port 8000)
|
|
97
|
-
cd alchemist-web && npm run dev # Terminal 2: Frontend (port 5173)
|
|
102
|
+
**Requirements:** Python 3.11 or higher
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
scripts\dev_start.bat # Windows
|
|
101
|
-
./scripts/dev_start.sh # Linux/Mac
|
|
102
|
-
```
|
|
104
|
+
We recommend using [Anaconda](https://www.anaconda.com/products/distribution) to manage your Python environments.
|
|
103
105
|
|
|
104
|
-
**
|
|
106
|
+
**1. Create a new environment:**
|
|
105
107
|
```bash
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
conda create -n alchemist-env python=3.11
|
|
109
|
+
conda activate alchemist-env
|
|
110
|
+
```
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
python run_api.py --production
|
|
112
|
+
**2. Install ALchemist:**
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
*Option A: From PyPI (recommended):*
|
|
115
|
+
```bash
|
|
116
|
+
pip install alchemist-nrel
|
|
114
117
|
```
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
*Option B: From GitHub:*
|
|
117
120
|
```bash
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
pip install git+https://github.com/NREL/ALchemist.git
|
|
122
|
+
```
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
*Option C: Development install (for contributors):*
|
|
125
|
+
```bash
|
|
126
|
+
git clone https://github.com/NREL/ALchemist.git
|
|
127
|
+
cd ALchemist
|
|
128
|
+
pip install -e .
|
|
124
129
|
```
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
All dependencies are specified in `pyproject.toml` and will be installed automatically.
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
**Note:** The web UI is pre-built and included in the package. You do **not** need Node.js/npm to use ALchemist unless you're developing the frontend.
|
|
129
134
|
|
|
130
|
-
|
|
135
|
+
### Running ALchemist
|
|
131
136
|
|
|
132
|
-
**
|
|
137
|
+
**Web Application (Recommended):**
|
|
133
138
|
```bash
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
alchemist-web
|
|
140
|
+
# Opens at http://localhost:8000
|
|
136
141
|
```
|
|
137
142
|
|
|
138
|
-
**
|
|
139
|
-
|
|
140
|
-
*Option A: Install directly from GitHub:*
|
|
143
|
+
**Desktop Application:**
|
|
141
144
|
```bash
|
|
142
|
-
|
|
145
|
+
alchemist
|
|
146
|
+
# Launches CustomTkinter GUI
|
|
143
147
|
```
|
|
144
148
|
|
|
145
|
-
|
|
149
|
+
**Development Mode (Frontend Developers):**
|
|
146
150
|
```bash
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
# Terminal 1: Backend with hot-reload
|
|
152
|
+
python run_api.py
|
|
153
|
+
|
|
154
|
+
# Terminal 2: Frontend with hot-reload
|
|
155
|
+
cd alchemist-web
|
|
156
|
+
npm install # First time only
|
|
157
|
+
npm run dev
|
|
158
|
+
# Opens at http://localhost:5173
|
|
150
159
|
```
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
**Docker Deployment:**
|
|
162
|
+
```bash
|
|
163
|
+
docker pull ghcr.io/nrel/alchemist:latest
|
|
164
|
+
docker run -p 8000:8000 ghcr.io/nrel/alchemist:latest
|
|
165
|
+
|
|
166
|
+
# Or build from source:
|
|
167
|
+
cd docker
|
|
168
|
+
docker-compose up --build
|
|
169
|
+
```
|
|
153
170
|
|
|
154
171
|
For step-by-step instructions, see the [Getting Started](https://nrel.github.io/ALchemist/) section of the documentation.
|
|
155
172
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
main.py,sha256=3sAO2QZxxibs4WRT82i2w6KVBBFmYEMNUoGiMYFowOw,126
|
|
2
|
+
run_api.py,sha256=-oxrFnDztyI0rfvZv0-QTBAbgW3OcCArdqg3c5l18qs,1849
|
|
3
|
+
alchemist_core/__init__.py,sha256=jYIygJyhCXUmX3oAaxw687uLLQcRSuxNRQrMeuWuuuI,2023
|
|
4
|
+
alchemist_core/audit_log.py,sha256=s8h3YKBgvcu_tgIrjP69rNr6yOnbks5J2RR_m2bwB4Q,22531
|
|
5
|
+
alchemist_core/config.py,sha256=Sk5eM1okktO5bUMlMPv9yzF2fpuiyGr9LUtlCWIBDc8,3366
|
|
6
|
+
alchemist_core/events.py,sha256=ty9nRzfZGHzk6b09dALIwrMY_5PYSv0wMaw94JLDjSk,6717
|
|
7
|
+
alchemist_core/session.py,sha256=UCjMEWTWPY9ywsuS1meWGF9hqVse7DaFFnhr2oW9qwc,48228
|
|
8
|
+
alchemist_core/acquisition/__init__.py,sha256=3CYGI24OTBS66ETrlGFyHCNpfS6DBMP41MZDhvjFEzg,32
|
|
9
|
+
alchemist_core/acquisition/base_acquisition.py,sha256=s51vGx0b0Nt91lSCiVwYP9IClugVg2VJ21dn2n_4LIs,483
|
|
10
|
+
alchemist_core/acquisition/botorch_acquisition.py,sha256=dGzXY7XMbrRzOIFC4UoA6mMYEDplKgb58ym_TzmiMss,31332
|
|
11
|
+
alchemist_core/acquisition/skopt_acquisition.py,sha256=YRdANqgiN3GWd4sn16oruN6jVnI4RLmvLhBMUfYyLp4,13115
|
|
12
|
+
alchemist_core/data/__init__.py,sha256=wgEb03x0RzVCi0uJXOzEKXkbA2oNHom5EgSB3tKgl1E,256
|
|
13
|
+
alchemist_core/data/experiment_manager.py,sha256=gyccrEq9ddTdKZ4zmCfGXB2bK7zZaCjL9a0FzZ9-eoY,8723
|
|
14
|
+
alchemist_core/data/search_space.py,sha256=oA9YEF3JRWpRklHzSo_Uxlmfy7bHwZfLZFDf4_nl4ew,6230
|
|
15
|
+
alchemist_core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
alchemist_core/models/ax_model.py,sha256=Fnu19Et376WxLXpApZSalIbWHQE9OQnMk41_PW69XnQ,6040
|
|
17
|
+
alchemist_core/models/base_model.py,sha256=gIpC2eoTZcp4ozI0Rctcxt_4bLhgaE6ALYCzvuIrMZw,3145
|
|
18
|
+
alchemist_core/models/botorch_model.py,sha256=VbZjggt8PszTJeG0x1A1vGGM1kT95Bjset3HWT6PQEM,42263
|
|
19
|
+
alchemist_core/models/sklearn_model.py,sha256=iCwZ-ZWZuYgFpRQDoWwaIBBWz9dPdf-6wNGPdUHM9cU,36698
|
|
20
|
+
alchemist_core/utils/__init__.py,sha256=oQsvUqukRng8GgiZSPMM-xmB-Lv46XveJzYQr2MdkOc,99
|
|
21
|
+
alchemist_core/utils/doe.py,sha256=hnrhIzm3Nz0qZBW4PXaNQ6pTDgiLn_lUhbMPmWmk_6Y,7110
|
|
22
|
+
alchemist_nrel-0.3.0.dist-info/licenses/LICENSE,sha256=wdIWWEj59ztfQViDuT_9wG3L1K8afUpRSygimXw36wY,1511
|
|
23
|
+
api/__init__.py,sha256=ODc6pq4OSImgK4xvhX_zMhqUjIc7JvLfqxKF_-Ubw7g,49
|
|
24
|
+
api/dependencies.py,sha256=sF1YYjnFRaw7nj7Y7URKLF2Ek-EfaXqjOyV1Zbktz2g,1090
|
|
25
|
+
api/example_client.py,sha256=aZMNuJmt00hpNEkijn5RnVEV7ZPfKzwQxucSasTrrdw,6645
|
|
26
|
+
api/main.py,sha256=OHs00UruREujnNT2h1lXtK_ZiNz14lwYH67kqH8o78o,4215
|
|
27
|
+
api/middleware/__init__.py,sha256=WM4JEg3DibymvEvQ6hx_FJkv8lKLjHD48qNouSORGxA,313
|
|
28
|
+
api/middleware/error_handlers.py,sha256=k5hNo6W5QDjGRHUY8Le-t7ubWkwSZBFpsTKcEF0nweI,4545
|
|
29
|
+
api/models/__init__.py,sha256=YFtp8989mH9Zjzvd8W6pXklQJXTf8zWj4I2YWnLegDQ,1204
|
|
30
|
+
api/models/requests.py,sha256=Hez7Pnk1Mm7hjsbsG1Kv7pX7imsIkqovroGRmUaIL38,9938
|
|
31
|
+
api/models/responses.py,sha256=5jzoIm7HiLWJPJ2U_iL3ACeixWdIuFwjMnhVNE9vCRE,12789
|
|
32
|
+
api/routers/__init__.py,sha256=Mhg62NdA6iaPy-L5HLVp_dd9aUHmJ72KtMSRyO2kusA,180
|
|
33
|
+
api/routers/acquisition.py,sha256=7c9ifuCFeDj8osWRHgCjAwbXjhd3iEBlVLW23FOAZXI,5710
|
|
34
|
+
api/routers/experiments.py,sha256=q7mgGmzHqe9wr9njSbUw5gmWt2kGoSv86-qbSakSNe0,9467
|
|
35
|
+
api/routers/models.py,sha256=32Ln0MtlnCEjfN3Q6Io_EBwwwGoJXb73UbQEMIcVGjI,3651
|
|
36
|
+
api/routers/sessions.py,sha256=5yvNNgQzZadEq6MU-rjV1DFgtH6awyxYne5UTVZ7NCU,15541
|
|
37
|
+
api/routers/variables.py,sha256=TiByX1ITabBDdTSMGAPa1lGd0LBipNgDfmPsbvTEAdE,10108
|
|
38
|
+
api/routers/visualizations.py,sha256=QPe1PkgGtzb4Oe4YYgFoi7J1oHJGS5pa1g75KKBDqUM,24180
|
|
39
|
+
api/services/__init__.py,sha256=0jw0tkL-8CtChv5ytdXRFeIz1OTVz7Vw1UaaDo86PQs,106
|
|
40
|
+
api/services/session_store.py,sha256=bVzRaf4YFoNKkXuSmzs76F1qaIl_6lUUI3KZJJgVc3U,16255
|
|
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=P9QXmIzDTIx7ybgXS7xZXUpvYsmc19j2zaXVFgIJQjQ,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-C0_glioA.js,sha256=PDlXBE-WtR6W_pMcuQepY5R62Ih_mQC6GLf0FuszNks,5741783
|
|
49
|
+
api/static/assets/index-CB4V1LI5.css,sha256=s-fUWesYGl8kRqel4-sVfuc-xgbVvoJkjocPZFG1y7k,19867
|
|
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.0.dist-info/METADATA,sha256=lkBbue0m5K4jyGYvhygJixkTlQhFvQ3oaQHiETdZcEs,7450
|
|
63
|
+
alchemist_nrel-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
64
|
+
alchemist_nrel-0.3.0.dist-info/entry_points.txt,sha256=asivtbePfGKa57dNbOe43lVbfN51S9cuJwfUoaE9TOs,69
|
|
65
|
+
alchemist_nrel-0.3.0.dist-info/top_level.txt,sha256=-T0oWa1AfOHigC-CJFatoYBZapTpqTBVccZ7eelT5jY,35
|
|
66
|
+
alchemist_nrel-0.3.0.dist-info/RECORD,,
|
api/main.py
CHANGED
|
@@ -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,
|
|
@@ -82,8 +87,17 @@ async def health_check():
|
|
|
82
87
|
|
|
83
88
|
|
|
84
89
|
# Mount static files for production (if they exist)
|
|
85
|
-
|
|
90
|
+
# Priority order:
|
|
91
|
+
# 1. api/static/ - Production (pip installed or built package)
|
|
92
|
+
# 2. alchemist-web/dist/ - Development (after manual npm run build)
|
|
93
|
+
api_static_dir = Path(__file__).parent / "static"
|
|
94
|
+
dev_static_dir = Path(__file__).parent.parent / "alchemist-web" / "dist"
|
|
95
|
+
|
|
96
|
+
# Use api/static if it exists (production), otherwise fall back to dev build
|
|
97
|
+
static_dir = api_static_dir if api_static_dir.exists() else dev_static_dir
|
|
98
|
+
|
|
86
99
|
if static_dir.exists():
|
|
100
|
+
logger.info(f"Serving static files from: {static_dir}")
|
|
87
101
|
app.mount("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets")
|
|
88
102
|
|
|
89
103
|
@app.get("/{full_path:path}")
|
|
@@ -104,6 +118,8 @@ if static_dir.exists():
|
|
|
104
118
|
return FileResponse(index_path)
|
|
105
119
|
|
|
106
120
|
return {"detail": "Not Found"}
|
|
121
|
+
else:
|
|
122
|
+
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
123
|
|
|
108
124
|
|
|
109
125
|
if __name__ == "__main__":
|
api/models/requests.py
CHANGED
|
@@ -182,6 +182,36 @@ class FindOptimumRequest(BaseModel):
|
|
|
182
182
|
)
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
# ============================================================
|
|
186
|
+
# Initial Design (DoE) Models
|
|
187
|
+
# ============================================================
|
|
188
|
+
|
|
189
|
+
class InitialDesignRequest(BaseModel):
|
|
190
|
+
"""Request for generating initial experimental design."""
|
|
191
|
+
method: Literal["random", "lhs", "sobol", "halton", "hammersly"] = Field(
|
|
192
|
+
default="lhs",
|
|
193
|
+
description="Sampling method"
|
|
194
|
+
)
|
|
195
|
+
n_points: int = Field(default=10, ge=1, le=1000, description="Number of points to generate")
|
|
196
|
+
random_seed: Optional[int] = Field(None, description="Random seed for reproducibility")
|
|
197
|
+
lhs_criterion: str = Field(
|
|
198
|
+
default="maximin",
|
|
199
|
+
pattern="^(maximin|correlation|ratio)$",
|
|
200
|
+
description="Criterion for LHS method"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(
|
|
204
|
+
json_schema_extra={
|
|
205
|
+
"example": {
|
|
206
|
+
"method": "lhs",
|
|
207
|
+
"n_points": 10,
|
|
208
|
+
"random_seed": 42,
|
|
209
|
+
"lhs_criterion": "maximin"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
185
215
|
# ============================================================
|
|
186
216
|
# Prediction Models
|
|
187
217
|
# ============================================================
|
|
@@ -200,3 +230,44 @@ class PredictionRequest(BaseModel):
|
|
|
200
230
|
}
|
|
201
231
|
}
|
|
202
232
|
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ============================================================
|
|
236
|
+
# Audit Log & Session Management Models
|
|
237
|
+
# ============================================================
|
|
238
|
+
|
|
239
|
+
class UpdateMetadataRequest(BaseModel):
|
|
240
|
+
"""Request to update session metadata."""
|
|
241
|
+
name: Optional[str] = Field(None, description="Session name")
|
|
242
|
+
description: Optional[str] = Field(None, description="Session description")
|
|
243
|
+
tags: Optional[List[str]] = Field(None, description="Session tags")
|
|
244
|
+
|
|
245
|
+
model_config = ConfigDict(
|
|
246
|
+
json_schema_extra={
|
|
247
|
+
"example": {
|
|
248
|
+
"name": "Catalyst_Screening_Nov2025",
|
|
249
|
+
"description": "Pt/Pd ratio optimization for CO2 reduction",
|
|
250
|
+
"tags": ["catalyst", "CO2", "electrochemistry"]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class LockDecisionRequest(BaseModel):
|
|
257
|
+
"""Request to lock in a decision to the audit log."""
|
|
258
|
+
lock_type: Literal["data", "model", "acquisition"] = Field(..., description="Type of decision to lock")
|
|
259
|
+
notes: Optional[str] = Field(None, description="Optional notes about this decision")
|
|
260
|
+
|
|
261
|
+
# For acquisition lock
|
|
262
|
+
strategy: Optional[str] = Field(None, description="Acquisition strategy (required for acquisition lock)")
|
|
263
|
+
parameters: Optional[Dict[str, Any]] = Field(None, description="Acquisition parameters (required for acquisition lock)")
|
|
264
|
+
suggestions: Optional[List[Dict[str, Any]]] = Field(None, description="Suggested experiments (required for acquisition lock)")
|
|
265
|
+
|
|
266
|
+
model_config = ConfigDict(
|
|
267
|
+
json_schema_extra={
|
|
268
|
+
"example": {
|
|
269
|
+
"lock_type": "model",
|
|
270
|
+
"notes": "Best cross-validation performance: R²=0.93"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
)
|