qualia_core_db/inference/lab/
config_space.rs1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum ParameterDef {
14 Int { lo: i64, hi: i64 },
16 Float { lo: f64, hi: f64 },
18 Bool,
20 Categorical { choices: Vec<String> },
22}
23
24impl ParameterDef {
25 pub fn clamp(&self, raw: &ParameterValue) -> ParameterValue {
27 match (self, raw) {
28 (ParameterDef::Int { lo, hi }, ParameterValue::Int(v)) => {
29 ParameterValue::Int((*v).clamp(*lo, *hi))
30 }
31 (ParameterDef::Float { lo, hi }, ParameterValue::Float(v)) => {
32 ParameterValue::Float(v.clamp(*lo, *hi))
33 }
34 (ParameterDef::Bool, ParameterValue::Bool(v)) => ParameterValue::Bool(*v),
35 (ParameterDef::Categorical { choices }, ParameterValue::String(v)) => {
36 if choices.iter().any(|c| c == v) {
37 ParameterValue::String(v.clone())
38 } else {
39 ParameterValue::String(choices.first().cloned().unwrap_or_default())
40 }
41 }
42 _ => raw.clone(),
43 }
44 }
45
46 pub fn is_valid(&self, val: &ParameterValue) -> bool {
48 match (self, val) {
49 (ParameterDef::Int { lo, hi }, ParameterValue::Int(v)) => v >= lo && v <= hi,
50 (ParameterDef::Float { lo, hi }, ParameterValue::Float(v)) => v >= lo && v <= hi,
51 (ParameterDef::Bool, ParameterValue::Bool(_)) => true,
52 (ParameterDef::Categorical { choices }, ParameterValue::String(v)) => {
53 choices.iter().any(|c| c == v)
54 }
55 _ => false,
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub enum ParameterValue {
63 Int(i64),
64 Float(f64),
65 Bool(bool),
66 String(String),
67}
68
69impl ParameterValue {
70 pub fn normalize(&self, def: &ParameterDef) -> f64 {
72 match (def, self) {
73 (ParameterDef::Int { lo, hi }, ParameterValue::Int(v)) => {
74 if hi == lo {
75 0.5
76 } else {
77 (*v as f64 - *lo as f64) / (*hi as f64 - *lo as f64)
78 }
79 }
80 (ParameterDef::Float { lo, hi }, ParameterValue::Float(v)) => {
81 if hi == lo {
82 0.5
83 } else {
84 (v - lo) / (hi - lo)
85 }
86 }
87 (ParameterDef::Bool, ParameterValue::Bool(v)) => {
88 if *v {
89 1.0
90 } else {
91 0.0
92 }
93 }
94 (ParameterDef::Categorical { choices }, ParameterValue::String(v)) => {
95 let idx = choices.iter().position(|c| c == v).unwrap_or(0);
96 if choices.len() <= 1 {
97 0.5
98 } else {
99 idx as f64 / (choices.len() - 1) as f64
100 }
101 }
102 _ => 0.5,
103 }
104 }
105
106 pub fn denormalize(t: f64, def: &ParameterDef) -> ParameterValue {
108 let t = t.clamp(0.0, 1.0);
109 match def {
110 ParameterDef::Int { lo, hi } => {
111 let v = lo + ((t * (*hi - *lo) as f64).round() as i64);
112 ParameterValue::Int(v.clamp(*lo, *hi))
113 }
114 ParameterDef::Float { lo, hi } => ParameterValue::Float(lo + t * (hi - lo)),
115 ParameterDef::Bool => ParameterValue::Bool(t >= 0.5),
116 ParameterDef::Categorical { choices } => {
117 if choices.is_empty() {
118 return ParameterValue::String(String::new());
119 }
120 let idx = if choices.len() == 1 {
121 0
122 } else {
123 ((t * (choices.len() - 1) as f64).round() as usize).min(choices.len() - 1)
124 };
125 ParameterValue::String(choices[idx].clone())
126 }
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct ConfigurationSpace {
134 pub name: String,
135 pub params: BTreeMap<String, ParameterDef>,
136}
137
138impl ConfigurationSpace {
139 pub fn new(name: impl Into<String>) -> Self {
140 Self {
141 name: name.into(),
142 params: BTreeMap::new(),
143 }
144 }
145
146 pub fn with(mut self, name: impl Into<String>, def: ParameterDef) -> Self {
147 self.params.insert(name.into(), def);
148 self
149 }
150
151 pub fn dims(&self) -> usize {
153 self.params.len()
154 }
155
156 pub fn default_config(&self) -> Configuration {
159 let values: BTreeMap<String, ParameterValue> = self
160 .params
161 .iter()
162 .map(|(name, def)| {
163 let val = match def {
164 ParameterDef::Int { lo, .. } => ParameterValue::Int(*lo),
165 ParameterDef::Float { lo, .. } => ParameterValue::Float(*lo),
166 ParameterDef::Bool => ParameterValue::Bool(false),
167 ParameterDef::Categorical { choices } => {
168 ParameterValue::String(choices.first().cloned().unwrap_or_default())
169 }
170 };
171 (name.clone(), val)
172 })
173 .collect();
174 Configuration {
175 space_name: self.name.clone(),
176 values,
177 }
178 }
179
180 pub fn build_config(&self, values: BTreeMap<String, ParameterValue>) -> Configuration {
182 let clamped = values
183 .iter()
184 .map(|(k, v)| {
185 let cv = self
186 .params
187 .get(k)
188 .map(|d| d.clamp(v))
189 .unwrap_or_else(|| v.clone());
190 (k.clone(), cv)
191 })
192 .collect();
193 Configuration {
194 space_name: self.name.clone(),
195 values: clamped,
196 }
197 }
198
199 pub fn build_from_normalized(&self, t: &[f64]) -> Configuration {
201 let mut values = BTreeMap::new();
202 for (i, (name, def)) in self.params.iter().enumerate() {
203 let t_i = t.get(i).copied().unwrap_or(0.5);
204 values.insert(name.clone(), ParameterValue::denormalize(t_i, def));
205 }
206 Configuration {
207 space_name: self.name.clone(),
208 values,
209 }
210 }
211
212 pub fn normalize_config(&self, cfg: &Configuration) -> Vec<f64> {
214 self.params
215 .iter()
216 .map(|(name, def)| {
217 cfg.values
218 .get(name)
219 .map(|v| v.normalize(def))
220 .unwrap_or(0.5)
221 })
222 .collect()
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct Configuration {
229 pub space_name: String,
230 pub values: BTreeMap<String, ParameterValue>,
231}
232
233impl Configuration {
234 pub fn to_cbor(&self) -> Vec<u8> {
236 let mut buf = Vec::new();
237 let _ = ciborium::into_writer(self, &mut buf);
238 buf
239 }
240
241 pub fn from_cbor(data: &[u8]) -> Result<Self, String> {
243 ciborium::from_reader(data).map_err(|e| e.to_string())
244 }
245
246 pub fn hash(&self) -> u64 {
248 let data = self.to_cbor();
249 let mut h: u64 = 0xcbf29ce484222325;
250 for &b in &data {
251 h ^= b as u64;
252 h = h.wrapping_mul(0x100000001b3);
253 }
254 h
255 }
256
257 pub fn get(&self, name: &str) -> Option<&ParameterValue> {
259 self.values.get(name)
260 }
261
262 pub fn get_int(&self, name: &str) -> Option<i64> {
264 match self.values.get(name) {
265 Some(ParameterValue::Int(v)) => Some(*v),
266 _ => None,
267 }
268 }
269
270 pub fn get_float(&self, name: &str) -> Option<f64> {
272 match self.values.get(name) {
273 Some(ParameterValue::Float(v)) => Some(*v),
274 _ => None,
275 }
276 }
277
278 pub fn get_bool(&self, name: &str) -> Option<bool> {
280 match self.values.get(name) {
281 Some(ParameterValue::Bool(v)) => Some(*v),
282 _ => None,
283 }
284 }
285
286 pub fn get_string(&self, name: &str) -> Option<&str> {
288 match self.values.get(name) {
289 Some(ParameterValue::String(v)) => Some(v),
290 _ => None,
291 }
292 }
293
294 pub fn to_json(&self) -> String {
296 serde_json::to_string(self).unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"))
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn config_space_roundtrip() {
306 let space = ConfigurationSpace::new("test")
307 .with("ngram", ParameterDef::Int { lo: 1, hi: 3 })
308 .with("bias", ParameterDef::Float { lo: 0.5, hi: 5.0 })
309 .with("enabled", ParameterDef::Bool)
310 .with(
311 "mode",
312 ParameterDef::Categorical {
313 choices: vec!["fast".into(), "slow".into()],
314 },
315 );
316 assert_eq!(space.dims(), 4);
317
318 let cfg = space.build_from_normalized(&[0.5, 1.0, 0.0, 0.0]);
320 assert!((cfg.get_float("bias").unwrap() - 2.75).abs() < 0.01);
322 assert_eq!(cfg.get_bool("enabled"), Some(true));
324 assert_eq!(cfg.get_string("mode"), Some("fast"));
326 assert_eq!(cfg.get_int("ngram"), Some(1));
328 }
329
330 #[test]
331 fn config_hash_dedup() {
332 let space = ConfigurationSpace::new("test").with("x", ParameterDef::Int { lo: 0, hi: 10 });
333 let a = space.build_from_normalized(&[0.5]);
334 let b = space.build_from_normalized(&[0.5]);
335 let c = space.build_from_normalized(&[0.0]);
336 assert_eq!(a.hash(), b.hash());
337 assert_ne!(a.hash(), c.hash());
338 }
339
340 #[test]
341 fn config_cbor_roundtrip() {
342 let space = ConfigurationSpace::new("test")
343 .with("x", ParameterDef::Int { lo: 0, hi: 10 })
344 .with("y", ParameterDef::Bool);
345 let cfg = space.build_from_normalized(&[0.3, 0.6]);
346 let cbor = cfg.to_cbor();
347 let back = Configuration::from_cbor(&cbor).unwrap();
348 assert_eq!(back.get_int("x"), cfg.get_int("x"));
349 assert_eq!(back.get_bool("y"), cfg.get_bool("y"));
350 }
351
352 #[test]
353 fn clamp_works() {
354 let def = ParameterDef::Int { lo: 1, hi: 5 };
355 assert_eq!(def.clamp(&ParameterValue::Int(10)), ParameterValue::Int(5));
356 assert_eq!(def.clamp(&ParameterValue::Int(0)), ParameterValue::Int(1));
357 }
358}