chembayes 0.1.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ Copyright (c) 2026 Jesus Alberto Martin del Campo
2
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+
4
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: chembayes
3
+ Version: 0.1.0
4
+ Summary: Experimental design and Bayesian optimization with Gaussian Processes and QMC sampling
5
+ Author-email: Jesus Alberto Martin del Campo <j.a.martin-campo@hotmail.com>
6
+ Project-URL: Homepage, https://github.com/jesusalmartin/chembayes
7
+ Project-URL: Bug Tracker, https://github.com/jesusalmartin/chembayes/issues
8
+ Keywords: bayesian-optimization,gaussian-processes,experimental-design,qmc,sampling,chemistry
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: pandas>=1.3
19
+ Requires-Dist: scikit-learn>=1.0
20
+ Requires-Dist: optuna>=3.0
21
+ Requires-Dist: matplotlib>=3.4
22
+ Requires-Dist: scipy>=1.7
23
+ Dynamic: license-file
24
+
25
+ # ChemBayes
26
+
27
+ **ChemBayes** is a Python library designed to streamline **experimental design (DoE)** and **Bayesian optimization** for scientific and chemical research. It leverages Gaussian Processes to model complex response surfaces and efficiently identify optimal conditions.
28
+
29
+ ## 🚀 Key Features
30
+
31
+ * **Intelligent Sampling (QMC):** Generate initial experimental designs using Halton sequences (Quasi-Monte Carlo) for optimal space-filling coverage.
32
+ * **Automatic Preprocessing:** Integrated handling of numeric variables (scaling) and categorical variables (one-hot encoding).
33
+ * **Hyperparameter Tuning:** Automated Gaussian Process parameter adjustment (Matern + WhiteKernel) using Leave-One-Out (LOO) cross-validation and NLPD loss.
34
+ * **Bayesian Optimization:** Global maximum search using the Expected Improvement (EI) acquisition function.
35
+ * **Visual Diagnostics:** Automated generation of feature importance, partial dependence plots (1D and 2D), and model validation charts.
36
+
37
+ ## 📦 Installation
38
+
39
+ From the project root (where `pyproject.toml` is located):
40
+
41
+ ```bash
42
+ pip install .
43
+ ```
44
+
45
+ Or for development mode:
46
+
47
+ ```bash
48
+ pip install -e .
49
+ ```
50
+
51
+ ## 🛠️ Quick Start
52
+
53
+ ### 1. Generating an Experimental Design (Sampling)
54
+
55
+ Define your search space with numeric and categorical parameters:
56
+
57
+ ```python
58
+ import chembayes as cb
59
+
60
+ parameters = {
61
+ 'temperature': {'type': 'float', 'l_bound': 20.0, 'u_bound': 100.0},
62
+ 'time': {'type': 'int', 'l_bound': 5, 'u_bound': 60},
63
+ 'catalyst': {'type': 'categoric', 'categories': ['Pd', 'Ni', 'Cu']}
64
+ }
65
+
66
+ # Generate 20 optimal experimental points
67
+ df_experiments = cb.qmc_sampling(parameters, n_points=20)
68
+
69
+ # Visualize the distribution
70
+ cb.plot_qmc_sampling(df_experiments)
71
+ ```
72
+
73
+ ### 2. Bayesian Optimization
74
+
75
+ Once you have experimental data, find the optimal conditions:
76
+
77
+ ```python
78
+ # Define input features and the target column
79
+ inputs = ['temperature', 'time', 'catalyst']
80
+ output = 'yield'
81
+
82
+ # Run the complete optimization pipeline
83
+ results = cb.optimize_experiment(
84
+ df=df_data,
85
+ inputs=inputs,
86
+ output=output,
87
+ n_tuning_trials=50,
88
+ n_opt_trials=100
89
+ )
90
+
91
+ # Access the best found parameters
92
+ print(f"Best configuration: {results['best_params']}")
93
+ ```
94
+
95
+ ## 📂 Project Structure
96
+
97
+ * `src/chembayes/sampler.py`: Tools for QMC sample generation.
98
+ * `src/chembayes/optimizer.py`: Bayesian optimization engine and Gaussian Processes.
99
+ * `src/chembayes/__init__.py`: Public API exposure.
100
+ * `pyproject.toml`: Modern package configuration and dependencies.
101
+
102
+ ## ✉️ Contact & Contribution
103
+
104
+ **Author:** Jesus Alberto Martin del Campo
105
+ **Email:** j.a.martin-campo@hotmail.com
106
+ **GitHub:** [jesusalmartin/chembayes](https://github.com/jesusalmartin/chembayes)
@@ -0,0 +1,82 @@
1
+ # ChemBayes
2
+
3
+ **ChemBayes** is a Python library designed to streamline **experimental design (DoE)** and **Bayesian optimization** for scientific and chemical research. It leverages Gaussian Processes to model complex response surfaces and efficiently identify optimal conditions.
4
+
5
+ ## 🚀 Key Features
6
+
7
+ * **Intelligent Sampling (QMC):** Generate initial experimental designs using Halton sequences (Quasi-Monte Carlo) for optimal space-filling coverage.
8
+ * **Automatic Preprocessing:** Integrated handling of numeric variables (scaling) and categorical variables (one-hot encoding).
9
+ * **Hyperparameter Tuning:** Automated Gaussian Process parameter adjustment (Matern + WhiteKernel) using Leave-One-Out (LOO) cross-validation and NLPD loss.
10
+ * **Bayesian Optimization:** Global maximum search using the Expected Improvement (EI) acquisition function.
11
+ * **Visual Diagnostics:** Automated generation of feature importance, partial dependence plots (1D and 2D), and model validation charts.
12
+
13
+ ## 📦 Installation
14
+
15
+ From the project root (where `pyproject.toml` is located):
16
+
17
+ ```bash
18
+ pip install .
19
+ ```
20
+
21
+ Or for development mode:
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ ## 🛠️ Quick Start
28
+
29
+ ### 1. Generating an Experimental Design (Sampling)
30
+
31
+ Define your search space with numeric and categorical parameters:
32
+
33
+ ```python
34
+ import chembayes as cb
35
+
36
+ parameters = {
37
+ 'temperature': {'type': 'float', 'l_bound': 20.0, 'u_bound': 100.0},
38
+ 'time': {'type': 'int', 'l_bound': 5, 'u_bound': 60},
39
+ 'catalyst': {'type': 'categoric', 'categories': ['Pd', 'Ni', 'Cu']}
40
+ }
41
+
42
+ # Generate 20 optimal experimental points
43
+ df_experiments = cb.qmc_sampling(parameters, n_points=20)
44
+
45
+ # Visualize the distribution
46
+ cb.plot_qmc_sampling(df_experiments)
47
+ ```
48
+
49
+ ### 2. Bayesian Optimization
50
+
51
+ Once you have experimental data, find the optimal conditions:
52
+
53
+ ```python
54
+ # Define input features and the target column
55
+ inputs = ['temperature', 'time', 'catalyst']
56
+ output = 'yield'
57
+
58
+ # Run the complete optimization pipeline
59
+ results = cb.optimize_experiment(
60
+ df=df_data,
61
+ inputs=inputs,
62
+ output=output,
63
+ n_tuning_trials=50,
64
+ n_opt_trials=100
65
+ )
66
+
67
+ # Access the best found parameters
68
+ print(f"Best configuration: {results['best_params']}")
69
+ ```
70
+
71
+ ## 📂 Project Structure
72
+
73
+ * `src/chembayes/sampler.py`: Tools for QMC sample generation.
74
+ * `src/chembayes/optimizer.py`: Bayesian optimization engine and Gaussian Processes.
75
+ * `src/chembayes/__init__.py`: Public API exposure.
76
+ * `pyproject.toml`: Modern package configuration and dependencies.
77
+
78
+ ## ✉️ Contact & Contribution
79
+
80
+ **Author:** Jesus Alberto Martin del Campo
81
+ **Email:** j.a.martin-campo@hotmail.com
82
+ **GitHub:** [jesusalmartin/chembayes](https://github.com/jesusalmartin/chembayes)
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chembayes"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Jesus Alberto Martin del Campo", email="j.a.martin-campo@hotmail.com" },
10
+ ]
11
+ # Descripción actualizada para incluir el muestreo experimental
12
+ description = "Experimental design and Bayesian optimization with Gaussian Processes and QMC sampling"
13
+ readme = "README.md"
14
+ requires-python = ">=3.9"
15
+ # Se añaden keywords para mejorar la búsqueda del paquete
16
+ keywords = ["bayesian-optimization", "gaussian-processes", "experimental-design", "qmc", "sampling", "chemistry"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Scientific/Engineering :: Chemistry",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "numpy>=1.21",
26
+ "pandas>=1.3",
27
+ "scikit-learn>=1.0",
28
+ "optuna>=3.0",
29
+ "matplotlib>=3.4",
30
+ "scipy>=1.7",
31
+ ]
32
+
33
+ [project.urls]
34
+ "Homepage" = "https://github.com/jesusalmartin/chembayes"
35
+ "Bug Tracker" = "https://github.com/jesusalmartin/chembayes/issues"
36
+
37
+ [tool.setuptools]
38
+ package-dir = {"" = "src"}
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """
2
+ ChemBayes - Bayesian optimization with Gaussian Processes for experimental design.
3
+ """
4
+
5
+ from .optimizer import create_objective, optimize_experiment
6
+ from .sampler import qmc_sampling, plot_qmc_sampling
7
+
8
+ __all__ = [
9
+ 'create_objective',
10
+ 'optimize_experiment',
11
+ 'qmc_sampling',
12
+ 'plot_qmc_sampling'
13
+ ]
@@ -0,0 +1,366 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ from sklearn.preprocessing import StandardScaler, OneHotEncoder
5
+ from sklearn.compose import ColumnTransformer
6
+ from sklearn.gaussian_process import GaussianProcessRegressor
7
+ from sklearn.gaussian_process.kernels import Matern, WhiteKernel
8
+ from sklearn.model_selection import LeaveOneOut
9
+ from sklearn.inspection import permutation_importance, partial_dependence
10
+ from scipy.stats import norm
11
+ import optuna
12
+
13
+ def create_objective(df, weights):
14
+ """
15
+ Create a weighted objective column.
16
+ """
17
+ cols = list(weights.keys())
18
+ subset = df[cols]
19
+ scaler = StandardScaler()
20
+ scaled_data = scaler.fit_transform(subset)
21
+ weight_series = pd.Series(weights)
22
+ objective = scaled_data @ weight_series.values
23
+ return objective
24
+
25
+ def optimize_experiment(df, inputs, output, n_tuning_trials=100, n_opt_trials=100, plot=True):
26
+ """
27
+ Run the complete optimization pipeline using Gaussian Processes and Bayesian optimization.
28
+
29
+ The pipeline consists of:
30
+ 1. Preprocessing numeric features (StandardScaler) and categorical features (OneHotEncoder).
31
+ 2. Tuning GP hyperparameters (Matern + WhiteKernel) via Leave-One-Out cross-validation
32
+ minimizing the average negative log predictive density (NLPD). Tuning uses Optuna.
33
+ 3. Fitting the final GP model with the best hyperparameters.
34
+ 4. (Optional) Diagnostic plots: true vs predicted (colored by uncertainty), permutation importance.
35
+ 5. Bayesian optimization (Expected Improvement) to find the input parameters that maximize
36
+ the objective. This also uses Optuna.
37
+ 6. Reconstructing the best numeric parameters on the original scale.
38
+ 7. (Optional) Partial dependence plots:
39
+ - For numeric features: 1D line plots on the diagonal, 2D contour plots below the diagonal.
40
+ - For categorical features: bar charts showing the average partial dependence per category.
41
+ 8. Printing the best parameters and predicted objective.
42
+
43
+ Parameters
44
+ ----------
45
+ df : pd.DataFrame
46
+ Input data. It must already be cleaned (e.g., NaNs handled) if needed.
47
+ inputs : list of str
48
+ Column names to use as input features.
49
+ output : str or dict
50
+ If str: name of the column to maximize.
51
+ If dict: weights for weighted objective (passed to create_objective).
52
+ n_tuning_trials : int, default=100
53
+ Number of Optuna trials for GP hyperparameter tuning.
54
+ n_opt_trials : int, default=100
55
+ Number of Optuna trials for Bayesian optimization (Expected Improvement).
56
+ plot : bool, default=True
57
+ Whether to show diagnostic plots:
58
+ - true vs predicted (with uncertainty)
59
+ - permutation importance
60
+ - partial dependence (1D/2D for numeric, bar charts for categorical)
61
+
62
+ Returns
63
+ -------
64
+ dict
65
+ A dictionary containing:
66
+ - 'best_params': dict of optimal input parameters (on original scale).
67
+ - 'best_value': float, predicted objective at best_params.
68
+ - 'model': fitted GaussianProcessRegressor.
69
+ - 'preprocessor': fitted ColumnTransformer.
70
+ - 'X': preprocessed input matrix (numpy array).
71
+ - 'y': objective array.
72
+ """
73
+ # Create reduced DataFrame with weighted objective if needed
74
+ if isinstance(output, dict):
75
+ df['objective'] = create_objective(df, output)
76
+ output = 'objective'
77
+
78
+ # Drop rows with missing values in selected inputs or output
79
+ model_df = df.dropna(subset=inputs+[output])
80
+
81
+ X_df = model_df[inputs]
82
+ y = model_df[output]
83
+
84
+ # Identify numeric and categorical columns
85
+ numeric_features = [col for col in X_df.columns if col in X_df.select_dtypes(include='number').columns]
86
+ categorical_features = [col for col in X_df.columns if col in X_df.select_dtypes(include=['object', 'category', 'string']).columns]
87
+
88
+ # Preprocess input columns using ColumnTransformer
89
+ preprocessor = ColumnTransformer(
90
+ transformers=[
91
+ ('num', StandardScaler(), numeric_features),
92
+ ('cat', OneHotEncoder(handle_unknown='ignore', sparse_output=False), categorical_features)
93
+ ])
94
+
95
+ X = preprocessor.fit_transform(X_df)
96
+ model_features = preprocessor.get_feature_names_out()
97
+ X_model = pd.DataFrame(X, columns=model_features)
98
+
99
+ # Keep a reference to the scaler for inverse transformation later
100
+ scaler = preprocessor.named_transformers_['num']
101
+
102
+ # ----- Gaussian Process hyperparameter tuning (LOO-CV with NLPD) -----
103
+ def gp_tuning_objective(trial):
104
+ # Suggest hyperparameters
105
+ length_scale = trial.suggest_float('length_scale', 0.1, 10.0, log=True)
106
+ noise_level = trial.suggest_float('noise_level', 1e-3, 10.0, log=True)
107
+
108
+ nu_values = [0.5, 1.5, 2.5, np.inf]
109
+ nu_idx = trial.suggest_int('nu_idx', 0, 3)
110
+ nu = nu_values[nu_idx]
111
+
112
+ # Leave-one-out cross-validation
113
+ loo = LeaveOneOut()
114
+ nlpds = [] # store negative log predictive densities
115
+
116
+ for train_idx, test_idx in loo.split(X_df):
117
+ X_train_df = X_model.iloc[train_idx]
118
+ y_train = y.iloc[train_idx]
119
+ X_test_df = X_model.iloc[test_idx]
120
+ y_test = y.iloc[test_idx]
121
+
122
+ # Create kernel with current hyperparameters
123
+ kernel = Matern(length_scale=length_scale, nu=nu) + WhiteKernel(noise_level=noise_level)
124
+
125
+ # Build GP (no internal optimization, fixed kernel parameters)
126
+ gp = GaussianProcessRegressor(
127
+ kernel=kernel,
128
+ alpha=0.0, # we use WhiteKernel instead
129
+ optimizer=None,
130
+ normalize_y=True,
131
+ random_state=42
132
+ )
133
+ gp.fit(X_train_df, y_train)
134
+
135
+ # Predict on test point (mean and std)
136
+ mu, sigma = gp.predict(X_test_df, return_std=True)
137
+ mu = mu[0]
138
+ sigma = sigma[0]
139
+
140
+ # Avoid sigma=0 by clipping
141
+ sigma = max(sigma, 1e-6)
142
+ nlpd = 0.5 * np.log(2 * np.pi * sigma**2) + (y_test - mu)**2 / (2 * sigma**2)
143
+ nlpds.append(nlpd)
144
+
145
+ # Average NLPD across folds (to be minimized)
146
+ return np.mean(nlpds)
147
+
148
+ gp_tuning_study = optuna.create_study(direction='minimize')
149
+ gp_tuning_study.optimize(gp_tuning_objective, n_trials=n_tuning_trials)
150
+ gp_best_params = gp_tuning_study.best_params
151
+
152
+ # Build final GP with best hyperparameters
153
+ nu_values = [0.5, 1.5, 2.5, np.inf]
154
+ length_scale = gp_best_params['length_scale']
155
+ noise_level = gp_best_params['noise_level']
156
+ nu = nu_values[gp_best_params['nu_idx']]
157
+
158
+ kernel = Matern(length_scale=length_scale, nu=nu) + WhiteKernel(noise_level)
159
+ model = GaussianProcessRegressor(
160
+ kernel=kernel,
161
+ alpha=0.0,
162
+ optimizer=None,
163
+ normalize_y=True,
164
+ random_state=42
165
+ )
166
+ model.fit(X_model, y)
167
+
168
+
169
+ # ----- Bayesian optimization (Expected Improvement) -----
170
+ y_best = y.max()
171
+
172
+ def objective(trial):
173
+ numeric_model_features = [f for f in model_features if f.startswith('num__')]
174
+ categorical_model_features = [f for f in model_features if f.startswith('cat__')]
175
+ trial_dict = {}
176
+ for feature in numeric_model_features:
177
+ trial_dict[feature] = [trial.suggest_float(feature, X_model[feature].min(), X_model[feature].max())]
178
+ for feature in categorical_features:
179
+ ohe_group = [ohe_feature for ohe_feature in categorical_model_features if f'cat__{feature}' in ohe_feature]
180
+ trial_category = trial.suggest_categorical(feature, X_df[feature].unique())
181
+ for ohe_feature in ohe_group:
182
+ trial_dict[ohe_feature] = 1 if ohe_feature == f'cat__{feature}_{trial_category}' else 0
183
+ trial_df = pd.DataFrame(trial_dict)
184
+ trial_df = trial_df[model_features]
185
+
186
+ # Predict mean and standard deviation
187
+ mu, sigma = model.predict(trial_df, return_std=True)
188
+ mu = mu[0]
189
+ sigma = sigma[0]
190
+
191
+ # Expected Improvement for maximization
192
+ if sigma <= 0:
193
+ return 0.0
194
+ imp = mu - y_best
195
+ z = imp / sigma
196
+ ei = imp * norm.cdf(z) + sigma * norm.pdf(z)
197
+ return ei
198
+
199
+ study = optuna.create_study(direction="maximize")
200
+ study.optimize(objective, n_trials=n_opt_trials)
201
+ best_params = study.best_params
202
+
203
+ # ----- Reconstruct best numeric values on original scale -----
204
+ best_num_values_scaled = np.array([value for feature, value in best_params.items() if 'num__' in feature]).reshape(1, -1)
205
+ best_num_values = scaler.inverse_transform(best_num_values_scaled)
206
+ opt_params = {}
207
+ for index, feature in enumerate(numeric_features):
208
+ opt_params[feature] = best_num_values[0, index]
209
+ for feature in categorical_features:
210
+ opt_params[feature] = best_params[feature]
211
+
212
+ # ----- Diagnostic plots (if requested) -----
213
+ if plot:
214
+ # True vs predicted with uncertainty
215
+ y_pred, sigma = model.predict(pd.DataFrame(X, columns=model.feature_names_in_), return_std=True)
216
+ plt.figure()
217
+ sc = plt.scatter(y, y_pred, c=sigma, alpha=0.6, cmap='coolwarm', edgecolor='k', linewidth=0.5)
218
+ plt.colorbar(sc, label=f'Prediction std ({output})')
219
+ plt.xlabel(f"True {output}")
220
+ plt.ylabel(f"Predicted {output}")
221
+ plt.title(f"{output}: true vs. predicted (colored by uncertainty)")
222
+ plt.plot([y.min(), y.max()], [y.min(), y.max()], 'k--', linewidth=1)
223
+ plt.tight_layout()
224
+ plt.show()
225
+
226
+ # Permutation importance
227
+ permutation_result = permutation_importance(model, X_model, y, scoring='r2', n_repeats=30, random_state=42)
228
+ importances = permutation_result.importances_mean
229
+ sorted_idx = importances.argsort()[::-1]
230
+ sorted_importances = importances[sorted_idx]
231
+ sorted_features = model.feature_names_in_[sorted_idx]
232
+ plt.figure()
233
+ plt.barh(sorted_features[:15], sorted_importances[:15])
234
+ plt.gca().invert_yaxis()
235
+ plt.xlabel("Permutation importance")
236
+ plt.tight_layout()
237
+ plt.show()
238
+
239
+ # Partial dependence for numeric features: 1D on diagonal, 2D below diagonal
240
+ if numeric_features:
241
+ numeric_model_features = [f for f in model_features if f.startswith('num__')]
242
+
243
+ n_plots = len(numeric_model_features)
244
+ grid_point_number = 25
245
+
246
+ fig, axes = plt.subplots(n_plots, n_plots, figsize=(3*n_plots, 3*n_plots))
247
+
248
+ for i, feature_i in enumerate(numeric_model_features):
249
+ for j, feature_j in enumerate(numeric_model_features):
250
+ ax = axes[i, j] if n_plots > 1 else axes
251
+
252
+ if i == j:
253
+ # 1D partial dependence on diagonal
254
+ part_dep = partial_dependence(
255
+ model,
256
+ X=X_model,
257
+ features=[feature_i],
258
+ custom_values={feature_i: np.linspace(X_model[feature_i].min(), X_model[feature_i].max(), grid_point_number)}
259
+ )
260
+ xi = part_dep['grid_values'][0]
261
+ yi = part_dep['average'][0]
262
+
263
+ # Transform back to original scale for labeling
264
+ original_feature_i = feature_i[5:]
265
+ original_idx_i = numeric_features.index(original_feature_i)
266
+ dummy_array = np.zeros((grid_point_number, len(numeric_features)))
267
+ dummy_array[:, original_idx_i] = xi
268
+ transformed_array = scaler.inverse_transform(dummy_array)
269
+ xi = transformed_array[:, original_idx_i]
270
+
271
+ ax.plot(xi, yi)
272
+ ax.axvline(x=opt_params[original_feature_i], color='black', alpha=0.5)
273
+ ax.set_xlabel(original_feature_i)
274
+ ax.set_ylabel(output)
275
+
276
+ elif j < i:
277
+ # 2D partial dependence below diagonal
278
+ part_dep = partial_dependence(
279
+ model,
280
+ X=X_model,
281
+ features=[feature_i, feature_j],
282
+ custom_values={
283
+ feature_i: np.linspace(X_model[feature_i].min(), X_model[feature_i].max(), grid_point_number),
284
+ feature_j: np.linspace(X_model[feature_j].min(), X_model[feature_j].max(), grid_point_number)
285
+ }
286
+ )
287
+
288
+ xi = part_dep['grid_values'][0]
289
+ yi = part_dep['grid_values'][1]
290
+ zi = part_dep['average'][0].T
291
+
292
+ original_feature_i = feature_i[5:]
293
+ original_feature_j = feature_j[5:]
294
+
295
+ original_idx_i = numeric_features.index(original_feature_i)
296
+ original_idx_j = numeric_features.index(original_feature_j)
297
+
298
+ dummy_array = np.zeros((grid_point_number, len(numeric_features)))
299
+ dummy_array[:, original_idx_i] = xi
300
+ dummy_array[:, original_idx_j] = yi
301
+
302
+ transformed_array = scaler.inverse_transform(dummy_array)
303
+ xi = transformed_array[:, original_idx_i]
304
+ yi = transformed_array[:, original_idx_j]
305
+
306
+ ax.pcolormesh(xi, yi, zi, cmap='coolwarm')
307
+ ax.scatter(opt_params[original_feature_i], opt_params[original_feature_j], color='black', alpha=0.5)
308
+ ax.set_xlabel(original_feature_i)
309
+ ax.set_ylabel(original_feature_j)
310
+ else:
311
+ # Upper triangular part: hide axes
312
+ ax.axis('off')
313
+ fig.tight_layout()
314
+ plt.show()
315
+
316
+ # Partial dependence for categorical features: bar charts per category
317
+ if categorical_features:
318
+ categorical_model_features = [f for f in model_features if f.startswith('cat__')]
319
+
320
+ n_plots = len(categorical_features)
321
+ fig, axes = plt.subplots(n_plots, 1, figsize=(6, 5*n_plots))
322
+
323
+ for i, feature in enumerate(categorical_features):
324
+ ax = axes[i] if n_plots > 1 else axes
325
+
326
+ # Get all one-hot columns for this original feature
327
+ ohe_features = [f for f in categorical_model_features if f.startswith(f'cat__{feature}')]
328
+
329
+ x_labels = []
330
+ y_values = []
331
+
332
+ for ohe_feature in ohe_features:
333
+ part_dep = partial_dependence(model, X=X_model, features=[ohe_feature])
334
+ yi = part_dep['average'][0]
335
+ category = ohe_feature[len(f'cat__{feature}_'):]
336
+ y_values.append(yi[-1]) # partial dependence value for this category
337
+ x_labels.append(category)
338
+
339
+ ax.bar(x_labels, y_values)
340
+ ax.set_xlabel(feature)
341
+ ax.set_ylabel(output)
342
+ ax.tick_params(axis='x', labelrotation=45)
343
+ fig.tight_layout()
344
+ plt.show()
345
+
346
+ # Predict objective at the optimal input
347
+ opt_params_df = pd.DataFrame([opt_params])
348
+ opt_params_input = preprocessor.transform(opt_params_df)
349
+ best_output = model.predict(pd.DataFrame(opt_params_input, columns=model.feature_names_in_))[0]
350
+
351
+ print("\n--- Optimization results ---")
352
+ print(f"Model score (R²): {model.score(pd.DataFrame(X, columns=model.feature_names_in_), y):.4f}")
353
+ print("Best input parameters:")
354
+ for feature, value in opt_params.items():
355
+ print(f" {feature}: {value}")
356
+ print(f"Best predicted {output}: {best_output:.4f}")
357
+
358
+ # ----- Return all important objects -----
359
+ return {
360
+ 'best_params': opt_params,
361
+ 'best_value': best_output,
362
+ 'model': model,
363
+ 'preprocessor': preprocessor,
364
+ 'X': X,
365
+ 'y': y
366
+ }
@@ -0,0 +1,106 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from scipy.stats import qmc
5
+
6
+ def qmc_sampling(parameters, n_points, force_edges=True):
7
+ """
8
+ Generate a Quasi-Monte Carlo (QMC) sample using the Halton sequence.
9
+
10
+ This function creates a multidimensional sample based on the provided parameter
11
+ definitions, scaling each dimension to its specified bounds and types.
12
+
13
+ Parameters
14
+ ----------
15
+ parameters : dict
16
+ A dictionary defining the search space. Each key is a parameter name,
17
+ and the value is another dict containing:
18
+ - 'type': str ('float', 'int', or 'categoric')
19
+ - 'l_bound': float/int (required for numeric types)
20
+ - 'u_bound': float/int (required for numeric types)
21
+ - 'categories': list (required for 'categoric' type)
22
+ n_points : int
23
+ The number of samples to generate.
24
+ force_edges : bool, default=True
25
+ If True, the samples are rescaled so that the minimum and maximum
26
+ generated values exactly match the specified bounds.
27
+ If False, values are kept in the [0, 1] probability space before scaling.
28
+
29
+ Returns
30
+ -------
31
+ pd.DataFrame
32
+ A DataFrame where each column corresponds to a parameter and each
33
+ row is a generated sample point.
34
+ """
35
+ sampler = qmc.Halton(d=len(parameters), seed=42)
36
+ sample = sampler.random(n=n_points)
37
+
38
+ sample_dict = {}
39
+ for index, p in enumerate(parameters.keys()):
40
+
41
+ values = sample[:, index]
42
+
43
+ if force_edges:
44
+ v_min, v_max = values.min(), values.max()
45
+ elif force_edges==False:
46
+ v_min, v_max = 0.0, 1.0
47
+ else:
48
+ return print('forced_edges must be True or False')
49
+
50
+ p_type = parameters[p]['type']
51
+
52
+ if p_type == 'float':
53
+ l_bound = parameters[p]['l_bound']
54
+ u_bound = parameters[p]['u_bound']
55
+ scaled = l_bound + (values - v_min) * (u_bound - l_bound) / (v_max - v_min)
56
+ sample_dict[p] = scaled
57
+
58
+ elif p_type == 'int':
59
+ l_bound = parameters[p]['l_bound']
60
+ u_bound = parameters[p]['u_bound']
61
+ scaled = l_bound + (values - v_min) * (u_bound - l_bound) / (v_max - v_min)
62
+ scaled = np.round(scaled).astype(int)
63
+ sample_dict[p] = scaled
64
+
65
+ elif p_type == 'categoric':
66
+ v_min, v_max = values.min(), values.max()
67
+ categories = parameters[p]['categories']
68
+ l_bound = 0
69
+ u_bound = len(categories)-1
70
+ scaled = l_bound + (values - v_min) * (u_bound - l_bound) / (v_max - v_min)
71
+ scaled = np.round(scaled).astype(int).tolist()
72
+ scaled = [categories[v] for v in scaled]
73
+ sample_dict[p] = scaled
74
+ else:
75
+ return print(f'{p}: type must be float, int or categoric')
76
+ df = pd.DataFrame(sample_dict)
77
+ return df
78
+
79
+ def plot_qmc_sampling(df):
80
+ """
81
+ Create a scatter plot matrix of the numerical variables in the sample.
82
+
83
+ This visualizes the space-filling properties of the QMC sampling by
84
+ plotting each numeric parameter against every other numeric parameter
85
+ in a grid of subplots.
86
+
87
+ Parameters
88
+ ----------
89
+ df : pd.DataFrame
90
+ The DataFrame generated by `qmc_sampling`.
91
+ """
92
+ num_df = df.select_dtypes(include='number')
93
+ n_vars = len(num_df.columns)
94
+
95
+ plt.figure(figsize=(3*n_vars, 3*n_vars))
96
+ plot = 1
97
+ for i, var_1 in enumerate(num_df.columns):
98
+ for j, var_2 in enumerate(num_df.columns):
99
+ if i >= j:
100
+ plt.subplot(n_vars, n_vars, plot)
101
+ plt.scatter(num_df[var_1], num_df[var_2])
102
+ plt.xlabel(var_1)
103
+ plt.ylabel(var_2)
104
+ plot +=1
105
+ plt.tight_layout()
106
+ plt.show()
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: chembayes
3
+ Version: 0.1.0
4
+ Summary: Experimental design and Bayesian optimization with Gaussian Processes and QMC sampling
5
+ Author-email: Jesus Alberto Martin del Campo <j.a.martin-campo@hotmail.com>
6
+ Project-URL: Homepage, https://github.com/jesusalmartin/chembayes
7
+ Project-URL: Bug Tracker, https://github.com/jesusalmartin/chembayes/issues
8
+ Keywords: bayesian-optimization,gaussian-processes,experimental-design,qmc,sampling,chemistry
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: pandas>=1.3
19
+ Requires-Dist: scikit-learn>=1.0
20
+ Requires-Dist: optuna>=3.0
21
+ Requires-Dist: matplotlib>=3.4
22
+ Requires-Dist: scipy>=1.7
23
+ Dynamic: license-file
24
+
25
+ # ChemBayes
26
+
27
+ **ChemBayes** is a Python library designed to streamline **experimental design (DoE)** and **Bayesian optimization** for scientific and chemical research. It leverages Gaussian Processes to model complex response surfaces and efficiently identify optimal conditions.
28
+
29
+ ## 🚀 Key Features
30
+
31
+ * **Intelligent Sampling (QMC):** Generate initial experimental designs using Halton sequences (Quasi-Monte Carlo) for optimal space-filling coverage.
32
+ * **Automatic Preprocessing:** Integrated handling of numeric variables (scaling) and categorical variables (one-hot encoding).
33
+ * **Hyperparameter Tuning:** Automated Gaussian Process parameter adjustment (Matern + WhiteKernel) using Leave-One-Out (LOO) cross-validation and NLPD loss.
34
+ * **Bayesian Optimization:** Global maximum search using the Expected Improvement (EI) acquisition function.
35
+ * **Visual Diagnostics:** Automated generation of feature importance, partial dependence plots (1D and 2D), and model validation charts.
36
+
37
+ ## 📦 Installation
38
+
39
+ From the project root (where `pyproject.toml` is located):
40
+
41
+ ```bash
42
+ pip install .
43
+ ```
44
+
45
+ Or for development mode:
46
+
47
+ ```bash
48
+ pip install -e .
49
+ ```
50
+
51
+ ## 🛠️ Quick Start
52
+
53
+ ### 1. Generating an Experimental Design (Sampling)
54
+
55
+ Define your search space with numeric and categorical parameters:
56
+
57
+ ```python
58
+ import chembayes as cb
59
+
60
+ parameters = {
61
+ 'temperature': {'type': 'float', 'l_bound': 20.0, 'u_bound': 100.0},
62
+ 'time': {'type': 'int', 'l_bound': 5, 'u_bound': 60},
63
+ 'catalyst': {'type': 'categoric', 'categories': ['Pd', 'Ni', 'Cu']}
64
+ }
65
+
66
+ # Generate 20 optimal experimental points
67
+ df_experiments = cb.qmc_sampling(parameters, n_points=20)
68
+
69
+ # Visualize the distribution
70
+ cb.plot_qmc_sampling(df_experiments)
71
+ ```
72
+
73
+ ### 2. Bayesian Optimization
74
+
75
+ Once you have experimental data, find the optimal conditions:
76
+
77
+ ```python
78
+ # Define input features and the target column
79
+ inputs = ['temperature', 'time', 'catalyst']
80
+ output = 'yield'
81
+
82
+ # Run the complete optimization pipeline
83
+ results = cb.optimize_experiment(
84
+ df=df_data,
85
+ inputs=inputs,
86
+ output=output,
87
+ n_tuning_trials=50,
88
+ n_opt_trials=100
89
+ )
90
+
91
+ # Access the best found parameters
92
+ print(f"Best configuration: {results['best_params']}")
93
+ ```
94
+
95
+ ## 📂 Project Structure
96
+
97
+ * `src/chembayes/sampler.py`: Tools for QMC sample generation.
98
+ * `src/chembayes/optimizer.py`: Bayesian optimization engine and Gaussian Processes.
99
+ * `src/chembayes/__init__.py`: Public API exposure.
100
+ * `pyproject.toml`: Modern package configuration and dependencies.
101
+
102
+ ## ✉️ Contact & Contribution
103
+
104
+ **Author:** Jesus Alberto Martin del Campo
105
+ **Email:** j.a.martin-campo@hotmail.com
106
+ **GitHub:** [jesusalmartin/chembayes](https://github.com/jesusalmartin/chembayes)
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/chembayes/__init__.py
5
+ src/chembayes/optimizer.py
6
+ src/chembayes/sampler.py
7
+ src/chembayes.egg-info/PKG-INFO
8
+ src/chembayes.egg-info/SOURCES.txt
9
+ src/chembayes.egg-info/dependency_links.txt
10
+ src/chembayes.egg-info/requires.txt
11
+ src/chembayes.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ numpy>=1.21
2
+ pandas>=1.3
3
+ scikit-learn>=1.0
4
+ optuna>=3.0
5
+ matplotlib>=3.4
6
+ scipy>=1.7
@@ -0,0 +1 @@
1
+ chembayes