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

Upload experiment/campaign.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. experiment/campaign.py +262 -0
experiment/campaign.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OptimizationCampaign: manages the full lifecycle of an optimization campaign."""
2
+
3
+ import json
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Callable, Dict, List, Optional, Tuple
8
+
9
+ import torch
10
+ from torch import Tensor
11
+ import pandas as pd
12
+
13
+ from physics_informed_bo.config import OptimizationConfig
14
+ from physics_informed_bo.experiment.designer import ExperimentDesigner
15
+ from physics_informed_bo.experiment.parameter_space import ParameterSpace
16
+
17
+
18
+ @dataclass
19
+ class ExperimentRecord:
20
+ """Record of a single experiment."""
21
+
22
+ iteration: int
23
+ parameters: Dict[str, float]
24
+ objective: float
25
+ timestamp: float = field(default_factory=time.time)
26
+ metadata: Dict = field(default_factory=dict)
27
+
28
+
29
+ class OptimizationCampaign:
30
+ """Manages an end-to-end Bayesian optimization campaign.
31
+
32
+ Provides:
33
+ - Full experiment tracking and history
34
+ - Save/load campaign state
35
+ - Convergence monitoring
36
+ - Human-in-the-loop workflow support
37
+ - Export to DataFrame for analysis
38
+
39
+ Example:
40
+ campaign = OptimizationCampaign(
41
+ name="polymer_optimization",
42
+ parameter_space=space,
43
+ physics_fn=my_physics_model,
44
+ config=OptimizationConfig(max_iterations=30),
45
+ )
46
+
47
+ # Automated loop
48
+ campaign.run_automated(objective_fn=evaluate_experiment)
49
+
50
+ # Or human-in-the-loop
51
+ next_exp = campaign.suggest_next()
52
+ # ... run experiment manually ...
53
+ campaign.report_result(next_exp, result_value)
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ name: str,
59
+ parameter_space: ParameterSpace,
60
+ physics_fn: Optional[Callable[[Tensor], Tensor]] = None,
61
+ initial_data: Optional[Tuple[Tensor, Tensor]] = None,
62
+ config: Optional[OptimizationConfig] = None,
63
+ maximize: bool = True,
64
+ ):
65
+ self.name = name
66
+ self.maximize = maximize
67
+ self.config = config or OptimizationConfig()
68
+ self.parameter_space = parameter_space
69
+
70
+ self._designer = ExperimentDesigner(
71
+ parameter_space=parameter_space,
72
+ physics_fn=physics_fn,
73
+ initial_data=initial_data,
74
+ config=self.config,
75
+ )
76
+
77
+ self._history: List[ExperimentRecord] = []
78
+ self._iteration = 0
79
+ self._start_time = time.time()
80
+
81
+ # Track initial data if provided
82
+ if initial_data is not None:
83
+ X_init, y_init = initial_data
84
+ if y_init.dim() == 1:
85
+ y_init = y_init.unsqueeze(-1)
86
+ param_dicts = parameter_space.to_dict(X_init)
87
+ for params, y_val in zip(param_dicts, y_init):
88
+ self._history.append(
89
+ ExperimentRecord(
90
+ iteration=0,
91
+ parameters=params,
92
+ objective=float(y_val),
93
+ metadata={"source": "initial_data"},
94
+ )
95
+ )
96
+
97
+ def suggest_next(self, n: int = 1) -> List[Dict]:
98
+ """Suggest the next experiment(s) to run.
99
+
100
+ Returns:
101
+ List of parameter dicts for suggested experiments.
102
+ """
103
+ self._iteration += 1
104
+ candidates = self._designer.suggest(n)
105
+ return self.parameter_space.to_dict(candidates)
106
+
107
+ def report_result(
108
+ self,
109
+ parameters: Dict[str, float],
110
+ objective: float,
111
+ metadata: Optional[Dict] = None,
112
+ ) -> None:
113
+ """Report the result of a completed experiment.
114
+
115
+ Args:
116
+ parameters: The parameter values that were tested.
117
+ objective: The measured objective value.
118
+ metadata: Optional metadata about the experiment.
119
+ """
120
+ record = ExperimentRecord(
121
+ iteration=self._iteration,
122
+ parameters=parameters,
123
+ objective=objective,
124
+ metadata=metadata or {},
125
+ )
126
+ self._history.append(record)
127
+
128
+ # Update the designer
129
+ X_new = self.parameter_space.from_dict(parameters).unsqueeze(0)
130
+ y_new = torch.tensor([[objective]], dtype=torch.float64)
131
+ self._designer.update(X_new, y_new)
132
+
133
+ def run_automated(
134
+ self,
135
+ objective_fn: Callable[[Dict[str, float]], float],
136
+ max_iterations: Optional[int] = None,
137
+ batch_size: int = 1,
138
+ callback: Optional[Callable] = None,
139
+ ) -> pd.DataFrame:
140
+ """Run a fully automated optimization loop.
141
+
142
+ Args:
143
+ objective_fn: Function that takes parameter dict and returns objective value.
144
+ max_iterations: Max iterations (defaults to config.max_iterations).
145
+ batch_size: Number of experiments per iteration.
146
+ callback: Optional callback(iteration, best_so_far) called each iteration.
147
+
148
+ Returns:
149
+ DataFrame of all experiments.
150
+ """
151
+ max_iter = max_iterations or self.config.max_iterations
152
+
153
+ for i in range(max_iter):
154
+ # Suggest experiments
155
+ suggestions = self.suggest_next(batch_size)
156
+
157
+ # Evaluate
158
+ for params in suggestions:
159
+ objective = objective_fn(params)
160
+ self.report_result(params, objective)
161
+
162
+ # Callback
163
+ if callback:
164
+ best = self.get_best()
165
+ callback(i + 1, best)
166
+
167
+ # Check convergence
168
+ if self._check_convergence():
169
+ break
170
+
171
+ return self.to_dataframe()
172
+
173
+ def _check_convergence(self, window: int = 10, tolerance: float = 1e-4) -> bool:
174
+ """Check if optimization has converged (no improvement in last `window` iterations)."""
175
+ if len(self._history) < window:
176
+ return False
177
+
178
+ recent = [r.objective for r in self._history[-window:]]
179
+ if self.maximize:
180
+ best_recent = max(recent)
181
+ best_before = max(r.objective for r in self._history[:-window])
182
+ return best_recent - best_before < tolerance
183
+ else:
184
+ best_recent = min(recent)
185
+ best_before = min(r.objective for r in self._history[:-window])
186
+ return best_before - best_recent < tolerance
187
+
188
+ def get_best(self) -> Dict:
189
+ """Get the best experiment so far."""
190
+ if not self._history:
191
+ return {"parameters": {}, "objective": None}
192
+
193
+ if self.maximize:
194
+ best = max(self._history, key=lambda r: r.objective)
195
+ else:
196
+ best = min(self._history, key=lambda r: r.objective)
197
+
198
+ return {"parameters": best.parameters, "objective": best.objective}
199
+
200
+ def to_dataframe(self) -> pd.DataFrame:
201
+ """Export campaign history as a pandas DataFrame."""
202
+ records = []
203
+ for r in self._history:
204
+ row = {"iteration": r.iteration, "objective": r.objective}
205
+ row.update(r.parameters)
206
+ row["timestamp"] = r.timestamp
207
+ records.append(row)
208
+ return pd.DataFrame(records)
209
+
210
+ def save(self, filepath: str) -> None:
211
+ """Save campaign state to a JSON file."""
212
+ state = {
213
+ "name": self.name,
214
+ "maximize": self.maximize,
215
+ "iteration": self._iteration,
216
+ "history": [
217
+ {
218
+ "iteration": r.iteration,
219
+ "parameters": r.parameters,
220
+ "objective": r.objective,
221
+ "timestamp": r.timestamp,
222
+ "metadata": r.metadata,
223
+ }
224
+ for r in self._history
225
+ ],
226
+ }
227
+ Path(filepath).write_text(json.dumps(state, indent=2))
228
+
229
+ def load(self, filepath: str) -> None:
230
+ """Load campaign state from a JSON file."""
231
+ state = json.loads(Path(filepath).read_text())
232
+ self.name = state["name"]
233
+ self.maximize = state["maximize"]
234
+ self._iteration = state["iteration"]
235
+ self._history = [
236
+ ExperimentRecord(**r) for r in state["history"]
237
+ ]
238
+
239
+ # Re-feed all data to the designer
240
+ if self._history:
241
+ all_params = [r.parameters for r in self._history]
242
+ X = torch.stack([self.parameter_space.from_dict(p) for p in all_params])
243
+ y = torch.tensor(
244
+ [r.objective for r in self._history], dtype=torch.float64
245
+ ).unsqueeze(-1)
246
+ self._designer.update(X, y)
247
+
248
+ @property
249
+ def n_experiments(self) -> int:
250
+ return len(self._history)
251
+
252
+ def summary(self) -> Dict:
253
+ """Campaign summary."""
254
+ best = self.get_best()
255
+ return {
256
+ "name": self.name,
257
+ "n_experiments": self.n_experiments,
258
+ "iteration": self._iteration,
259
+ "best": best,
260
+ "elapsed_time_s": time.time() - self._start_time,
261
+ "model_summary": self._designer.summary(),
262
+ }