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

Upload experiment/parameter_space.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. experiment/parameter_space.py +209 -0
experiment/parameter_space.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Parameter space definitions for experiment design."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, List, Optional, Tuple, Union
5
+
6
+ import torch
7
+ from torch import Tensor
8
+ import numpy as np
9
+
10
+
11
+ @dataclass
12
+ class ContinuousParameter:
13
+ """A continuous real-valued parameter."""
14
+
15
+ name: str
16
+ lower: float
17
+ upper: float
18
+ log_scale: bool = False # Use log-scale for parameters spanning orders of magnitude
19
+ units: str = ""
20
+
21
+ def sample(self, n: int = 1, dtype=torch.float64) -> Tensor:
22
+ if self.log_scale:
23
+ log_samples = torch.rand(n, dtype=dtype) * (
24
+ np.log(self.upper) - np.log(self.lower)
25
+ ) + np.log(self.lower)
26
+ return log_samples.exp()
27
+ return torch.rand(n, dtype=dtype) * (self.upper - self.lower) + self.lower
28
+
29
+
30
+ @dataclass
31
+ class IntegerParameter:
32
+ """An integer-valued parameter."""
33
+
34
+ name: str
35
+ lower: int
36
+ upper: int
37
+ units: str = ""
38
+
39
+ def sample(self, n: int = 1, dtype=torch.float64) -> Tensor:
40
+ return torch.randint(self.lower, self.upper + 1, (n,)).to(dtype=dtype)
41
+
42
+
43
+ @dataclass
44
+ class CategoricalParameter:
45
+ """A categorical parameter with discrete choices."""
46
+
47
+ name: str
48
+ categories: List[str]
49
+ units: str = ""
50
+
51
+ def sample(self, n: int = 1, dtype=torch.float64) -> Tensor:
52
+ indices = torch.randint(0, len(self.categories), (n,))
53
+ return indices.to(dtype=dtype)
54
+
55
+ def encode(self, category: str) -> int:
56
+ return self.categories.index(category)
57
+
58
+ def decode(self, index: int) -> str:
59
+ return self.categories[index]
60
+
61
+
62
+ class ParameterSpace:
63
+ """Defines the experimental parameter space for optimization.
64
+
65
+ Supports continuous, integer, and categorical parameters with
66
+ optional linear constraints between parameters.
67
+ """
68
+
69
+ def __init__(self):
70
+ self._parameters: Dict[str, Union[ContinuousParameter, IntegerParameter, CategoricalParameter]] = {}
71
+ self._order: List[str] = []
72
+ self._constraints: List[Dict] = []
73
+
74
+ def add_continuous(
75
+ self,
76
+ name: str,
77
+ lower: float,
78
+ upper: float,
79
+ log_scale: bool = False,
80
+ units: str = "",
81
+ ) -> "ParameterSpace":
82
+ """Add a continuous parameter."""
83
+ self._parameters[name] = ContinuousParameter(name, lower, upper, log_scale, units)
84
+ self._order.append(name)
85
+ return self
86
+
87
+ def add_integer(
88
+ self, name: str, lower: int, upper: int, units: str = ""
89
+ ) -> "ParameterSpace":
90
+ """Add an integer parameter."""
91
+ self._parameters[name] = IntegerParameter(name, lower, upper, units)
92
+ self._order.append(name)
93
+ return self
94
+
95
+ def add_categorical(
96
+ self, name: str, categories: List[str], units: str = ""
97
+ ) -> "ParameterSpace":
98
+ """Add a categorical parameter."""
99
+ self._parameters[name] = CategoricalParameter(name, categories, units)
100
+ self._order.append(name)
101
+ return self
102
+
103
+ def add_sum_constraint(
104
+ self, parameter_names: List[str], target_sum: float = 1.0
105
+ ) -> "ParameterSpace":
106
+ """Add a constraint that parameters must sum to a target value.
107
+
108
+ Useful for mixture/composition experiments.
109
+ """
110
+ self._constraints.append({
111
+ "type": "sum",
112
+ "parameters": parameter_names,
113
+ "target": target_sum,
114
+ })
115
+ return self
116
+
117
+ def add_linear_constraint(
118
+ self,
119
+ parameter_names: List[str],
120
+ coefficients: List[float],
121
+ bound: float,
122
+ constraint_type: str = "<=",
123
+ ) -> "ParameterSpace":
124
+ """Add a linear constraint: sum(coeff_i * param_i) <= bound."""
125
+ self._constraints.append({
126
+ "type": "linear",
127
+ "parameters": parameter_names,
128
+ "coefficients": coefficients,
129
+ "bound": bound,
130
+ "constraint_type": constraint_type,
131
+ })
132
+ return self
133
+
134
+ @property
135
+ def dimension(self) -> int:
136
+ return len(self._parameters)
137
+
138
+ @property
139
+ def parameter_names(self) -> List[str]:
140
+ return self._order
141
+
142
+ @property
143
+ def bounds(self) -> Tensor:
144
+ """Get bounds as a (2, d) tensor for BoTorch."""
145
+ lowers, uppers = [], []
146
+ for name in self._order:
147
+ p = self._parameters[name]
148
+ if isinstance(p, ContinuousParameter):
149
+ lowers.append(p.lower)
150
+ uppers.append(p.upper)
151
+ elif isinstance(p, IntegerParameter):
152
+ lowers.append(float(p.lower))
153
+ uppers.append(float(p.upper))
154
+ elif isinstance(p, CategoricalParameter):
155
+ lowers.append(0.0)
156
+ uppers.append(float(len(p.categories) - 1))
157
+ return torch.tensor([lowers, uppers], dtype=torch.float64)
158
+
159
+ def sample_random(self, n: int = 1, dtype=torch.float64) -> Tensor:
160
+ """Generate random samples from the parameter space."""
161
+ samples = []
162
+ for name in self._order:
163
+ samples.append(self._parameters[name].sample(n, dtype))
164
+ return torch.stack(samples, dim=-1)
165
+
166
+ def sample_latin_hypercube(self, n: int, dtype=torch.float64) -> Tensor:
167
+ """Generate Latin Hypercube samples for space-filling initial design."""
168
+ d = self.dimension
169
+ # Create LHS grid
170
+ intervals = torch.linspace(0, 1, n + 1)
171
+ samples = torch.zeros(n, d, dtype=dtype)
172
+
173
+ for j in range(d):
174
+ # Random permutation within each dimension
175
+ perm = torch.randperm(n)
176
+ for i in range(n):
177
+ low = intervals[perm[i]]
178
+ high = intervals[perm[i] + 1]
179
+ samples[i, j] = low + (high - low) * torch.rand(1, dtype=dtype)
180
+
181
+ # Scale to parameter bounds
182
+ bounds = self.bounds
183
+ samples = samples * (bounds[1] - bounds[0]) + bounds[0]
184
+ return samples
185
+
186
+ def to_dict(self, X: Tensor) -> List[Dict]:
187
+ """Convert a tensor of parameter values to list of dicts."""
188
+ results = []
189
+ for i in range(len(X)):
190
+ d = {}
191
+ for j, name in enumerate(self._order):
192
+ p = self._parameters[name]
193
+ if isinstance(p, CategoricalParameter):
194
+ d[name] = p.decode(int(X[i, j].item()))
195
+ else:
196
+ d[name] = X[i, j].item()
197
+ results.append(d)
198
+ return results
199
+
200
+ def from_dict(self, params: Dict[str, float], dtype=torch.float64) -> Tensor:
201
+ """Convert a parameter dict to a tensor row."""
202
+ values = []
203
+ for name in self._order:
204
+ p = self._parameters[name]
205
+ if isinstance(p, CategoricalParameter):
206
+ values.append(float(p.encode(params[name])))
207
+ else:
208
+ values.append(float(params[name]))
209
+ return torch.tensor(values, dtype=dtype)