1use indexmap::IndexMap;
7
8use crate::contract::CalibrationConfig;
9use crate::error::{CoreError, Result};
10use crate::math::{
11 argmax, composite_confidence, expected_score, normalized_variance_confidence, noul_probability,
12 softmax_into,
13};
14use crate::schema::{Answer, Criteria, Question, QuestionType};
15
16pub fn evaluate_choice_with_buf(
36 logits: &[f64],
37 criteria: &Criteria,
38 temperature: f64,
39 probs_buf: &mut [f64],
40) -> Result<Answer> {
41 let map = match criteria {
42 Criteria::Map(m) => m,
43 Criteria::List(_) => {
44 return Err(CoreError::InvalidCriteriaType {
45 question_type: "choice".to_string(),
46 expected: "Map (オブジェクト)",
47 actual: "List (配列)",
48 });
49 }
50 Criteria::None => {
51 return Err(CoreError::MissingCriteria {
52 question_type: "choice".to_string(),
53 });
54 }
55 };
56
57 let k = map.len();
58 if k == 0 || k > 255 {
59 return Err(CoreError::InvalidChoiceCount { count: k });
60 }
61
62 if logits.len() != k {
63 return Err(CoreError::MathError {
64 message: format!(
65 "ロジットの要素数({logits_len})が候補数({k})と一致しません。",
66 logits_len = logits.len()
67 ),
68 });
69 }
70
71 if k == 1 {
73 let (first_key, _) = map
74 .get_index(0)
75 .expect("k == 1 なので必ず先頭要素が存在する。");
76 let mut probabilities = IndexMap::with_capacity(1);
77 probabilities.insert(first_key.clone(), 1.0);
78 return Ok(Answer::choice(first_key.clone(), probabilities, 1.0));
79 }
80
81 softmax_into(logits, probs_buf, temperature)?;
83
84 let best_idx = argmax(probs_buf)?;
86 let (selected_key, _) = map
87 .get_index(best_idx)
88 .expect("best_idx は 0..k の範囲内であることが保証されている。");
89
90 let confidence = composite_confidence(probs_buf)?;
92
93 let mut probabilities = IndexMap::with_capacity(k);
95 for (idx, (key, _)) in map.iter().enumerate() {
96 probabilities.insert(key.clone(), probs_buf[idx]);
97 }
98
99 Ok(Answer::choice(
100 selected_key.clone(),
101 probabilities,
102 confidence,
103 ))
104}
105
106pub fn evaluate_choice(logits: &[f64], criteria: &Criteria, temperature: f64) -> Result<Answer> {
111 let mut probs_buf = vec![0.0; logits.len()];
112 evaluate_choice_with_buf(logits, criteria, temperature, &mut probs_buf)
113}
114
115pub fn evaluate_score_with_buf(
136 logits: &[f64],
137 criteria: &Criteria,
138 temperature: f64,
139 probs_buf: &mut [f64],
140) -> Result<Answer> {
141 let list = match criteria {
142 Criteria::List(l) => l,
143 Criteria::Map(_) => {
144 return Err(CoreError::InvalidCriteriaType {
145 question_type: "score".to_string(),
146 expected: "List (配列)",
147 actual: "Map (オブジェクト)",
148 });
149 }
150 Criteria::None => {
151 return Err(CoreError::MissingCriteria {
152 question_type: "score".to_string(),
153 });
154 }
155 };
156
157 let m = list.len();
158 if !(2..=10).contains(&m) {
159 return Err(CoreError::InvalidScoreLevelCount { count: m });
160 }
161
162 if logits.len() != m {
163 return Err(CoreError::MathError {
164 message: format!(
165 "ロジットの要素数({logits_len})が段階数({m})と一致しません。",
166 logits_len = logits.len()
167 ),
168 });
169 }
170
171 softmax_into(logits, probs_buf, temperature)?;
173
174 let score_val = expected_score(probs_buf)?;
176
177 let confidence = normalized_variance_confidence(probs_buf)?;
179
180 let is_unique_and_non_empty = list
182 .iter()
183 .enumerate()
184 .all(|(i, label)| !label.is_empty() && list[i + 1..].iter().all(|other| label != other));
185
186 let mut probabilities = IndexMap::with_capacity(m);
187 for (k, &prob) in probs_buf.iter().enumerate() {
188 let key = if is_unique_and_non_empty {
189 list[k].clone()
190 } else {
191 k.to_string()
192 };
193 probabilities.insert(key, prob);
194 }
195
196 Ok(Answer::score(score_val, probabilities, confidence))
197}
198
199pub fn evaluate_score(logits: &[f64], criteria: &Criteria, temperature: f64) -> Result<Answer> {
204 let mut probs_buf = vec![0.0; logits.len()];
205 evaluate_score_with_buf(logits, criteria, temperature, &mut probs_buf)
206}
207
208pub fn evaluate_noul(logits: &[f64], temperature: f64) -> Result<Answer> {
223 if logits.len() < 2 {
224 return Err(CoreError::MathError {
225 message: format!(
226 "Noul 判定には真・偽に対応する 2 要素以上のロジットが必要です(指定数: {})。",
227 logits.len()
228 ),
229 });
230 }
231
232 let prob_true = noul_probability(logits[0], logits[1], temperature)?;
233 Ok(Answer::noul(prob_true))
234}
235
236pub fn evaluate_question_with_buf(
238 question: &Question,
239 logits: &[f64],
240 config: &CalibrationConfig,
241 probs_buf: &mut [f64],
242) -> Result<Answer> {
243 question.validate("evaluate_question")?;
245
246 match question.question_type {
247 QuestionType::Choice => {
248 let criteria =
249 question
250 .criteria
251 .as_ref()
252 .ok_or_else(|| CoreError::MissingCriteria {
253 question_type: "choice".to_string(),
254 })?;
255 let k = criteria.len();
256 let temperature = config.get_temperature(QuestionType::Choice, k);
257 evaluate_choice_with_buf(logits, criteria, temperature, probs_buf)
258 }
259 QuestionType::Score => {
260 let criteria =
261 question
262 .criteria
263 .as_ref()
264 .ok_or_else(|| CoreError::MissingCriteria {
265 question_type: "score".to_string(),
266 })?;
267 let m = criteria.len();
268 let temperature = config.get_temperature(QuestionType::Score, m);
269 evaluate_score_with_buf(logits, criteria, temperature, probs_buf)
270 }
271 QuestionType::Noul => {
272 let temperature = config.get_temperature(QuestionType::Noul, 2);
273 evaluate_noul(logits, temperature)
274 }
275 }
276}
277
278pub fn evaluate_question(
293 question: &Question,
294 logits: &[f64],
295 config: &CalibrationConfig,
296) -> Result<Answer> {
297 let mut probs_buf = vec![0.0; logits.len()];
298 evaluate_question_with_buf(question, logits, config, &mut probs_buf)
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_evaluate_choice_basic() {
307 let mut criteria_map = IndexMap::new();
308 criteria_map.insert("cat".to_string(), "猫".to_string());
309 criteria_map.insert("dog".to_string(), "犬".to_string());
310 criteria_map.insert("bird".to_string(), "鳥".to_string());
311 let criteria = Criteria::Map(criteria_map);
312
313 let logits = vec![1.0, 5.0, 2.0];
314 let answer = evaluate_choice(&logits, &criteria, 1.0).unwrap();
315
316 assert_eq!(answer.choice.as_deref(), Some("dog"));
317 assert!(answer.score.is_none());
318 assert!(answer.noul.is_none());
319
320 let probs = answer.probabilities.unwrap();
321 assert_eq!(probs.len(), 3);
322 assert!(probs["dog"] > probs["bird"]);
323 assert!(probs["bird"] > probs["cat"]);
324
325 let conf = answer.confidence.unwrap();
326 assert!(conf > 0.0 && conf <= 1.0);
327 }
328
329 #[test]
330 fn test_evaluate_choice_single_candidate() {
331 let mut criteria_map = IndexMap::new();
332 criteria_map.insert("only_one".to_string(), "唯一の候補".to_string());
333 let criteria = Criteria::Map(criteria_map);
334
335 let logits = vec![std::f64::consts::PI];
336 let answer = evaluate_choice(&logits, &criteria, 1.0).unwrap();
337
338 assert_eq!(answer.choice.as_deref(), Some("only_one"));
339 assert_eq!(answer.confidence, Some(1.0));
340 let probs = answer.probabilities.unwrap();
341 assert_eq!(probs["only_one"], 1.0);
342 }
343
344 #[test]
345 fn test_evaluate_choice_tied_logits() {
346 let mut criteria_map = IndexMap::new();
348 criteria_map.insert("opt_a".to_string(), "候補A".to_string());
349 criteria_map.insert("opt_b".to_string(), "候補B".to_string());
350 criteria_map.insert("opt_c".to_string(), "候補C".to_string());
351 let criteria = Criteria::Map(criteria_map);
352
353 let logits = vec![4.0, 4.0, 1.0];
354 let answer = evaluate_choice(&logits, &criteria, 1.0).unwrap();
355
356 assert_eq!(answer.choice.as_deref(), Some("opt_a"));
357 }
358
359 #[test]
360 fn test_evaluate_choice_errors() {
361 let mut criteria_map = IndexMap::new();
362 criteria_map.insert("a".to_string(), "A".to_string());
363 let criteria = Criteria::Map(criteria_map);
364
365 let logits = vec![1.0, 2.0];
367 assert!(evaluate_choice(&logits, &criteria, 1.0).is_err());
368
369 assert!(evaluate_choice(&logits, &Criteria::List(vec!["A".to_string()]), 1.0).is_err());
371 assert!(evaluate_choice(&logits, &Criteria::None, 1.0).is_err());
372 }
373
374 #[test]
375 fn test_evaluate_score_basic() {
376 let criteria = Criteria::List(vec!["低".to_string(), "中".to_string(), "高".to_string()]);
377
378 let logits = vec![0.0, 1.0, 4.0];
380 let answer = evaluate_score(&logits, &criteria, 1.0).unwrap();
381
382 assert!(answer.choice.is_none());
383 assert!(answer.noul.is_none());
384
385 let score = answer.score.unwrap();
386 assert!(score > 1.5 && score <= 2.0);
387
388 let probs = answer.probabilities.unwrap();
389 assert_eq!(probs.len(), 3);
390 assert!(probs["高"] > probs["中"]);
391
392 let conf = answer.confidence.unwrap();
393 assert!(conf > 0.0 && conf <= 1.0);
394 }
395
396 #[test]
397 fn test_evaluate_score_duplicate_labels() {
398 let criteria = Criteria::List(vec![
400 "同じ".to_string(),
401 "同じ".to_string(),
402 "同じ".to_string(),
403 ]);
404
405 let logits = vec![1.0, 2.0, 3.0];
406 let answer = evaluate_score(&logits, &criteria, 1.0).unwrap();
407 let probs = answer.probabilities.unwrap();
408
409 assert_eq!(probs.len(), 3);
410 assert!(probs.contains_key("0"));
411 assert!(probs.contains_key("1"));
412 assert!(probs.contains_key("2"));
413 }
414
415 #[test]
416 fn test_evaluate_score_errors() {
417 let criteria_small = Criteria::List(vec!["単一".to_string()]);
419 assert!(evaluate_score(&[1.0], &criteria_small, 1.0).is_err());
420
421 let criteria_large = Criteria::List((0..11).map(|i| i.to_string()).collect());
423 let logits_11 = vec![0.0; 11];
424 assert!(evaluate_score(&logits_11, &criteria_large, 1.0).is_err());
425
426 let criteria_3 = Criteria::List(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
428 assert!(evaluate_score(&[1.0, 2.0], &criteria_3, 1.0).is_err());
429 }
430
431 #[test]
432 fn test_evaluate_noul_basic() {
433 let logits = vec![3.0, 0.0];
434 let answer = evaluate_noul(&logits, 1.0).unwrap();
435
436 assert!(answer.choice.is_none());
437 assert!(answer.score.is_none());
438 assert!(answer.probabilities.is_none());
439 assert!(answer.confidence.is_none());
440
441 let prob = answer.noul.unwrap();
442 assert!(prob > 0.90 && prob <= 1.0);
443
444 assert!(evaluate_noul(&[1.0], 1.0).is_err());
446 }
447
448 #[test]
449 fn test_evaluate_question_dispatch() {
450 let config = CalibrationConfig::default();
451
452 let mut c_map = IndexMap::new();
454 c_map.insert("yes".to_string(), "はい".to_string());
455 c_map.insert("no".to_string(), "いいえ".to_string());
456 let q_choice = Question::new_choice("選択肢を選べ。", c_map);
457 let ans_choice = evaluate_question(&q_choice, &[2.0, 0.5], &config).unwrap();
458 assert_eq!(ans_choice.choice.as_deref(), Some("yes"));
459
460 let q_score = Question::new_score(
462 "評価せよ。",
463 vec!["1".to_string(), "2".to_string(), "3".to_string()],
464 );
465 let ans_score = evaluate_question(&q_score, &[0.0, 2.0, 4.0], &config).unwrap();
466 assert!(ans_score.score.unwrap() > 1.0);
467
468 let q_noul = Question::new_noul("真偽を判定せよ。");
470 let ans_noul = evaluate_question(&q_noul, &[2.5, 0.0], &config).unwrap();
471 assert!(ans_noul.noul.unwrap() > 0.85);
472 }
473}