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
@@ -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.2.1
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/logo.png" alt="ALchemist" width="50%" />
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
- ### Web Application (Recommended)
100
+ ### Installation
92
101
 
93
- **Development Mode:**
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
- # Option 2: Automated start
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
- **Production Mode:**
106
+ **1. Create a new environment:**
105
107
  ```bash
106
- # Build and run
107
- scripts\build_production.bat # Windows
108
- ./scripts/build_production.sh # Linux/Mac
108
+ conda create -n alchemist-env python=3.11
109
+ conda activate alchemist-env
110
+ ```
109
111
 
110
- # Start production server
111
- python run_api.py --production
112
+ **2. Install ALchemist:**
112
113
 
113
- # Access at: http://localhost:8000
114
+ *Option A: From PyPI (recommended):*
115
+ ```bash
116
+ pip install alchemist-nrel
114
117
  ```
115
118
 
116
- **Docker Deployment:**
119
+ *Option B: From GitHub:*
117
120
  ```bash
118
- # Build frontend first
119
- cd alchemist-web && npm run build && cd ..
121
+ pip install git+https://github.com/NREL/ALchemist.git
122
+ ```
120
123
 
121
- # Run with Docker Compose
122
- cd docker
123
- docker-compose up --build
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
- ### Python Package Installation
131
+ All dependencies are specified in `pyproject.toml` and will be installed automatically.
127
132
 
128
- Requirements: Python 3.9 or higher
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
- We recommend using [Anaconda](https://www.anaconda.com/products/distribution) to manage your Python environments.
135
+ ### Running ALchemist
131
136
 
132
- **1. Create a new environment:**
137
+ **Web Application (Recommended):**
133
138
  ```bash
134
- conda create -n alchemist-env python=3.12
135
- conda activate alchemist-env
139
+ alchemist-web
140
+ # Opens at http://localhost:8000
136
141
  ```
137
142
 
138
- **2. Install ALchemist:**
139
-
140
- *Option A: Install directly from GitHub:*
143
+ **Desktop Application:**
141
144
  ```bash
142
- python -m pip install git+https://github.com/NREL/ALchemist.git
145
+ alchemist
146
+ # Launches CustomTkinter GUI
143
147
  ```
144
148
 
145
- *Option B: Clone and install (recommended for development):*
149
+ **Development Mode (Frontend Developers):**
146
150
  ```bash
147
- git clone https://github.com/NREL/ALchemist.git
148
- cd ALchemist
149
- python -m pip install -e .
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
- All dependencies are specified in `pyproject.toml` and will be installed automatically.
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,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  alchemist = main:main
3
+ alchemist-web = run_api:main
@@ -1,4 +1,5 @@
1
1
  alchemist_core
2
2
  api
3
3
  main
4
+ run_api
4
5
  ui
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.1.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
- ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:5173").split(",")
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
- static_dir = Path(__file__).parent / "static"
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
+ )