ravimohan19 commited on
Commit
fe4aa70
·
verified ·
1 Parent(s): 495f8fd

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +565 -0
app.py ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio application for the Physics-Informed Bayesian Optimization Platform.
3
+
4
+ Provides an interactive UI for:
5
+ 1. Defining parameter spaces
6
+ 2. Specifying physics models (Python code)
7
+ 3. Uploading initial experimental data
8
+ 4. Running BO campaigns
9
+ 5. Visualizing results
10
+ """
11
+
12
+ import io
13
+ import json
14
+ import traceback
15
+ from typing import Optional
16
+
17
+ import gradio as gr
18
+ import matplotlib
19
+ matplotlib.use("Agg")
20
+ import matplotlib.pyplot as plt
21
+ import numpy as np
22
+ import pandas as pd
23
+ import torch
24
+ from torch import Tensor
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Utility: safely compile user-supplied physics model code
28
+ # ---------------------------------------------------------------------------
29
+
30
+ BUILTIN_PHYSICS = {
31
+ "Arrhenius Kinetics": {
32
+ "code": """\
33
+ def physics_model(X):
34
+ \"\"\"Arrhenius kinetics: rate = A * exp(-Ea / (R*T)) * C^n\"\"\"
35
+ T = X[:, 0] # temperature (K)
36
+ C = X[:, 1] # concentration
37
+ A = 1e8 # pre-exponential factor
38
+ Ea = 50.0 # activation energy (kJ/mol)
39
+ R = 8.314e-3 # gas constant (kJ/mol·K)
40
+ n = 0.5 # reaction order
41
+ return A * torch.exp(-Ea / (R * T)) * C ** n
42
+ """,
43
+ "params": "temperature (K): 300-800\nconcentration: 0.1-10",
44
+ },
45
+ "Flory-Huggins Mixing": {
46
+ "code": """\
47
+ def physics_model(X):
48
+ \"\"\"Flory-Huggins free energy of mixing for binary polymer blend.\"\"\"
49
+ phi = X[:, 0] # volume fraction (0-1)
50
+ chi = X[:, 1] # Flory-Huggins parameter
51
+ N = 100.0 # degree of polymerisation
52
+ entropy = phi * torch.log(phi + 1e-8) / N + (1 - phi) * torch.log(1 - phi + 1e-8) / N
53
+ enthalpy = chi * phi * (1 - phi)
54
+ return -(entropy + enthalpy) # negative ΔG_mix (higher = better mixing)
55
+ """,
56
+ "params": "volume_fraction: 0.05-0.95\nchi_parameter: 0.0-2.0",
57
+ },
58
+ "Polymer Recyclability": {
59
+ "code": """\
60
+ def physics_model(X):
61
+ \"\"\"Simplified recyclability metric for polymer formulation.\"\"\"
62
+ ratio = X[:, 0] # monomer ratio
63
+ temp = X[:, 1] # temperature (K)
64
+ catalyst = X[:, 2] # catalyst loading (wt%)
65
+ mixing = -ratio * torch.log(ratio + 1e-8) - (1 - ratio) * torch.log(1 - ratio + 1e-8)
66
+ chi = 0.5 - 0.3 * (ratio - 0.5) ** 2
67
+ mixing_fe = mixing - chi * ratio * (1 - ratio)
68
+ rate = torch.exp(-50.0 / (8.314e-3 * temp))
69
+ cat_eff = 1 - torch.exp(-0.8 * catalyst)
70
+ return 5.0 * mixing_fe * rate * cat_eff + 2.0
71
+ """,
72
+ "params": "monomer_ratio: 0.1-0.9\ntemperature (K): 350-500\ncatalyst_loading (wt%): 0.5-5.0",
73
+ },
74
+ "Custom (enter code below)": {"code": "", "params": ""},
75
+ }
76
+
77
+ DEMO_CSV = """\
78
+ temperature,concentration,yield
79
+ 350,1.0,0.12
80
+ 400,3.0,0.45
81
+ 450,5.0,0.78
82
+ 500,2.0,0.55
83
+ 480,7.0,0.91
84
+ """
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Compile physics model from code string
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def _compile_physics_fn(code: str):
91
+ """Safely compile user-provided physics model code.
92
+
93
+ The code must define a function called `physics_model(X)`.
94
+ """
95
+ allowed_globals = {"torch": torch, "np": np, "Tensor": Tensor, "__builtins__": {}}
96
+ # Add safe builtins
97
+ import builtins
98
+ safe_builtins = {
99
+ k: getattr(builtins, k)
100
+ for k in ("range", "len", "float", "int", "abs", "max", "min", "print", "list", "tuple", "dict", "True", "False", "None")
101
+ }
102
+ allowed_globals["__builtins__"] = safe_builtins
103
+
104
+ local_ns = {}
105
+ exec(code, allowed_globals, local_ns) # noqa: S102
106
+ if "physics_model" not in local_ns:
107
+ raise ValueError("Code must define a function called `physics_model(X)`.")
108
+ return local_ns["physics_model"]
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Parse parameter space from multiline text
113
+ # ---------------------------------------------------------------------------
114
+
115
+ def _parse_params(text: str):
116
+ """Parse parameter definitions from multiline text.
117
+
118
+ Format per line: name: lower-upper
119
+ Example: temperature (K): 300-800
120
+ """
121
+ from physics_informed_bo.experiment.parameter_space import ParameterSpace
122
+
123
+ space = ParameterSpace()
124
+ names = []
125
+ for line in text.strip().splitlines():
126
+ line = line.strip()
127
+ if not line:
128
+ continue
129
+ name_part, bounds_part = line.rsplit(":", 1)
130
+ name = name_part.strip()
131
+ lo, hi = bounds_part.strip().split("-")
132
+ space.add_continuous(name, float(lo), float(hi))
133
+ names.append(name)
134
+ return space, names
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Core optimisation routine
139
+ # ---------------------------------------------------------------------------
140
+
141
+ def run_optimization(
142
+ physics_template: str,
143
+ physics_code: str,
144
+ param_text: str,
145
+ csv_file,
146
+ csv_text: str,
147
+ objective_col: str,
148
+ acq_fn: str,
149
+ n_initial: int,
150
+ n_iterations: int,
151
+ batch_size: int,
152
+ noise_var: float,
153
+ maximize: bool,
154
+ seed: int,
155
+ ):
156
+ """Run the full physics-informed BO campaign and return results."""
157
+ try:
158
+ torch.manual_seed(seed)
159
+
160
+ # ── 1. Physics model ──────────────────────────────────────────────
161
+ code = physics_code.strip()
162
+ if physics_template != "Custom (enter code below)" and not code:
163
+ code = BUILTIN_PHYSICS[physics_template]["code"]
164
+ physics_fn = _compile_physics_fn(code) if code else None
165
+
166
+ # ── 2. Parameter space ────────────────────────────────────────────
167
+ if not param_text.strip():
168
+ if physics_template != "Custom (enter code below)":
169
+ param_text = BUILTIN_PHYSICS[physics_template]["params"]
170
+ space, param_names = _parse_params(param_text)
171
+
172
+ # ── 3. Initial data ──────────────────────────────────────────────
173
+ X_init, y_init = None, None
174
+ df_init = None
175
+
176
+ if csv_file is not None:
177
+ df_init = pd.read_csv(csv_file.name)
178
+ elif csv_text.strip():
179
+ df_init = pd.read_csv(io.StringIO(csv_text.strip()))
180
+
181
+ if df_init is not None:
182
+ obj = objective_col.strip() or df_init.columns[-1]
183
+ feature_cols = [c for c in df_init.columns if c != obj]
184
+ # Match feature columns to param names
185
+ if set(feature_cols) != set(param_names):
186
+ # Try to align by order
187
+ feature_cols = [c for c in df_init.columns if c != obj][:len(param_names)]
188
+ X_init = torch.tensor(df_init[feature_cols].values, dtype=torch.float64)
189
+ y_init = torch.tensor(df_init[obj].values, dtype=torch.float64).unsqueeze(-1)
190
+
191
+ # ── 4. Configuration ─────────────────────────────────────────────
192
+ from physics_informed_bo.config import OptimizationConfig, AcquisitionType
193
+
194
+ acq_map = {
195
+ "Expected Improvement (EI)": AcquisitionType.EXPECTED_IMPROVEMENT,
196
+ "Upper Confidence Bound (UCB)": AcquisitionType.UPPER_CONFIDENCE_BOUND,
197
+ "Probability of Improvement (PI)": AcquisitionType.PROBABILITY_OF_IMPROVEMENT,
198
+ "Physics-Informed EI": AcquisitionType.PHYSICS_INFORMED_EI,
199
+ }
200
+
201
+ config = OptimizationConfig(
202
+ acquisition_type=acq_map.get(acq_fn, AcquisitionType.EXPECTED_IMPROVEMENT),
203
+ n_initial_samples=n_initial,
204
+ max_iterations=n_iterations,
205
+ batch_size=batch_size,
206
+ noise_variance=noise_var,
207
+ seed=seed,
208
+ )
209
+
210
+ # ── 5. Build campaign ────────────────────────────────────────────
211
+ from physics_informed_bo.experiment.campaign import OptimizationCampaign
212
+
213
+ initial_data = (X_init, y_init) if X_init is not None else None
214
+
215
+ campaign = OptimizationCampaign(
216
+ name="hf_space_campaign",
217
+ parameter_space=space,
218
+ physics_fn=physics_fn,
219
+ initial_data=initial_data,
220
+ config=config,
221
+ maximize=maximize,
222
+ )
223
+
224
+ # ── 6. Synthetic objective (demo) ─────────────────────────────────
225
+ # When there is a physics model we simulate experiments as
226
+ # physics + discrepancy + noise so the user sees the BO loop in action.
227
+
228
+ def synthetic_objective(params: dict) -> float:
229
+ vals = [params[n] for n in param_names]
230
+ X = torch.tensor([vals], dtype=torch.float64)
231
+ if physics_fn is not None:
232
+ base = physics_fn(X).item()
233
+ else:
234
+ base = 0.0
235
+ discrepancy = 0.15 * np.sin(3.0 * sum(vals))
236
+ noise = noise_var**0.5 * np.random.randn()
237
+ return base + discrepancy + noise
238
+
239
+ # ── 7. Run BO loop ────────────────────────────────────────────────
240
+ log_lines = []
241
+ best_vals = []
242
+
243
+ for it in range(n_iterations):
244
+ suggestions = campaign.suggest_next(batch_size)
245
+ for params in suggestions:
246
+ obj_val = synthetic_objective(params)
247
+ campaign.report_result(params, obj_val)
248
+ best = campaign.get_best() if maximize else campaign.get_best()
249
+ best_vals.append(best["objective"])
250
+ log_lines.append(
251
+ f"Iter {it + 1:3d} | suggested {len(suggestions)} exp(s) | "
252
+ f"best so far = {best['objective']:.4f}"
253
+ )
254
+
255
+ # ── 8. Results ────────────────────────────────────────────────────
256
+ results_df = campaign.to_dataframe()
257
+ best = campaign.get_best()
258
+ summary = campaign.summary()
259
+
260
+ # ── Convergence plot ──────────────────────────────────────────────
261
+ fig_conv, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))
262
+
263
+ objs = results_df["objective"].values
264
+ ax1.plot(objs, "o-", markersize=3, alpha=0.7)
265
+ ax1.set_xlabel("Experiment #")
266
+ ax1.set_ylabel("Objective")
267
+ ax1.set_title("All Observations")
268
+ ax1.grid(True, alpha=0.3)
269
+
270
+ if maximize:
271
+ bsf = np.maximum.accumulate(objs)
272
+ else:
273
+ bsf = np.minimum.accumulate(objs)
274
+ ax2.plot(bsf, "s-", color="green", markersize=3)
275
+ ax2.set_xlabel("Experiment #")
276
+ ax2.set_ylabel("Best Objective")
277
+ ax2.set_title("Convergence (Best So Far)")
278
+ ax2.grid(True, alpha=0.3)
279
+ fig_conv.tight_layout()
280
+
281
+ # ── Parameter exploration heatmap ────────────────────────────────
282
+ fig_params = None
283
+ if len(param_names) >= 2:
284
+ fig_params, ax = plt.subplots(figsize=(7, 5))
285
+ sc = ax.scatter(
286
+ results_df[param_names[0]],
287
+ results_df[param_names[1]],
288
+ c=results_df["objective"],
289
+ cmap="viridis",
290
+ s=30,
291
+ edgecolors="k",
292
+ linewidths=0.5,
293
+ )
294
+ plt.colorbar(sc, ax=ax, label="Objective")
295
+ ax.set_xlabel(param_names[0])
296
+ ax.set_ylabel(param_names[1])
297
+ ax.set_title("Parameter Exploration")
298
+ fig_params.tight_layout()
299
+
300
+ # ── Surrogate 1-D slice ──────────────────────────────────────────
301
+ fig_surrogate = None
302
+ if physics_fn is not None and campaign._designer._surrogate is not None:
303
+ try:
304
+ surrogate = campaign._designer._surrogate
305
+ bounds = space.bounds
306
+ n_grid = 150
307
+
308
+ # Slice through first parameter, others at midpoint
309
+ mid = (bounds[0] + bounds[1]) / 2
310
+ x_range = torch.linspace(float(bounds[0, 0]), float(bounds[1, 0]), n_grid, dtype=torch.float64)
311
+ X_grid = mid.unsqueeze(0).repeat(n_grid, 1)
312
+ X_grid[:, 0] = x_range
313
+
314
+ mean, var = surrogate.predict(X_grid)
315
+ std = var.sqrt()
316
+
317
+ fig_surrogate, ax = plt.subplots(figsize=(8, 5))
318
+ x_np = x_range.numpy()
319
+ m_np = mean.squeeze().detach().numpy()
320
+ s_np = std.squeeze().detach().numpy()
321
+
322
+ ax.plot(x_np, m_np, "b-", lw=2, label="Surrogate mean")
323
+ ax.fill_between(x_np, m_np - 2 * s_np, m_np + 2 * s_np, alpha=0.2, color="blue", label="95% CI")
324
+
325
+ # Physics model line
326
+ phys_np = physics_fn(X_grid).detach().numpy()
327
+ ax.plot(x_np, phys_np, "r--", lw=1.5, label="Physics model")
328
+
329
+ # Observed data projected onto this slice
330
+ if X_init is not None:
331
+ ax.scatter(X_init[:, 0].numpy(), y_init.squeeze().numpy(), c="red", s=40, zorder=5, edgecolors="k", label="Initial data")
332
+
333
+ ax.set_xlabel(param_names[0])
334
+ ax.set_ylabel("Objective")
335
+ ax.set_title(f"Surrogate vs Physics (slice along {param_names[0]})")
336
+ ax.legend()
337
+ ax.grid(True, alpha=0.3)
338
+ fig_surrogate.tight_layout()
339
+ except Exception:
340
+ fig_surrogate = None
341
+
342
+ # ── Format outputs ────────────────────────────────────────────────
343
+ log_text = "\n".join(log_lines)
344
+ best_text = (
345
+ f"**Best objective: {best['objective']:.4f}**\n\n"
346
+ f"Parameters:\n"
347
+ + "\n".join(f" - **{k}**: {v:.4f}" for k, v in best["parameters"].items())
348
+ )
349
+ summary_text = json.dumps(summary, indent=2, default=str)
350
+
351
+ return (
352
+ log_text,
353
+ best_text,
354
+ fig_conv,
355
+ fig_params,
356
+ fig_surrogate,
357
+ results_df.round(4).to_string(index=False),
358
+ summary_text,
359
+ )
360
+
361
+ except Exception as exc:
362
+ tb = traceback.format_exc()
363
+ err = f"**Error:** {exc}\n\n```\n{tb}\n```"
364
+ return err, err, None, None, None, "", ""
365
+
366
+
367
+ # ---------------------------------------------------------------------------
368
+ # Gradio interface
369
+ # ---------------------------------------------------------------------------
370
+
371
+ def on_template_change(template_name):
372
+ """Populate code and params when a built-in template is selected."""
373
+ info = BUILTIN_PHYSICS.get(template_name, {"code": "", "params": ""})
374
+ return info["code"], info["params"]
375
+
376
+
377
+ def build_app() -> gr.Blocks:
378
+ with gr.Blocks(
379
+ title="Physics-Informed Bayesian Optimization",
380
+ theme=gr.themes.Soft(),
381
+ ) as app:
382
+ gr.Markdown(
383
+ """
384
+ # ⚗️ Physics-Informed Bayesian Optimization Platform
385
+
386
+ Design experiments efficiently by combining **physics models** with
387
+ **Gaussian Process surrogates**. The physics model acts as a structured prior
388
+ (GP mean function), and the GP learns the residual — dramatically reducing
389
+ the number of experiments needed.
390
+
391
+ **Backends:** BoTorch · GPyTorch · AX · BoFire
392
+ """
393
+ )
394
+
395
+ with gr.Tabs():
396
+ # ── Tab 1: Setup ──────────────────────────────────────────────
397
+ with gr.TabItem("1 · Setup"):
398
+ with gr.Row():
399
+ with gr.Column(scale=1):
400
+ gr.Markdown("### Physics Model")
401
+ physics_template = gr.Dropdown(
402
+ choices=list(BUILTIN_PHYSICS.keys()),
403
+ value="Arrhenius Kinetics",
404
+ label="Built-in template",
405
+ )
406
+ physics_code = gr.Code(
407
+ value=BUILTIN_PHYSICS["Arrhenius Kinetics"]["code"],
408
+ language="python",
409
+ label="Physics model code (must define `physics_model(X)`)",
410
+ lines=14,
411
+ )
412
+
413
+ with gr.Column(scale=1):
414
+ gr.Markdown("### Parameter Space")
415
+ param_text = gr.Textbox(
416
+ value=BUILTIN_PHYSICS["Arrhenius Kinetics"]["params"],
417
+ label="Parameters (name: lower-upper, one per line)",
418
+ lines=6,
419
+ )
420
+
421
+ gr.Markdown("### Initial Data (optional)")
422
+ csv_file = gr.File(label="Upload CSV", file_types=[".csv"])
423
+ csv_text = gr.Textbox(
424
+ value="",
425
+ label="… or paste CSV text",
426
+ lines=5,
427
+ placeholder=DEMO_CSV,
428
+ )
429
+ objective_col = gr.Textbox(
430
+ value="",
431
+ label="Objective column name (leave blank → last column)",
432
+ )
433
+
434
+ physics_template.change(
435
+ on_template_change,
436
+ inputs=[physics_template],
437
+ outputs=[physics_code, param_text],
438
+ )
439
+
440
+ # ── Tab 2: Configure ──────────────────────────────────────────
441
+ with gr.TabItem("2 · Configure"):
442
+ with gr.Row():
443
+ acq_fn = gr.Dropdown(
444
+ choices=[
445
+ "Expected Improvement (EI)",
446
+ "Upper Confidence Bound (UCB)",
447
+ "Probability of Improvement (PI)",
448
+ "Physics-Informed EI",
449
+ ],
450
+ value="Expected Improvement (EI)",
451
+ label="Acquisition Function",
452
+ )
453
+ maximize = gr.Checkbox(value=True, label="Maximize objective")
454
+ with gr.Row():
455
+ n_initial = gr.Slider(3, 30, value=5, step=1, label="Initial samples (if no CSV)")
456
+ n_iterations = gr.Slider(5, 100, value=20, step=1, label="BO iterations")
457
+ batch_size = gr.Slider(1, 5, value=1, step=1, label="Batch size")
458
+ with gr.Row():
459
+ noise_var = gr.Slider(0.001, 1.0, value=0.01, step=0.001, label="Noise variance")
460
+ seed = gr.Number(value=42, label="Random seed", precision=0)
461
+
462
+ # ── Tab 3: Run & Results ──────────────��───────────────────────
463
+ with gr.TabItem("3 · Run & Results"):
464
+ run_btn = gr.Button("🚀 Run Optimization", variant="primary", size="lg")
465
+
466
+ with gr.Row():
467
+ best_md = gr.Markdown(label="Best Result")
468
+
469
+ with gr.Row():
470
+ convergence_plot = gr.Plot(label="Convergence")
471
+ params_plot = gr.Plot(label="Parameter Exploration")
472
+
473
+ with gr.Row():
474
+ surrogate_plot = gr.Plot(label="Surrogate vs Physics")
475
+
476
+ with gr.Accordion("Optimization log", open=False):
477
+ log_box = gr.Textbox(label="Log", lines=15, interactive=False)
478
+
479
+ with gr.Accordion("Full results table", open=False):
480
+ results_box = gr.Textbox(label="Results", lines=12, interactive=False)
481
+
482
+ with gr.Accordion("Campaign summary (JSON)", open=False):
483
+ summary_box = gr.Textbox(label="Summary", lines=10, interactive=False)
484
+
485
+ run_btn.click(
486
+ run_optimization,
487
+ inputs=[
488
+ physics_template,
489
+ physics_code,
490
+ param_text,
491
+ csv_file,
492
+ csv_text,
493
+ objective_col,
494
+ acq_fn,
495
+ n_initial,
496
+ n_iterations,
497
+ batch_size,
498
+ noise_var,
499
+ maximize,
500
+ seed,
501
+ ],
502
+ outputs=[
503
+ log_box,
504
+ best_md,
505
+ convergence_plot,
506
+ params_plot,
507
+ surrogate_plot,
508
+ results_box,
509
+ summary_box,
510
+ ],
511
+ )
512
+
513
+ # ── Tab 4: About ──────────────────────────────────────────────
514
+ with gr.TabItem("About"):
515
+ gr.Markdown(
516
+ """
517
+ ## How it works
518
+
519
+ Traditional Bayesian optimisation uses a GP with a flat (constant) mean.
520
+ This platform **replaces the mean with a physics model**:
521
+
522
+ $$f(x) = \\phi(x) + \\varepsilon(x)$$
523
+
524
+ where $\\phi(x)$ is the physics model and
525
+ $\\varepsilon(x) \\sim \\mathcal{GP}(0,\\, k(x,x'))$ captures the
526
+ residual (model discrepancy + noise).
527
+
528
+ ### Benefits
529
+ - **Sample efficiency** — physics captures the trend; the GP only
530
+ learns small deviations.
531
+ - **Extrapolation** — physics provides reasonable predictions
532
+ outside observed data.
533
+ - **Constraint awareness** — physical constraints steer the
534
+ search toward feasible regions.
535
+ - **Graceful degradation** — works physics-only (no data),
536
+ hybrid, or pure GP.
537
+
538
+ ### Surrogate mode selection
539
+
540
+ | Data | Physics model | Mode |
541
+ |------|--------------|------|
542
+ | None | ✓ | `physics_only` |
543
+ | < 20 | ✓ | `physics_as_mean` |
544
+ | 20-50 | ✓ | `weighted_ensemble` |
545
+ | Any | ✗ | `gp_only` |
546
+
547
+ ### Stack
548
+ **PyTorch** · **GPyTorch** · **BoTorch** · AX Platform · BoFire
549
+
550
+ ---
551
+ *Built by Plinity — infinite recyclable polymers*
552
+ """
553
+ )
554
+
555
+ return app
556
+
557
+
558
+ # ---------------------------------------------------------------------------
559
+ # Entry point
560
+ # ---------------------------------------------------------------------------
561
+
562
+ app = build_app()
563
+
564
+ if __name__ == "__main__":
565
+ app.launch()