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.
Files changed (42) hide show
  1. alchemist_core/__init__.py +14 -7
  2. alchemist_core/acquisition/botorch_acquisition.py +15 -6
  3. alchemist_core/audit_log.py +594 -0
  4. alchemist_core/data/experiment_manager.py +76 -5
  5. alchemist_core/models/botorch_model.py +6 -4
  6. alchemist_core/models/sklearn_model.py +74 -8
  7. alchemist_core/session.py +788 -39
  8. alchemist_core/utils/doe.py +200 -0
  9. alchemist_nrel-0.3.1.dist-info/METADATA +185 -0
  10. alchemist_nrel-0.3.1.dist-info/RECORD +66 -0
  11. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/entry_points.txt +1 -0
  12. api/example_client.py +7 -2
  13. api/main.py +21 -4
  14. api/models/requests.py +95 -1
  15. api/models/responses.py +167 -0
  16. api/routers/acquisition.py +25 -0
  17. api/routers/experiments.py +134 -6
  18. api/routers/sessions.py +438 -10
  19. api/routers/visualizations.py +10 -5
  20. api/routers/websocket.py +132 -0
  21. api/run_api.py +56 -0
  22. api/services/session_store.py +285 -54
  23. api/static/NEW_ICON.ico +0 -0
  24. api/static/NEW_ICON.png +0 -0
  25. api/static/NEW_LOGO_DARK.png +0 -0
  26. api/static/NEW_LOGO_LIGHT.png +0 -0
  27. api/static/assets/api-vcoXEqyq.js +1 -0
  28. api/static/assets/index-DWfIKU9j.js +4094 -0
  29. api/static/assets/index-sMIa_1hV.css +1 -0
  30. api/static/index.html +14 -0
  31. api/static/vite.svg +1 -0
  32. ui/gpr_panel.py +7 -2
  33. ui/notifications.py +197 -10
  34. ui/ui.py +1117 -68
  35. ui/variables_setup.py +47 -2
  36. ui/visualizations.py +60 -3
  37. alchemist_core/models/ax_model.py +0 -159
  38. alchemist_nrel-0.2.1.dist-info/METADATA +0 -206
  39. alchemist_nrel-0.2.1.dist-info/RECORD +0 -54
  40. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/WHEEL +0 -0
  41. {alchemist_nrel-0.2.1.dist-info → alchemist_nrel-0.3.1.dist-info}/licenses/LICENSE +0 -0
  42. {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,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  alchemist = main:main
3
+ alchemist-web = api.run_api:main
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
- n_added = response.json()["n_added"]
94
- print(f" ✓ Added {n_added} experiments")
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.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,
@@ -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
- static_dir = Path(__file__).parent / "static"
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
+ )