1use crate::bandit::{
34 ContextualBandit, checked_finite_add, checked_increment, validate_arm, validate_reward_finite,
35};
36use crate::error::RillError;
37#[cfg(feature = "serde")]
38use crate::persistence::ValidateState;
39use rand::Rng;
40
41#[derive(Debug, Clone, PartialEq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56#[non_exhaustive]
57pub struct LinUcbConfig {
58 pub alpha: f64,
63 pub arm_count: usize,
65 pub feature_count: usize,
67}
68
69impl Default for LinUcbConfig {
70 fn default() -> Self {
71 Self {
72 alpha: 1.0,
73 arm_count: 2,
74 feature_count: 1,
75 }
76 }
77}
78
79impl LinUcbConfig {
80 pub fn validate(&self) -> Result<(), RillError> {
82 if self.arm_count == 0 {
83 return Err(RillError::InvalidArmCount(self.arm_count));
84 }
85 if self.feature_count == 0 {
86 return Err(RillError::InvalidFeatureCount(self.feature_count));
87 }
88 if !self.alpha.is_finite() || self.alpha <= 0.0 {
89 return Err(RillError::InvalidParameter {
90 name: "alpha",
91 value: self.alpha,
92 });
93 }
94 Ok(())
95 }
96}
97
98#[derive(Debug, Clone)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize))]
125pub struct LinUcb {
126 arm_count: usize,
127 feature_count: usize,
128 alpha: f64,
129 a_matrices: Vec<Vec<Vec<f64>>>,
131 b_vectors: Vec<Vec<f64>>,
133 samples_seen: u64,
135}
136
137impl LinUcb {
138 pub fn new(config: LinUcbConfig) -> Result<Self, RillError> {
147 config.validate()?;
148
149 let d = config.feature_count;
150 let a_matrices = (0..config.arm_count).map(|_| identity_matrix(d)).collect();
151 let b_vectors = (0..config.arm_count).map(|_| vec![0.0; d]).collect();
152
153 Ok(Self {
154 arm_count: config.arm_count,
155 feature_count: config.feature_count,
156 alpha: config.alpha,
157 a_matrices,
158 b_vectors,
159 samples_seen: 0,
160 })
161 }
162
163 pub const fn alpha(&self) -> f64 {
165 self.alpha
166 }
167
168 pub fn a_matrix(&self, arm: usize) -> Result<&[Vec<f64>], RillError> {
174 validate_arm(self.arm_count, arm)?;
175 Ok(&self.a_matrices[arm])
176 }
177
178 pub fn b_vector(&self, arm: usize) -> Result<&[f64], RillError> {
184 validate_arm(self.arm_count, arm)?;
185 Ok(&self.b_vectors[arm])
186 }
187
188 pub fn validate(&self) -> Result<(), RillError> {
192 LinUcbConfig {
193 alpha: self.alpha,
194 arm_count: self.arm_count,
195 feature_count: self.feature_count,
196 }
197 .validate()?;
198 if self.a_matrices.len() != self.arm_count || self.b_vectors.len() != self.arm_count {
199 return Err(RillError::InvalidState(
200 "arm_count does not match per-arm state lengths".to_owned(),
201 ));
202 }
203
204 for arm in 0..self.arm_count {
205 let matrix = &self.a_matrices[arm];
206 let vector = &self.b_vectors[arm];
207 if matrix.len() != self.feature_count
208 || matrix.iter().any(|row| row.len() != self.feature_count)
209 || vector.len() != self.feature_count
210 {
211 return Err(RillError::InvalidState(format!(
212 "arm {arm} state does not match feature_count"
213 )));
214 }
215 if matrix.iter().flatten().any(|value| !value.is_finite())
216 || vector.iter().any(|value| !value.is_finite())
217 {
218 return Err(RillError::InvalidState(format!(
219 "arm {arm} state contains a non-finite value"
220 )));
221 }
222 for (i, row) in matrix.iter().enumerate() {
223 for (j, &value) in row.iter().take(i).enumerate() {
224 if value != matrix[j][i] {
225 return Err(RillError::InvalidState(format!(
226 "A matrix for arm {arm} is not symmetric"
227 )));
228 }
229 }
230 }
231 if !matrix_is_positive_definite(matrix) {
232 return Err(RillError::InvalidState(format!(
233 "A matrix for arm {arm} is not positive definite"
234 )));
235 }
236 }
237 Ok(())
238 }
239
240 fn validate_context(&self, context: &[f64]) -> Result<(), RillError> {
242 if context.len() != self.feature_count {
243 return Err(RillError::DimensionMismatch {
244 expected: self.feature_count,
245 actual: context.len(),
246 });
247 }
248 for (i, &v) in context.iter().enumerate() {
249 if !v.is_finite() {
250 return Err(RillError::NonFiniteValue {
251 field: "context",
252 value: context[i],
253 });
254 }
255 }
256 Ok(())
257 }
258
259 fn arm_score(&self, arm: usize, context: &[f64]) -> Result<f64, RillError> {
264 let a_inv = matrix_inverse(&self.a_matrices[arm])?;
265 let b = &self.b_vectors[arm];
266 let theta = matrix_vector_mul(&a_inv, b);
268 let exploitation = dot(&theta, context);
270 let quad = quadratic_form(context, &a_inv);
272 let quad_safe = if quad < 0.0 { 0.0 } else { quad };
275 let score = exploitation + self.alpha * quad_safe.sqrt();
276 if !score.is_finite() {
277 return Err(RillError::NonFiniteValue {
278 field: "LinUCB score",
279 value: score,
280 });
281 }
282 Ok(score)
283 }
284}
285
286impl ContextualBandit for LinUcb {
287 fn arm_count(&self) -> usize {
288 self.arm_count
289 }
290
291 fn feature_count(&self) -> usize {
292 self.feature_count
293 }
294
295 fn samples_seen(&self) -> u64 {
296 self.samples_seen
297 }
298
299 fn select(&self, context: &[f64], rng: &mut impl Rng) -> Result<usize, RillError> {
300 self.validate_context(context)?;
301
302 let mut best_arm = 0usize;
303 let mut best_score = f64::NEG_INFINITY;
304 let mut tied = 0usize;
305 for arm in 0..self.arm_count {
306 let score = self.arm_score(arm, context)?;
307 if score > best_score {
308 best_score = score;
309 best_arm = arm;
310 tied = 1;
311 } else if score == best_score {
312 tied += 1;
315 if rng.gen_range(0..tied) == 0 {
316 best_arm = arm;
317 }
318 }
319 }
320 Ok(best_arm)
321 }
322
323 fn update(&mut self, arm: usize, context: &[f64], reward: f64) -> Result<(), RillError> {
324 validate_arm(self.arm_count, arm)?;
325 self.validate_context(context)?;
326 validate_reward_finite(reward)?;
327
328 let d = self.feature_count;
329 let mut next_a = self.a_matrices[arm].clone();
330 for i in 0..d {
331 for j in 0..d {
332 next_a[i][j] =
333 checked_finite_add(next_a[i][j], context[i] * context[j], "A matrix")?;
334 }
335 }
336 let mut next_b = self.b_vectors[arm].clone();
337 for i in 0..d {
338 next_b[i] = checked_finite_add(next_b[i], reward * context[i], "b vector")?;
339 }
340 let next_samples = checked_increment(self.samples_seen, "samples_seen")?;
341
342 self.a_matrices[arm] = next_a;
343 self.b_vectors[arm] = next_b;
344 self.samples_seen = next_samples;
345 Ok(())
346 }
347
348 fn reset(&mut self) {
349 for a in &mut self.a_matrices {
350 *a = identity_matrix(self.feature_count);
351 }
352 for b in &mut self.b_vectors {
353 for v in b.iter_mut() {
354 *v = 0.0;
355 }
356 }
357 self.samples_seen = 0;
358 }
359}
360
361#[cfg(feature = "serde")]
362#[derive(serde::Deserialize)]
363struct LinUcbState {
364 arm_count: usize,
365 feature_count: usize,
366 alpha: f64,
367 a_matrices: Vec<Vec<Vec<f64>>>,
368 b_vectors: Vec<Vec<f64>>,
369 samples_seen: u64,
370}
371
372#[cfg(feature = "serde")]
373impl<'de> serde::Deserialize<'de> for LinUcb {
374 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
375 where
376 D: serde::Deserializer<'de>,
377 {
378 let state = LinUcbState::deserialize(deserializer)?;
379 let bandit = Self {
380 arm_count: state.arm_count,
381 feature_count: state.feature_count,
382 alpha: state.alpha,
383 a_matrices: state.a_matrices,
384 b_vectors: state.b_vectors,
385 samples_seen: state.samples_seen,
386 };
387 bandit.validate().map_err(serde::de::Error::custom)?;
388 Ok(bandit)
389 }
390}
391
392#[cfg(feature = "serde")]
393impl ValidateState for LinUcb {
394 fn validate_state(&self) -> Result<(), RillError> {
395 LinUcb::validate(self)
396 }
397}
398
399fn identity_matrix(d: usize) -> Vec<Vec<f64>> {
405 let mut m = vec![vec![0.0; d]; d];
406 for (i, row) in m.iter_mut().enumerate() {
407 row[i] = 1.0;
408 }
409 m
410}
411
412fn matrix_is_positive_definite(matrix: &[Vec<f64>]) -> bool {
414 let n = matrix.len();
415 let mut lower = vec![vec![0.0; n]; n];
416 for i in 0..n {
417 for j in 0..=i {
418 let correction: f64 = (0..j).map(|k| lower[i][k] * lower[j][k]).sum();
419 let residual = matrix[i][j] - correction;
420 if i == j {
421 if !residual.is_finite() || residual <= 0.0 {
422 return false;
423 }
424 lower[i][j] = residual.sqrt();
425 } else {
426 lower[i][j] = residual / lower[j][j];
427 if !lower[i][j].is_finite() {
428 return false;
429 }
430 }
431 }
432 }
433 true
434}
435
436#[allow(clippy::needless_range_loop)]
442fn matrix_inverse(matrix: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, RillError> {
443 let n = matrix.len();
444 let mut aug = vec![vec![0.0; 2 * n]; n];
446 for i in 0..n {
447 for j in 0..n {
448 aug[i][j] = matrix[i][j];
449 }
450 aug[i][n + i] = 1.0;
451 }
452
453 for col in 0..n {
455 let mut pivot = col;
457 let mut max_val = aug[col][col].abs();
458 for row in (col + 1)..n {
459 if aug[row][col].abs() > max_val {
460 max_val = aug[row][col].abs();
461 pivot = row;
462 }
463 }
464 if max_val < 1e-12 {
465 return Err(RillError::InvalidParameter {
466 name: "matrix",
467 value: 0.0,
468 });
469 }
470 if pivot != col {
471 aug.swap(col, pivot);
472 }
473 let pivot_val = aug[col][col];
475 for j in 0..(2 * n) {
476 aug[col][j] /= pivot_val;
477 }
478 for row in 0..n {
480 if row == col {
481 continue;
482 }
483 let factor = aug[row][col];
484 if factor == 0.0 {
485 continue;
486 }
487 for j in 0..(2 * n) {
488 aug[row][j] -= factor * aug[col][j];
489 }
490 }
491 }
492
493 let mut inv = vec![vec![0.0; n]; n];
495 for i in 0..n {
496 for j in 0..n {
497 inv[i][j] = aug[i][n + j];
498 }
499 }
500 Ok(inv)
501}
502
503fn matrix_vector_mul(matrix: &[Vec<f64>], vector: &[f64]) -> Vec<f64> {
505 let n = matrix.len();
506 let vector = &vector[..n];
507 matrix
508 .iter()
509 .map(|row| {
510 row[..n]
511 .iter()
512 .zip(vector)
513 .map(|(matrix_value, vector_value)| matrix_value * vector_value)
514 .sum()
515 })
516 .collect()
517}
518
519fn dot(a: &[f64], b: &[f64]) -> f64 {
521 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
522}
523
524fn quadratic_form(x: &[f64], matrix: &[Vec<f64>]) -> f64 {
526 let n = x.len();
527 let mut result = 0.0;
528 for i in 0..n {
529 for j in 0..n {
530 result += x[i] * matrix[i][j] * x[j];
531 }
532 }
533 result
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use rand::SeedableRng;
540 use rand_chacha::ChaCha8Rng;
541
542 fn make_bandit() -> LinUcb {
543 LinUcb::new(LinUcbConfig {
544 alpha: 1.0,
545 arm_count: 3,
546 feature_count: 2,
547 })
548 .unwrap()
549 }
550
551 #[test]
552 fn rejects_zero_arm_count() {
553 let result = LinUcb::new(LinUcbConfig {
554 alpha: 1.0,
555 arm_count: 0,
556 feature_count: 2,
557 });
558 assert!(matches!(result, Err(RillError::InvalidArmCount(0))));
559 }
560
561 #[test]
562 fn rejects_zero_feature_count() {
563 let result = LinUcb::new(LinUcbConfig {
564 alpha: 1.0,
565 arm_count: 3,
566 feature_count: 0,
567 });
568 assert!(matches!(result, Err(RillError::InvalidFeatureCount(0))));
569 }
570
571 #[test]
572 fn rejects_invalid_alpha() {
573 for &bad in &[0.0, -1.0, f64::NAN, f64::INFINITY] {
574 let result = LinUcb::new(LinUcbConfig {
575 alpha: bad,
576 arm_count: 3,
577 feature_count: 2,
578 });
579 assert!(matches!(result, Err(RillError::InvalidParameter { .. })));
580 }
581 }
582
583 #[test]
584 fn initial_state() {
585 let b = make_bandit();
586 assert_eq!(b.arm_count(), 3);
587 assert_eq!(b.feature_count(), 2);
588 assert_eq!(b.samples_seen(), 0);
589 assert!((b.alpha() - 1.0).abs() < 1e-12);
590 }
591
592 #[test]
593 fn initial_a_is_identity() {
594 let b = make_bandit();
595 let a = b.a_matrix(0).unwrap();
596 assert!((a[0][0] - 1.0).abs() < 1e-12);
597 assert!((a[0][1] - 0.0).abs() < 1e-12);
598 assert!((a[1][0] - 0.0).abs() < 1e-12);
599 assert!((a[1][1] - 1.0).abs() < 1e-12);
600 }
601
602 #[test]
603 fn initial_b_is_zero() {
604 let b = make_bandit();
605 let bv = b.b_vector(0).unwrap();
606 assert!((bv[0] - 0.0).abs() < 1e-12);
607 assert!((bv[1] - 0.0).abs() < 1e-12);
608 }
609
610 #[test]
611 fn initial_ties_are_randomized() {
612 let b = make_bandit();
613 let mut rng = ChaCha8Rng::seed_from_u64(12);
614 let mut seen = std::collections::HashSet::new();
615 for _ in 0..100 {
616 seen.insert(b.select(&[0.5, 0.8], &mut rng).unwrap());
617 }
618 assert_eq!(seen.len(), b.arm_count());
619 }
620
621 #[test]
622 fn select_returns_valid_arm() {
623 let b = make_bandit();
624 let mut rng = ChaCha8Rng::seed_from_u64(42);
625 let context = [0.5, 0.8];
626 let arm = b.select(&context, &mut rng).unwrap();
627 assert!(arm < 3);
628 }
629
630 #[test]
631 fn update_modifies_a_and_b() {
632 let mut b = make_bandit();
633 let context = [0.5, 0.8];
634 b.update(0, &context, 1.0).unwrap();
635
636 let a = b.a_matrix(0).unwrap();
637 assert!((a[0][0] - (1.0 + 0.5 * 0.5)).abs() < 1e-12);
639 assert!((a[0][1] - (0.5 * 0.8)).abs() < 1e-12);
640 assert!((a[1][0] - (0.8 * 0.5)).abs() < 1e-12);
641 assert!((a[1][1] - (1.0 + 0.8 * 0.8)).abs() < 1e-12);
642
643 let bv = b.b_vector(0).unwrap();
644 assert!((bv[0] - 0.5).abs() < 1e-12);
645 assert!((bv[1] - 0.8).abs() < 1e-12);
646 assert_eq!(b.samples_seen(), 1);
647 }
648
649 #[test]
650 fn update_does_not_affect_other_arms() {
651 let mut b = make_bandit();
652 let context = [0.5, 0.8];
653 b.update(0, &context, 1.0).unwrap();
654
655 let a1 = b.a_matrix(1).unwrap();
657 assert!((a1[0][0] - 1.0).abs() < 1e-12);
658 let b1 = b.b_vector(1).unwrap();
659 assert!((b1[0] - 0.0).abs() < 1e-12);
660 }
661
662 #[test]
663 fn select_rejects_wrong_context_length() {
664 let b = make_bandit();
665 let mut rng = ChaCha8Rng::seed_from_u64(0);
666 assert!(b.select(&[0.5], &mut rng).is_err());
667 assert!(b.select(&[0.5, 0.8, 0.9], &mut rng).is_err());
668 }
669
670 #[test]
671 fn select_rejects_non_finite_context() {
672 let b = make_bandit();
673 let mut rng = ChaCha8Rng::seed_from_u64(0);
674 assert!(b.select(&[f64::NAN, 0.8], &mut rng).is_err());
675 assert!(b.select(&[0.5, f64::INFINITY], &mut rng).is_err());
676 }
677
678 #[test]
679 fn update_rejects_invalid_arm() {
680 let mut b = make_bandit();
681 let context = [0.5, 0.8];
682 assert!(b.update(3, &context, 1.0).is_err());
683 }
684
685 #[test]
686 fn update_rejects_wrong_context_length() {
687 let mut b = make_bandit();
688 assert!(b.update(0, &[0.5], 1.0).is_err());
689 assert!(b.update(0, &[0.5, 0.8, 0.9], 1.0).is_err());
690 }
691
692 #[test]
693 fn update_rejects_non_finite_reward() {
694 let mut b = make_bandit();
695 let context = [0.5, 0.8];
696 assert!(b.update(0, &context, f64::NAN).is_err());
697 assert!(b.update(0, &context, f64::INFINITY).is_err());
698 }
699
700 #[test]
701 fn update_rejects_arithmetic_overflow_without_mutating_state() {
702 let mut b = make_bandit();
703 let before = b.clone();
704 assert!(b.update(0, &[f64::MAX, f64::MAX], 1.0).is_err());
705 assert_eq!(b.a_matrices, before.a_matrices);
706 assert_eq!(b.b_vectors, before.b_vectors);
707 assert_eq!(b.samples_seen(), before.samples_seen());
708 }
709
710 #[test]
711 fn a_matrix_rejects_invalid_arm() {
712 let b = make_bandit();
713 assert!(b.a_matrix(5).is_err());
714 }
715
716 #[test]
717 fn b_vector_rejects_invalid_arm() {
718 let b = make_bandit();
719 assert!(b.b_vector(5).is_err());
720 }
721
722 #[test]
723 fn reset_clears_state() {
724 let mut b = make_bandit();
725 let context = [0.5, 0.8];
726 b.update(0, &context, 1.0).unwrap();
727 b.update(1, &context, 0.5).unwrap();
728 assert_eq!(b.samples_seen(), 2);
729
730 b.reset();
731 assert_eq!(b.samples_seen(), 0);
732 let a = b.a_matrix(0).unwrap();
734 assert!((a[0][0] - 1.0).abs() < 1e-12);
735 let bv = b.b_vector(0).unwrap();
737 assert!((bv[0] - 0.0).abs() < 1e-12);
738 }
739
740 #[test]
741 fn identity_matrix_inverse_is_identity() {
742 let ident = identity_matrix(3);
743 let inv = matrix_inverse(&ident).unwrap();
744 for (i, row) in inv.iter().enumerate() {
745 for (j, &val) in row.iter().enumerate() {
746 let expected = if i == j { 1.0 } else { 0.0 };
747 assert!((val - expected).abs() < 1e-12);
748 }
749 }
750 }
751
752 #[test]
753 fn known_2x2_matrix_inverse() {
754 let matrix = vec![vec![4.0, 7.0], vec![2.0, 6.0]];
756 let inv = matrix_inverse(&matrix).unwrap();
757 assert!((inv[0][0] - 0.6).abs() < 1e-10);
758 assert!((inv[0][1] - (-0.7)).abs() < 1e-10);
759 assert!((inv[1][0] - (-0.2)).abs() < 1e-10);
760 assert!((inv[1][1] - 0.4).abs() < 1e-10);
761 }
762
763 #[test]
764 fn singular_matrix_inverse_returns_error() {
765 let matrix = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
767 let result = matrix_inverse(&matrix);
768 assert!(result.is_err());
769 }
770
771 #[test]
772 fn dot_product_correct() {
773 assert!((dot(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]) - 32.0).abs() < 1e-12);
774 }
775
776 #[test]
777 fn matrix_vector_mul_correct() {
778 let m = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
779 let v = vec![5.0, 6.0];
780 let r = matrix_vector_mul(&m, &v);
781 assert!((r[0] - 17.0).abs() < 1e-12);
782 assert!((r[1] - 39.0).abs() < 1e-12);
783 }
784
785 #[test]
786 fn quadratic_form_correct() {
787 let ident = identity_matrix(3);
789 let x = [1.0, 2.0, 3.0];
790 let q = quadratic_form(&x, &ident);
791 assert!((q - 14.0).abs() < 1e-12);
792 }
793
794 #[test]
795 fn contextual_selection_prefers_aligned_arm() {
796 let mut b = LinUcb::new(LinUcbConfig {
801 alpha: 0.1,
802 arm_count: 2,
803 feature_count: 2,
804 })
805 .unwrap();
806 let mut rng = ChaCha8Rng::seed_from_u64(7);
807
808 for _ in 0..20 {
810 b.update(0, &[1.0, 0.0], 1.0).unwrap();
811 }
812 for _ in 0..20 {
814 b.update(1, &[0.0, 1.0], 1.0).unwrap();
815 }
816
817 let arm = b.select(&[1.0, 0.0], &mut rng).unwrap();
819 assert_eq!(arm, 0);
820
821 let arm = b.select(&[0.0, 1.0], &mut rng).unwrap();
823 assert_eq!(arm, 1);
824 }
825
826 #[test]
827 fn learns_to_prefer_high_reward_arm() {
828 let mut b = LinUcb::new(LinUcbConfig {
831 alpha: 0.5,
832 arm_count: 2,
833 feature_count: 1,
834 })
835 .unwrap();
836 let mut rng = ChaCha8Rng::seed_from_u64(99);
837
838 for step in 1..=100 {
839 let x = step as f64 * 0.1;
840 let arm = b.select(&[x], &mut rng).unwrap();
841 let reward = if arm == 0 { 2.0 * x } else { 0.1 * x };
843 b.update(arm, &[x], reward).unwrap();
844 }
845
846 let final_arm = b.select(&[5.0], &mut rng).unwrap();
848 assert_eq!(final_arm, 0);
849 }
850
851 #[cfg(feature = "serde")]
852 #[test]
853 fn serde_roundtrip() {
854 let mut b = LinUcb::new(LinUcbConfig {
855 alpha: 1.5,
856 arm_count: 2,
857 feature_count: 3,
858 })
859 .unwrap();
860 b.update(0, &[1.0, 0.5, 0.2], 1.0).unwrap();
861 b.update(1, &[0.3, 0.7, 0.9], 0.5).unwrap();
862
863 let json = serde_json::to_string(&b).unwrap();
864 let restored: LinUcb = serde_json::from_str(&json).unwrap();
865 assert_eq!(restored.arm_count(), b.arm_count());
866 assert_eq!(restored.feature_count(), b.feature_count());
867 assert_eq!(restored.samples_seen(), b.samples_seen());
868 assert!((restored.alpha() - b.alpha()).abs() < 1e-12);
869 let orig_a = b.a_matrix(0).unwrap();
871 let rest_a = restored.a_matrix(0).unwrap();
872 for (orig_row, rest_row) in orig_a.iter().zip(rest_a.iter()) {
873 for (&o, &r) in orig_row.iter().zip(rest_row.iter()) {
874 assert!((o - r).abs() < 1e-12);
875 }
876 }
877 }
878
879 #[cfg(feature = "serde")]
880 #[test]
881 fn serde_rejects_malformed_state() {
882 let json = r#"{
883 "arm_count": 2,
884 "feature_count": 2,
885 "alpha": 1.0,
886 "a_matrices": [[[1.0, 0.0], [0.0, 1.0]]],
887 "b_vectors": [[0.0, 0.0]],
888 "samples_seen": 0
889 }"#;
890 assert!(serde_json::from_str::<LinUcb>(json).is_err());
891 }
892}