1use crate::error::{LayaError, Result};
7use serde_json::Value;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum QType {
13 Choice,
14 Score,
15 Noul,
16}
17
18impl QType {
19 pub fn index(self) -> usize {
20 match self {
21 QType::Choice => 0,
22 QType::Score => 1,
23 QType::Noul => 2,
24 }
25 }
26
27 pub fn name(self) -> &'static str {
28 match self {
29 QType::Choice => "choice",
30 QType::Score => "score",
31 QType::Noul => "noul",
32 }
33 }
34
35 pub fn from_name(s: &str) -> Option<QType> {
36 match s {
37 "choice" => Some(QType::Choice),
38 "score" => Some(QType::Score),
39 "noul" => Some(QType::Noul),
40 _ => None,
41 }
42 }
43
44 pub fn from_index(i: usize) -> Option<QType> {
45 match i {
46 0 => Some(QType::Choice),
47 1 => Some(QType::Score),
48 2 => Some(QType::Noul),
49 _ => None,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
58pub struct InternalQ {
59 pub t: QType,
60 pub ins: String,
61 pub crit: Value,
62 pub labels: Option<Value>,
63 pub option_order: Option<Vec<usize>>,
67}
68
69pub trait Tokenizer {
72 fn cls_id(&self) -> u32;
73 fn sep_id(&self) -> u32;
74 fn mask_id(&self) -> u32;
75 fn pad_id(&self) -> u32;
76 fn mask_token(&self) -> &str;
77 fn encode(&self, text: &str) -> Vec<u32>;
78}
79
80const DEFAULT_NOUL_FALSE: &str = "false";
81const DEFAULT_NOUL_TRUE: &str = "true";
82
83pub fn py_json(v: &Value) -> String {
89 match v {
90 Value::Null => "null".to_string(),
91 Value::Bool(b) => {
92 if *b {
93 "true".to_string()
94 } else {
95 "false".to_string()
96 }
97 }
98 Value::Number(n) => n.to_string(),
99 Value::String(_) => serde_json::to_string(v).unwrap_or_else(|_| "\"\"".to_string()),
102 Value::Array(a) => {
103 let parts: Vec<String> = a.iter().map(py_json).collect();
104 format!("[{}]", parts.join(", "))
105 }
106 Value::Object(m) => {
107 let parts: Vec<String> = m
108 .iter()
109 .map(|(k, val)| {
110 let key = serde_json::to_string(&Value::String(k.clone()))
111 .unwrap_or_else(|_| "\"\"".to_string());
112 format!("{}: {}", key, py_json(val))
113 })
114 .collect();
115 format!("{{{}}}", parts.join(", "))
116 }
117 }
118}
119
120pub fn serialize_state(state: &Value) -> String {
122 match state {
123 Value::String(s) => s.clone(),
124 other => py_json(other),
125 }
126}
127
128pub fn render_criterion(v: &Value) -> String {
130 match v {
131 Value::String(s) => s.clone(),
132 other => py_json(other),
133 }
134}
135
136fn is_empty_criterion(v: Option<&Value>) -> bool {
138 match v {
139 None => true,
140 Some(Value::Null) => true,
141 Some(Value::String(s)) if s.is_empty() => true,
142 _ => false,
143 }
144}
145
146pub fn resolve_noul_labels(labels: Option<&Value>) -> Result<(String, String)> {
148 let err = || {
149 LayaError::InvalidQuestion(
150 "noul labels must map exactly 'false' and 'true' to distinct non-empty strings"
151 .to_string(),
152 )
153 };
154 let (f, t) = match labels {
155 None | Some(Value::Null) => {
156 return Ok((
157 DEFAULT_NOUL_FALSE.to_string(),
158 DEFAULT_NOUL_TRUE.to_string(),
159 ))
160 }
161 Some(Value::Object(m)) => {
162 let keys: std::collections::BTreeSet<&str> = m.keys().map(|s| s.as_str()).collect();
163 let want: std::collections::BTreeSet<&str> = ["false", "true"].into_iter().collect();
164 if keys != want {
165 return Err(err());
166 }
167 (m.get("false"), m.get("true"))
168 }
169 _ => return Err(err()),
170 };
171 let (f, t) = match (f, t) {
172 (Some(Value::String(f)), Some(Value::String(t))) => (f, t),
173 _ => return Err(err()),
174 };
175 let f = f.trim();
176 let t = t.trim();
177 if f.is_empty() || t.is_empty() || f == t {
178 return Err(err());
179 }
180 Ok((f.to_string(), t.to_string()))
181}
182
183pub fn render_options(q: &InternalQ) -> Result<Vec<String>> {
185 if q.t != QType::Noul && q.labels.is_some() {
186 return Err(LayaError::InvalidQuestion(
187 "labels is only supported for noul questions".to_string(),
188 ));
189 }
190 match q.t {
191 QType::Choice => {
192 let obj = q.crit.as_object().ok_or_else(|| {
193 LayaError::InvalidQuestion("choice criteria must be an object".to_string())
194 })?;
195 Ok(obj
196 .iter()
197 .map(|(k, v)| {
198 if is_empty_criterion(Some(v)) {
199 k.clone()
200 } else {
201 format!("{}: {}", k, render_criterion(v))
202 }
203 })
204 .collect())
205 }
206 QType::Score => {
207 let arr = q.crit.as_array().ok_or_else(|| {
208 LayaError::InvalidQuestion("score criteria must be a list".to_string())
209 })?;
210 Ok(arr
211 .iter()
212 .enumerate()
213 .map(|(i, c)| format!("level {}: {}", i, render_criterion(c)))
214 .collect())
215 }
216 QType::Noul => {
217 let (false_label, true_label) = resolve_noul_labels(q.labels.as_ref())?;
218 let empty = serde_json::Map::new();
219 let crit = q.crit.as_object().unwrap_or(&empty);
220 let false_crit = crit.get("false");
221 let true_crit = crit.get("true");
222 let false_text = if is_empty_criterion(false_crit) {
223 "no, the statement does not hold".to_string()
224 } else {
225 render_criterion(false_crit.unwrap())
226 };
227 let true_text = if is_empty_criterion(true_crit) {
228 "yes, the statement holds".to_string()
229 } else {
230 render_criterion(true_crit.unwrap())
231 };
232 Ok(vec![
233 format!("{}: {}", false_label, false_text),
234 format!("{}: {}", true_label, true_text),
235 ])
236 }
237 }
238}
239
240pub fn build_sequence<T: Tokenizer + ?Sized>(
245 tok: &T,
246 state: &Value,
247 q: &InternalQ,
248 max_len: usize,
249 head_max_len: usize,
250 option_order: Option<&[usize]>,
251 truncate_left: bool,
252) -> Result<(Vec<u32>, Vec<usize>)> {
253 let mask_tok = tok.mask_token();
254 let opts = render_options(q)?;
255 let order: Vec<usize> = match option_order {
256 Some(o) => o.to_vec(),
257 None => (0..opts.len()).collect(),
258 };
259
260 let ins = q.ins.replace(mask_tok, " ");
261 let head_text = format!("{} question: {}", q.t.name(), ins);
262 let mut head_ids = tok.encode(&head_text);
263
264 let mut opt_ids: Vec<Vec<u32>> = Vec::with_capacity(order.len());
265 for &i in &order {
266 let mut o = vec![tok.mask_id()];
267 let enc = tok.encode(&format!(" {}", opts[i].replace(mask_tok, " ")));
268 o.extend(enc.into_iter().take(48));
269 opt_ids.push(o);
270 }
271
272 let sum_opt = |v: &[Vec<u32>]| -> usize { v.iter().map(|o| o.len()).sum() };
273 let mut opt_budget: isize = head_max_len as isize - sum_opt(&opt_ids) as isize;
274 if opt_budget < 16 {
275 let denom = std::cmp::max(1, opt_ids.len()) as isize;
276 let per = std::cmp::max(4, (head_max_len as isize - 16) / denom) as usize;
277 for o in opt_ids.iter_mut() {
278 o.truncate(per);
279 }
280 opt_budget = head_max_len as isize - sum_opt(&opt_ids) as isize;
281 }
282 let head_keep = opt_budget.max(8) as usize;
283 head_ids.truncate(head_keep.min(head_ids.len()));
284
285 let mut ids: Vec<u32> = Vec::new();
286 ids.push(tok.cls_id());
287 ids.extend_from_slice(&head_ids);
288 ids.push(tok.sep_id());
289
290 let mut markers: Vec<usize> = Vec::with_capacity(opt_ids.len());
291 for o in &opt_ids {
292 markers.push(ids.len());
293 ids.extend_from_slice(o);
294 }
295 ids.push(tok.sep_id());
296
297 let room = (max_len as isize - ids.len() as isize - 1).max(0) as usize;
298 let st_all = tok.encode(&serialize_state(state).replace(mask_tok, " "));
299 let st: Vec<u32> = if truncate_left {
300 let start = st_all.len().saturating_sub(room);
301 st_all[start..].to_vec()
302 } else {
303 st_all.into_iter().take(room).collect()
304 };
305 ids.extend_from_slice(&st);
306 ids.push(tok.sep_id());
307 ids.truncate(max_len);
308
309 let markers = markers.into_iter().filter(|&m| m < max_len).collect();
310 Ok((ids, markers))
311}
312
313pub fn round_half_even(x: f64, ndigits: i32) -> f64 {
315 if !x.is_finite() {
316 return x;
317 }
318 let factor = 10f64.powi(ndigits);
319 let scaled = x * factor;
320 let floor = scaled.floor();
321 let diff = scaled - floor;
322 let rounded = if (diff - 0.5).abs() < 1e-9 {
323 if (floor as i64) % 2 == 0 {
325 floor
326 } else {
327 floor + 1.0
328 }
329 } else {
330 scaled.round()
331 };
332 rounded / factor
333}
334
335pub fn round4(x: f64) -> f64 {
337 round_half_even(x, 4)
338}
339
340pub fn softmax(z: &[f64]) -> Vec<f64> {
342 if z.is_empty() {
343 return Vec::new();
344 }
345 let m = z.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
346 let exps: Vec<f64> = z.iter().map(|&v| (v - m).exp()).collect();
347 let sum: f64 = exps.iter().sum();
348 exps.into_iter().map(|v| v / sum).collect()
349}
350
351pub fn answer_confidence(p: &[f64], k: usize) -> f64 {
358 if k < 1 {
359 return 1.0;
360 }
361 let p = &p[..k.min(p.len())];
362 let max = p.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
363 max.clamp(0.0, 1.0)
364}
365
366pub fn unpermute_probs(p: &[f64], option_order: Option<&[usize]>) -> Vec<f64> {
374 match option_order {
375 Some(order) if order.len() == p.len() => {
376 let mut canonical = vec![0.0; p.len()];
377 for (slot, &opt) in order.iter().enumerate() {
378 canonical[opt] = p[slot];
379 }
380 canonical
381 }
382 _ => p.to_vec(),
383 }
384}
385
386pub fn confidence_from_probs(p: &[f64], k: usize) -> f64 {
389 if k < 2 {
390 return 1.0;
391 }
392 let p = &p[..k.min(p.len())];
393 let ent: f64 = -p.iter().map(|&v| v * v.clamp(1e-12, 1.0).ln()).sum::<f64>();
394 (1.0 - ent / (k as f64).ln()).clamp(0.0, 1.0)
395}
396
397pub const TEMP_MIN: f64 = 0.5;
398pub const TEMP_MAX: f64 = 5.0;
399
400pub fn clamp_temperature(t: &Value) -> f64 {
402 let f = match t {
403 Value::Number(n) => n.as_f64(),
404 Value::String(s) => s.trim().parse::<f64>().ok(),
405 _ => None,
406 };
407 match f {
408 Some(x) if x.is_finite() => x.clamp(TEMP_MIN, TEMP_MAX),
409 _ => 1.0,
410 }
411}
412
413pub fn temp_bucket(qtype: usize, k: usize) -> String {
415 let size = if k <= 2 {
416 "2"
417 } else if k <= 5 {
418 "3-5"
419 } else if k <= 10 {
420 "6-10"
421 } else {
422 "11+"
423 };
424 let name = QType::from_index(qtype)
425 .map(|q| q.name())
426 .unwrap_or("choice");
427 format!("{}:{}", name, size)
428}
429
430#[derive(Debug, Clone)]
432pub struct Item {
433 pub ids: Vec<u32>,
434 pub markers: Vec<usize>,
435 pub qtype: usize,
436}
437
438#[derive(Debug, Clone)]
440pub struct CollatedBatch {
441 pub input_ids: Vec<Vec<u32>>,
442 pub attention_mask: Vec<Vec<u32>>,
443 pub marker_pos: Vec<Vec<usize>>,
444 pub marker_mask: Vec<Vec<bool>>,
445 pub qtype: Vec<usize>,
446}
447
448pub fn collate_items(groups: &[Vec<Item>], _pad_id: u32) -> Option<CollatedBatch> {
451 let items: Vec<&Item> = groups.iter().flatten().collect();
452 if items.is_empty() {
453 return None;
454 }
455 let l = items.iter().map(|it| it.ids.len()).max().unwrap();
456 let kmax = items.iter().map(|it| it.markers.len()).max().unwrap();
457
458 let mut input_ids = Vec::with_capacity(items.len());
459 let mut attention_mask = Vec::with_capacity(items.len());
460 let mut marker_pos = Vec::with_capacity(items.len());
461 let mut marker_mask = Vec::with_capacity(items.len());
462 let mut qtype = Vec::with_capacity(items.len());
463
464 for it in &items {
465 let mut ids = it.ids.clone();
466 let mut att = vec![1u32; ids.len()];
467 ids.resize(l, _pad_id);
468 att.resize(l, 0);
469 input_ids.push(ids);
470 attention_mask.push(att);
471
472 let k = it.markers.len();
473 let mut mpos: Vec<usize> = it.markers.clone();
474 mpos.resize(kmax, 0);
475 let mut mmask: Vec<bool> = vec![true; k];
476 mmask.resize(kmax, false);
477 marker_pos.push(mpos);
478 marker_mask.push(mmask);
479
480 qtype.push(it.qtype);
481 }
482
483 Some(CollatedBatch {
484 input_ids,
485 attention_mask,
486 marker_pos,
487 marker_mask,
488 qtype,
489 })
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use serde_json::json;
496
497 struct Fake;
499 impl Tokenizer for Fake {
500 fn cls_id(&self) -> u32 {
501 1
502 }
503 fn sep_id(&self) -> u32 {
504 2
505 }
506 fn mask_id(&self) -> u32 {
507 3
508 }
509 fn pad_id(&self) -> u32 {
510 0
511 }
512 fn mask_token(&self) -> &str {
513 "[MASK]"
514 }
515 fn encode(&self, text: &str) -> Vec<u32> {
516 text.split_whitespace()
517 .map(|w| 100 + (w.len() as u32))
518 .collect()
519 }
520 }
521
522 #[test]
523 fn py_json_matches_python_spacing() {
524 let v = json!({"a": 1, "b": "x"});
526 assert_eq!(py_json(&v), r#"{"a": 1, "b": "x"}"#);
527 assert_eq!(py_json(&json!([1, 2, 3])), "[1, 2, 3]");
528 assert_eq!(py_json(&json!("café")), "\"café\""); assert_eq!(py_json(&json!(true)), "true");
530 assert_eq!(py_json(&json!(null)), "null");
531 }
532
533 #[test]
534 fn serialize_state_passes_strings_through() {
535 assert_eq!(serialize_state(&json!("hello")), "hello");
536 assert_eq!(serialize_state(&json!({"k": "v"})), r#"{"k": "v"}"#);
537 }
538
539 #[test]
540 fn answer_confidence_is_max_prob() {
541 assert_eq!(answer_confidence(&[0.2, 0.7, 0.1], 3), 0.7);
543 assert_eq!(answer_confidence(&[0.6, 0.4], 2), 0.6);
544 assert_eq!(answer_confidence(&[0.9], 1), 0.9);
545 assert_eq!(answer_confidence(&[], 0), 1.0);
546 assert_eq!(answer_confidence(&[0.3, 0.3, 0.99], 2), 0.3);
548 }
549
550 #[test]
551 fn round4_half_to_even() {
552 assert_eq!(round4(0.12345), 0.1234); assert_eq!(round4(0.12355), 0.1236);
554 assert_eq!(round4(1.0), 1.0);
555 }
556
557 #[test]
558 fn render_options_per_type() {
559 let choice = InternalQ {
560 t: QType::Choice,
561 ins: String::new(),
562 crit: json!({"a": "first", "b": null, "c": ""}),
563 labels: None,
564 option_order: None,
565 };
566 assert_eq!(render_options(&choice).unwrap(), vec!["a: first", "b", "c"]);
567
568 let score = InternalQ {
569 t: QType::Score,
570 ins: String::new(),
571 crit: json!(["low", "high"]),
572 labels: None,
573 option_order: None,
574 };
575 assert_eq!(
576 render_options(&score).unwrap(),
577 vec!["level 0: low", "level 1: high"]
578 );
579
580 let noul = InternalQ {
581 t: QType::Noul,
582 ins: String::new(),
583 crit: json!({"true": "yes it does", "false": "no"}),
584 labels: None,
585 option_order: None,
586 };
587 assert_eq!(
588 render_options(&noul).unwrap(),
589 vec!["false: no", "true: yes it does"]
590 );
591
592 let noul_default = InternalQ {
593 t: QType::Noul,
594 ins: String::new(),
595 crit: Value::Null,
596 labels: None,
597 option_order: None,
598 };
599 assert_eq!(
600 render_options(&noul_default).unwrap(),
601 vec![
602 "false: no, the statement does not hold",
603 "true: yes, the statement holds"
604 ]
605 );
606 }
607
608 #[test]
609 fn temp_bucket_sizes() {
610 assert_eq!(temp_bucket(0, 2), "choice:2");
611 assert_eq!(temp_bucket(1, 4), "score:3-5");
612 assert_eq!(temp_bucket(2, 8), "noul:6-10");
613 assert_eq!(temp_bucket(0, 20), "choice:11+");
614 }
615
616 #[test]
617 fn build_sequence_structure() {
618 let q = InternalQ {
619 t: QType::Choice,
620 ins: "pick one".to_string(),
621 crit: json!({"a": null, "b": null}),
622 labels: None,
623 option_order: None,
624 };
625 let (ids, markers) =
626 build_sequence(&Fake, &json!("some state text"), &q, 512, 192, None, false).unwrap();
627 assert_eq!(ids[0], 1); assert_eq!(*ids.last().unwrap(), 2); assert_eq!(markers.len(), 2); for &m in &markers {
632 assert_eq!(ids[m], 3);
633 }
634 }
635
636 #[test]
637 fn confidence_bounds() {
638 assert_eq!(confidence_from_probs(&[1.0], 1), 1.0); let uniform = confidence_from_probs(&[0.5, 0.5], 2);
640 assert!(uniform.abs() < 1e-9); let certain = confidence_from_probs(&[1.0, 0.0], 2);
642 assert!((certain - 1.0).abs() < 1e-9);
643 }
644
645 #[test]
646 fn unpermute_inverts_slot_order() {
647 let p = [0.7, 0.2, 0.1];
650 let order = [2usize, 0, 1];
651 assert_eq!(unpermute_probs(&p, Some(&order)), vec![0.2, 0.1, 0.7]);
652 assert_eq!(unpermute_probs(&p, None), vec![0.7, 0.2, 0.1]);
654 assert_eq!(unpermute_probs(&p, Some(&[0usize, 1])), vec![0.7, 0.2, 0.1]);
655 }
656
657 #[test]
658 fn build_sequence_honours_option_order() {
659 let q = InternalQ {
663 t: QType::Choice,
664 ins: "pick one".to_string(),
665 crit: json!({"aa": null, "b": null, "ccc": null}),
666 labels: None,
667 option_order: Some(vec![2, 1, 0]),
668 };
669 let (ids, markers) = build_sequence(
670 &Fake,
671 &json!("state"),
672 &q,
673 512,
674 192,
675 Some(&[2, 1, 0]),
676 false,
677 )
678 .unwrap();
679 assert_eq!(markers.len(), 3);
680 assert_eq!(ids[markers[0] + 1], 103);
683 assert_eq!(ids[markers[2] + 1], 102);
684 }
685}