ravimohan19 commited on
Commit
e4ccd4f
·
verified ·
1 Parent(s): aa6c18e

Upload experiment/designer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. experiment/designer.py +241 -0
experiment/designer.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ExperimentDesigner: the main entry point for designing experiments."""
2
+
3
+ from typing import Callable, Dict, List, Optional, Tuple
4
+
5
+ import torch
6
+ from torch import Tensor
7
+
8
+ from physics_informed_bo.config import OptimizationConfig, OptimizerBackend
9
+ from physics_informed_bo.experiment.parameter_space import ParameterSpace
10
+ from physics_informed_bo.models.hybrid_model import HybridSurrogate
11
+ from physics_informed_bo.priors.prior_manager import PriorManager
12
+ from physics_informed_bo.priors.data_prior import DataPrior
13
+ from physics_informed_bo.priors.physics_prior import PhysicsPrior
14
+ from physics_informed_bo.optimizers.factory import create_optimizer
15
+ from physics_informed_bo.optimizers.base_optimizer import BaseOptimizer
16
+
17
+
18
+ class ExperimentDesigner:
19
+ """High-level API for physics-informed Bayesian experiment design.
20
+
21
+ This is the main user-facing class. It orchestrates:
22
+ 1. Parameter space definition
23
+ 2. Physics and data prior management
24
+ 3. Surrogate model selection and fitting
25
+ 4. Acquisition function optimization
26
+ 5. Experiment suggestion
27
+
28
+ Example:
29
+ designer = ExperimentDesigner(
30
+ parameter_space=space,
31
+ physics_fn=arrhenius_model,
32
+ initial_data=(X_init, y_init),
33
+ )
34
+
35
+ # Get next experiment suggestions
36
+ next_experiments = designer.suggest(n=3)
37
+
38
+ # After running experiments, update with results
39
+ designer.update(X_new, y_new)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ parameter_space: ParameterSpace,
45
+ physics_fn: Optional[Callable[[Tensor], Tensor]] = None,
46
+ initial_data: Optional[Tuple[Tensor, Tensor]] = None,
47
+ config: Optional[OptimizationConfig] = None,
48
+ physics_constraints: Optional[List[Dict]] = None,
49
+ ):
50
+ """
51
+ Args:
52
+ parameter_space: The experimental parameter space.
53
+ physics_fn: Optional physics model function.
54
+ initial_data: Optional tuple of (X, y) initial observations.
55
+ config: Optimization configuration. Defaults to sensible settings.
56
+ physics_constraints: Optional list of physics constraint dicts.
57
+ """
58
+ self.parameter_space = parameter_space
59
+ self.config = config or OptimizationConfig()
60
+
61
+ # Set up physics prior
62
+ physics_prior = None
63
+ if physics_fn is not None:
64
+ physics_prior = PhysicsPrior(physics_fn=physics_fn)
65
+ if physics_constraints:
66
+ for c in physics_constraints:
67
+ physics_prior.add_constraint(**c)
68
+
69
+ # Set up data prior
70
+ data_prior = DataPrior()
71
+ if initial_data is not None:
72
+ X_init, y_init = initial_data
73
+ if y_init.dim() == 1:
74
+ y_init = y_init.unsqueeze(-1)
75
+ data_prior.X = X_init
76
+ data_prior.y = y_init
77
+ data_prior.feature_names = parameter_space.parameter_names
78
+
79
+ # Prior manager
80
+ self.prior_manager = PriorManager(
81
+ physics_prior=physics_prior,
82
+ data_prior=data_prior,
83
+ )
84
+
85
+ # Build surrogate
86
+ self._surrogate: Optional[HybridSurrogate] = None
87
+ self._optimizer: Optional[BaseOptimizer] = None
88
+ self._iteration = 0
89
+
90
+ # Initialize if we have enough data
91
+ self._initialize()
92
+
93
+ def _initialize(self) -> None:
94
+ """Initialize surrogate model and optimizer."""
95
+ try:
96
+ mode = self.prior_manager.recommend_surrogate_mode()
97
+ except ValueError:
98
+ # Not enough data or physics model
99
+ return
100
+
101
+ self._surrogate = self.prior_manager.build_surrogate(
102
+ mode=mode,
103
+ kernel="matern",
104
+ noise_variance=self.config.noise_variance,
105
+ device=self.config.device,
106
+ )
107
+
108
+ # Set up optimizer
109
+ self._optimizer = create_optimizer(self.config)
110
+ self._optimizer.set_surrogate(self._surrogate)
111
+ self._optimizer.set_bounds(self.parameter_space.bounds)
112
+
113
+ if self.prior_manager.physics_prior:
114
+ self._optimizer.set_physics_prior(self.prior_manager.physics_prior)
115
+
116
+ def suggest(self, n: int = 1) -> Tensor:
117
+ """Suggest the next n experiments to run.
118
+
119
+ If not enough data exists for GP-based suggestion, falls back to:
120
+ 1. Physics-guided sampling (if physics model available)
121
+ 2. Latin Hypercube sampling (space-filling design)
122
+
123
+ Args:
124
+ n: Number of experiments to suggest.
125
+
126
+ Returns:
127
+ Tensor of shape (n, d) with suggested parameter values.
128
+ """
129
+ self._iteration += 1
130
+
131
+ # Not enough data for BO: use initial design
132
+ if self._surrogate is None or self.prior_manager.data_prior.n_observations < 3:
133
+ return self._initial_design(n)
134
+
135
+ # Re-fit surrogate with latest data
136
+ data = self.prior_manager.data_prior
137
+ if data.n_observations >= 3:
138
+ self._surrogate.fit(data.X, data.y)
139
+ self._optimizer.set_surrogate(self._surrogate)
140
+
141
+ # Suggest via optimizer
142
+ candidates = self._optimizer.suggest(
143
+ n_candidates=n,
144
+ X_observed=data.X,
145
+ y_observed=data.y,
146
+ )
147
+
148
+ return candidates
149
+
150
+ def _initial_design(self, n: int) -> Tensor:
151
+ """Generate initial design points when insufficient data for BO.
152
+
153
+ Uses physics model to prioritize promising regions if available.
154
+ """
155
+ if self.prior_manager.physics_prior is not None:
156
+ # Sample candidates and pick those with best physics predictions
157
+ n_candidates = max(n * 20, 200)
158
+ candidates = self.parameter_space.sample_latin_hypercube(n_candidates)
159
+
160
+ # Filter by physics constraints
161
+ candidates = self.prior_manager.physics_prior.get_feasible_subset(candidates)
162
+ if len(candidates) < n:
163
+ candidates = self.parameter_space.sample_latin_hypercube(n_candidates)
164
+
165
+ # Rank by physics model prediction
166
+ with torch.no_grad():
167
+ physics_scores = self.prior_manager.physics_prior.evaluate(candidates)
168
+
169
+ # Select top-n diverse points (greedy furthest-point selection)
170
+ selected = self._select_diverse_top_k(candidates, physics_scores, n)
171
+ return selected
172
+ else:
173
+ return self.parameter_space.sample_latin_hypercube(n)
174
+
175
+ def _select_diverse_top_k(
176
+ self, X: Tensor, scores: Tensor, k: int, top_fraction: float = 0.3
177
+ ) -> Tensor:
178
+ """Select k diverse points from the top-scoring candidates."""
179
+ # Pre-filter to top fraction
180
+ n_top = max(k * 3, int(len(X) * top_fraction))
181
+ top_idx = scores.argsort(descending=True)[:n_top]
182
+ X_top = X[top_idx]
183
+
184
+ # Greedy furthest-point selection for diversity
185
+ selected_idx = [0]
186
+ for _ in range(k - 1):
187
+ dists = torch.cdist(X_top, X_top[selected_idx]).min(dim=1).values
188
+ next_idx = dists.argmax().item()
189
+ selected_idx.append(next_idx)
190
+
191
+ return X_top[selected_idx]
192
+
193
+ def update(self, X_new: Tensor, y_new: Tensor) -> None:
194
+ """Update the designer with new experimental observations.
195
+
196
+ Args:
197
+ X_new: New input observations (n, d).
198
+ y_new: New output observations (n, 1) or (n,).
199
+ """
200
+ if y_new.dim() == 1:
201
+ y_new = y_new.unsqueeze(-1)
202
+
203
+ self.prior_manager.update_with_observations(X_new, y_new)
204
+
205
+ # Re-initialize if we now have enough data
206
+ if self._surrogate is None and self.prior_manager.data_prior.n_observations >= 3:
207
+ self._initialize()
208
+
209
+ def get_best(self, maximize: bool = True) -> Dict:
210
+ """Get the best observation so far."""
211
+ X_best, y_best = self.prior_manager.data_prior.get_best(maximize)
212
+ params = self.parameter_space.to_dict(X_best.unsqueeze(0))[0]
213
+ return {"parameters": params, "objective": float(y_best)}
214
+
215
+ def predict(self, X: Tensor) -> Tuple[Tensor, Tensor]:
216
+ """Get surrogate model predictions at X."""
217
+ if self._surrogate is None:
218
+ if self.prior_manager.physics_prior:
219
+ pred = self.prior_manager.physics_prior.evaluate(X)
220
+ return pred.unsqueeze(-1), torch.ones_like(pred.unsqueeze(-1)) * 0.1
221
+ raise RuntimeError("No surrogate model fitted yet.")
222
+ return self._surrogate.predict(X)
223
+
224
+ def model_quality(self) -> Dict:
225
+ """Assess current surrogate model quality."""
226
+ if self._surrogate is None:
227
+ return {"status": "no_model"}
228
+ return self._surrogate.physics_model_quality()
229
+
230
+ def summary(self) -> Dict:
231
+ """Get a summary of the current optimization state."""
232
+ return {
233
+ "iteration": self._iteration,
234
+ "n_observations": self.prior_manager.data_prior.n_observations,
235
+ "prior_summary": self.prior_manager.summary(),
236
+ "model_quality": self.model_quality(),
237
+ "parameter_space": {
238
+ "dimension": self.parameter_space.dimension,
239
+ "parameters": self.parameter_space.parameter_names,
240
+ },
241
+ }