1use std::sync::{LazyLock, RwLock};
2
3#[derive(Clone, Debug)]
5pub struct CalcConfig {
6 pub epsilon: f64,
8 pub tolerance: f64,
10}
11
12impl Default for CalcConfig {
13 fn default() -> Self {
14 Self {
15 epsilon: 1e-7,
16 tolerance: 1e-9,
17 }
18 }
19}
20
21#[derive(Clone, Debug)]
23pub struct StandardConfig {
24 pub preference: PreferenceConfig,
26 pub indifference: IndifferenceConfig,
28 pub optimisation: OptimisationConfig,
30}
31
32#[derive(Clone, Debug)]
34pub struct PreferenceConfig {
35 pub samples: usize,
37 pub seed: u32,
39 pub strict_monotonicity: bool,
41 pub strict_convexity: bool,
43 pub epsilon: f64,
45 pub tolerance: f64,
47 pub continuity_threshold: f64,
49 pub calc_epsilon: f64,
51 pub calc_tolerance: f64,
53}
54
55#[derive(Clone, Debug)]
57pub struct IndifferenceConfig {
58 pub indiff_n_points: usize,
60 pub indiff_tol: f64,
62}
63
64#[derive(Clone, Debug)]
66pub struct OptimisationConfig {
67 pub optim_mu_init: f64,
69 pub optim_mu_decay: f64,
71 pub optim_outer_iters: usize,
73 pub optim_inner_iters: usize,
75 pub optim_step_size: f64,
77 pub optim_tol: f64,
79}
80
81impl Default for StandardConfig {
82 fn default() -> Self {
83 Self {
84 preference: PreferenceConfig {
85 samples: 60_000,
86 seed: 0,
87 strict_monotonicity: false,
88 strict_convexity: false,
89 epsilon: 1e-6,
90 tolerance: 1e-9,
91 continuity_threshold: 1.0,
92 calc_epsilon: 1e-7,
93 calc_tolerance: 1e-9,
94 },
95 indifference: IndifferenceConfig {
96 indiff_n_points: 200,
97 indiff_tol: 1e-10,
98 },
99 optimisation: OptimisationConfig {
100 optim_mu_init: 1.0,
101 optim_mu_decay: 0.1,
102 optim_outer_iters: 10,
103 optim_inner_iters: 500,
104 optim_step_size: 1e-2,
105 optim_tol: 1e-8,
106 },
107 }
108 }
109}
110
111static STANDARD_CONFIG: LazyLock<RwLock<StandardConfig>> =
112 LazyLock::new(|| RwLock::new(StandardConfig::default()));
113
114#[derive(Clone, Debug)]
116pub struct ValidationConfig {
117 pub samples: usize,
119 pub seed: u32,
121 pub strict_monotonicity: bool,
124 pub strict_convexity: bool,
127 pub epsilon: f64,
129 pub tolerance: f64,
131 pub continuity_threshold: f64,
133 pub validate: bool,
135}
136
137impl Default for ValidationConfig {
138 fn default() -> Self {
139 StandardConfig::get().validation_config(true)
140 }
141}
142
143impl StandardConfig {
144 pub fn set(config: StandardConfig) {
146 *STANDARD_CONFIG.write().unwrap() = config;
147 }
148
149 pub fn get() -> StandardConfig {
151 STANDARD_CONFIG.read().unwrap().clone()
152 }
153
154 pub fn validation_config(&self, validate: bool) -> ValidationConfig {
156 ValidationConfig {
157 samples: self.preference.samples,
158 seed: self.preference.seed,
159 strict_monotonicity: self.preference.strict_monotonicity,
160 strict_convexity: self.preference.strict_convexity,
161 epsilon: self.preference.epsilon,
162 tolerance: self.preference.tolerance,
163 continuity_threshold: self.preference.continuity_threshold,
164 validate,
165 }
166 }
167
168 pub fn calc_config(&self) -> CalcConfig {
170 CalcConfig {
171 epsilon: self.preference.calc_epsilon,
172 tolerance: self.preference.calc_tolerance,
173 }
174 }
175}
176
177#[derive(Debug)]
179pub enum PreferenceError<E> {
180 Config(String),
182 Eval(E),
184}
185
186impl<E> PreferenceError<E> {
187 fn config(message: impl Into<String>) -> Self {
188 Self::Config(message.into())
189 }
190}
191
192pub struct Preference<F>
194where
195 F: Fn(&[f64]) -> f64,
196{
197 utility_func: F, min_bounds: Vec<f64>, max_bounds: Vec<f64>, config: ValidationConfig,
201 calc_config: CalcConfig,
202}
203
204pub struct FalliblePreference<F, E>
209where
210 F: Fn(&[f64]) -> Result<f64, E>,
211{
212 utility_func: F,
213 min_bounds: Vec<f64>,
214 max_bounds: Vec<f64>,
215 config: ValidationConfig,
216 calc_config: CalcConfig,
217}
218
219impl<F> Preference<F>
220where
221 F: Fn(&[f64]) -> f64,
222{
223 pub fn new(
225 utility_func: F,
226 min_bounds: Vec<f64>,
227 max_bounds: Vec<f64>,
228 ) -> Result<Self, String> {
229 Self::with_validation(utility_func, min_bounds, max_bounds, true)
230 }
231
232 pub fn with_validation(
234 utility_func: F,
235 min_bounds: Vec<f64>,
236 max_bounds: Vec<f64>,
237 validate: bool,
238 ) -> Result<Self, String> {
239 let standard = StandardConfig::get();
240 Self::with_config(
241 utility_func,
242 min_bounds,
243 max_bounds,
244 standard.validation_config(validate),
245 standard.calc_config(),
246 )
247 }
248
249 pub fn with_config(
252 utility_func: F,
253 min_bounds: Vec<f64>,
254 max_bounds: Vec<f64>,
255 config: ValidationConfig,
256 calc_config: CalcConfig,
257 ) -> Result<Self, String> {
258 validate_bounds(&min_bounds, &max_bounds)?;
259
260 let instance = Self {
261 utility_func,
262 min_bounds,
263 max_bounds,
264 config,
265 calc_config,
266 };
267
268 if instance.config.validate {
269 instance.validate_rationality()?;
270 }
271
272 Ok(instance)
273 }
274
275 pub fn get_utility(&self, bundle: &[f64]) -> f64 {
277 (self.utility_func)(bundle)
278 }
279
280 pub fn min_bounds(&self) -> &[f64] {
281 &self.min_bounds
282 }
283
284 pub fn max_bounds(&self) -> &[f64] {
285 &self.max_bounds
286 }
287
288 pub fn get_mu(&self, bundle: &[f64], good: usize) -> f64 {
300 let ep = self.calc_config.epsilon;
301 let mut increase = bundle.to_vec();
302 let mut decrease = bundle.to_vec();
303 increase[good] += ep;
304 decrease[good] -= ep;
305 (self.get_utility(&increase) - self.get_utility(&decrease)) / (2.0 * ep)
306 }
307
308 pub fn get_mrs(&self, bundle: &[f64], good_i: usize, good_j: usize) -> Result<f64, String> {
309 let mu_j = self.get_mu(bundle, good_j);
310 if mu_j.abs() < self.calc_config.tolerance {
311 return Err(format!(
312 "MRS undefined: MU of good {} is zero at {:?}",
313 good_j, bundle
314 ));
315 }
316 Ok(self.get_mu(bundle, good_i) / mu_j)
317 }
318 fn validate_rationality(&self) -> Result<(), String> {
320 let points = self.generate_samples();
322
323 self.check_continuity(&points)?;
324 self.check_monotonicity(&points)?;
325 self.check_convexity(&points)?;
326 Ok(())
327 }
328
329 fn generate_samples(&self) -> Vec<Vec<f64>> {
331 let mut points = Vec::new();
332 let dims = self.min_bounds.len();
333
334 points.push(
336 self.min_bounds
337 .iter()
338 .zip(&self.max_bounds)
339 .map(|(l, h)| (l + h) / 2.0)
340 .collect(),
341 );
342
343 points.push(self.min_bounds.clone());
349
350 points.push(self.max_bounds.clone());
352
353 for d in 0..dims {
355 let mut corner = self.min_bounds.clone();
356 corner[d] = self.max_bounds[d];
357 points.push(corner);
358 }
359
360 if dims > 1 {
362 for d in 0..dims {
363 let mut corner = self.max_bounds.clone();
364 corner[d] = self.min_bounds[d];
365 points.push(corner);
366 }
367 }
368
369 if self.config.samples > 0 {
370 self.generate_sobol_samples(&mut points, self.config.samples);
371 }
372
373 points
374 }
375
376 fn generate_sobol_samples(&self, points: &mut Vec<Vec<f64>>, n: usize) {
378 let dims = self.min_bounds.len();
379
380 if dims > 0 && dims <= 65535 {
382 for i in 0..n {
383 let mut p = Vec::with_capacity(dims);
384 for d in 0..dims {
385 let min = self.min_bounds[d];
386 let max = self.max_bounds[d];
387
388 let sobol_val = sobol_burley::sample(i as u32, d as u32, self.config.seed);
390 p.push(min + sobol_val as f64 * (max - min));
391 }
392 points.push(p);
393 }
394 }
395 }
396
397 fn check_continuity(&self, points: &[Vec<f64>]) -> Result<(), String> {
399 for p in points {
400 let u_start = self.get_utility(p);
401
402 for i in 0..p.len() {
403 let mut p_tiny = p.clone();
404 if p_tiny[i] + self.config.epsilon <= self.max_bounds[i] {
406 p_tiny[i] += self.config.epsilon;
407 } else if p_tiny[i] - self.config.epsilon >= self.min_bounds[i] {
408 p_tiny[i] -= self.config.epsilon;
409 } else {
410 continue; }
412
413 let u_end = self.get_utility(&p_tiny);
414 if (u_end - u_start).abs() > self.config.continuity_threshold {
415 return Err(format!(
416 "Continuity Violated: Detected a jump in the utility function \
417 at index {} near {:?}",
418 i, p
419 ));
420 }
421 }
422 }
423 Ok(())
424 }
425
426 fn check_monotonicity(&self, points: &[Vec<f64>]) -> Result<(), String> {
428 for test_point in points {
429 let u_base = self.get_utility(test_point);
430
431 for i in 0..test_point.len() {
432 let mut p_plus = test_point.clone();
433 if p_plus[i] + self.config.epsilon <= self.max_bounds[i] {
435 p_plus[i] += self.config.epsilon;
436 let u_plus = self.get_utility(&p_plus);
437
438 if self.config.strict_monotonicity {
439 if u_plus <= u_base + self.config.tolerance {
440 return Err(format!(
441 "Strict Monotonicity Violated: Utility failed to \
442 strictly increase at index {} near {:?}",
443 i, test_point
444 ));
445 }
446 } else {
447 if u_plus < u_base - self.config.tolerance {
448 return Err(format!(
449 "Weak Monotonicity Violated: Utility decreased at \
450 index {} near {:?}",
451 i, test_point
452 ));
453 }
454 }
455 }
456 }
457 }
458 Ok(())
459 }
460
461 fn check_convexity(&self, points: &[Vec<f64>]) -> Result<(), String> {
463 let len = points.len();
465 for i in 0..(len / 2) {
466 let a = &points[i];
467 let b = &points[len - 1 - i];
468
469 let u_a = self.get_utility(a);
470 let u_b = self.get_utility(b);
471
472 let midpoint: Vec<f64> = a.iter().zip(b).map(|(x, y)| (x + y) / 2.0).collect();
473 let u_mid = self.get_utility(&midpoint);
474
475 let worst_utility = u_a.min(u_b);
476
477 if self.config.strict_convexity {
478 let distance_squared: f64 = a.iter().zip(b).map(|(x, y)| (x - y).powi(2)).sum();
479 if distance_squared > self.config.epsilon
480 && u_mid <= worst_utility + self.config.tolerance
481 {
482 return Err(format!(
483 "Strict Convexity Violated: Averages not strictly \
484 preferred to extremes between {:?} and {:?}",
485 a, b
486 ));
487 }
488 } else {
489 if u_mid < worst_utility - self.config.tolerance {
490 return Err(format!(
491 "Weak Convexity Violated: Preference for extremes detected \
492 between {:?} and {:?}",
493 a, b
494 ));
495 }
496 }
497 }
498 Ok(())
499 }
500}
501
502impl<F, E> FalliblePreference<F, E>
503where
504 F: Fn(&[f64]) -> Result<f64, E>,
505{
506 pub fn with_validation(
507 utility_func: F,
508 min_bounds: Vec<f64>,
509 max_bounds: Vec<f64>,
510 validate: bool,
511 ) -> Result<Self, PreferenceError<E>> {
512 let standard = StandardConfig::get();
513 Self::with_config(
514 utility_func,
515 min_bounds,
516 max_bounds,
517 standard.validation_config(validate),
518 standard.calc_config(),
519 )
520 }
521
522 pub fn with_config(
523 utility_func: F,
524 min_bounds: Vec<f64>,
525 max_bounds: Vec<f64>,
526 config: ValidationConfig,
527 calc_config: CalcConfig,
528 ) -> Result<Self, PreferenceError<E>> {
529 validate_bounds(&min_bounds, &max_bounds).map_err(PreferenceError::config)?;
530
531 let instance = Self {
532 utility_func,
533 min_bounds,
534 max_bounds,
535 config,
536 calc_config,
537 };
538
539 if instance.config.validate {
540 instance.validate_rationality()?;
541 }
542
543 Ok(instance)
544 }
545
546 pub fn get_utility(&self, bundle: &[f64]) -> Result<f64, PreferenceError<E>> {
547 (self.utility_func)(bundle).map_err(PreferenceError::Eval)
548 }
549
550 pub fn min_bounds(&self) -> &[f64] {
551 &self.min_bounds
552 }
553
554 pub fn max_bounds(&self) -> &[f64] {
555 &self.max_bounds
556 }
557
558 pub fn get_mu(&self, bundle: &[f64], good: usize) -> Result<f64, PreferenceError<E>> {
559 let ep = self.calc_config.epsilon;
560 let mut increase = bundle.to_vec();
561 let mut decrease = bundle.to_vec();
562 increase[good] += ep;
563 decrease[good] -= ep;
564 Ok((self.get_utility(&increase)? - self.get_utility(&decrease)?) / (2.0 * ep))
565 }
566
567 pub fn get_mrs(
568 &self,
569 bundle: &[f64],
570 good_i: usize,
571 good_j: usize,
572 ) -> Result<f64, PreferenceError<E>> {
573 let mu_j = self.get_mu(bundle, good_j)?;
574 if mu_j.abs() < self.calc_config.tolerance {
575 return Err(PreferenceError::config(format!(
576 "MRS undefined: MU of good {} is zero at {:?}",
577 good_j, bundle
578 )));
579 }
580 Ok(self.get_mu(bundle, good_i)? / mu_j)
581 }
582
583 fn validate_rationality(&self) -> Result<(), PreferenceError<E>> {
584 let points = self.generate_samples();
585 self.check_continuity(&points)?;
586 self.check_monotonicity(&points)?;
587 self.check_convexity(&points)?;
588 Ok(())
589 }
590
591 fn generate_samples(&self) -> Vec<Vec<f64>> {
592 let mut points = Vec::new();
593 let dims = self.min_bounds.len();
594
595 points.push(
596 self.min_bounds
597 .iter()
598 .zip(&self.max_bounds)
599 .map(|(l, h)| (l + h) / 2.0)
600 .collect(),
601 );
602 points.push(self.min_bounds.clone());
603 points.push(self.max_bounds.clone());
604
605 for d in 0..dims {
606 let mut corner = self.min_bounds.clone();
607 corner[d] = self.max_bounds[d];
608 points.push(corner);
609 }
610
611 if dims > 1 {
612 for d in 0..dims {
613 let mut corner = self.max_bounds.clone();
614 corner[d] = self.min_bounds[d];
615 points.push(corner);
616 }
617 }
618
619 if self.config.samples > 0 {
620 self.generate_sobol_samples(&mut points, self.config.samples);
621 }
622
623 points
624 }
625
626 fn generate_sobol_samples(&self, points: &mut Vec<Vec<f64>>, n: usize) {
627 let dims = self.min_bounds.len();
628
629 if dims > 0 && dims <= 65535 {
630 for i in 0..n {
631 let mut p = Vec::with_capacity(dims);
632 for d in 0..dims {
633 let min = self.min_bounds[d];
634 let max = self.max_bounds[d];
635 let sobol_val = sobol_burley::sample(i as u32, d as u32, self.config.seed);
636 p.push(min + sobol_val as f64 * (max - min));
637 }
638 points.push(p);
639 }
640 }
641 }
642
643 fn check_continuity(&self, points: &[Vec<f64>]) -> Result<(), PreferenceError<E>> {
644 for p in points {
645 let u_start = self.get_utility(p)?;
646
647 for i in 0..p.len() {
648 let mut p_tiny = p.clone();
649 if p_tiny[i] + self.config.epsilon <= self.max_bounds[i] {
650 p_tiny[i] += self.config.epsilon;
651 } else if p_tiny[i] - self.config.epsilon >= self.min_bounds[i] {
652 p_tiny[i] -= self.config.epsilon;
653 } else {
654 continue;
655 }
656
657 let u_end = self.get_utility(&p_tiny)?;
658 if (u_end - u_start).abs() > self.config.continuity_threshold {
659 return Err(PreferenceError::config(format!(
660 "Continuity Violated: Detected a jump in the utility function \
661 at index {} near {:?}",
662 i, p
663 )));
664 }
665 }
666 }
667 Ok(())
668 }
669
670 fn check_monotonicity(&self, points: &[Vec<f64>]) -> Result<(), PreferenceError<E>> {
671 for test_point in points {
672 let u_base = self.get_utility(test_point)?;
673
674 for i in 0..test_point.len() {
675 let mut p_plus = test_point.clone();
676 if p_plus[i] + self.config.epsilon <= self.max_bounds[i] {
677 p_plus[i] += self.config.epsilon;
678 let u_plus = self.get_utility(&p_plus)?;
679
680 if self.config.strict_monotonicity {
681 if u_plus <= u_base + self.config.tolerance {
682 return Err(PreferenceError::config(format!(
683 "Strict Monotonicity Violated: Utility failed to \
684 strictly increase at index {} near {:?}",
685 i, test_point
686 )));
687 }
688 } else if u_plus < u_base - self.config.tolerance {
689 return Err(PreferenceError::config(format!(
690 "Weak Monotonicity Violated: Utility decreased at \
691 index {} near {:?}",
692 i, test_point
693 )));
694 }
695 }
696 }
697 }
698 Ok(())
699 }
700
701 fn check_convexity(&self, points: &[Vec<f64>]) -> Result<(), PreferenceError<E>> {
702 let len = points.len();
703 for i in 0..(len / 2) {
704 let a = &points[i];
705 let b = &points[len - 1 - i];
706
707 let u_a = self.get_utility(a)?;
708 let u_b = self.get_utility(b)?;
709 let midpoint: Vec<f64> = a.iter().zip(b).map(|(x, y)| (x + y) / 2.0).collect();
710 let u_mid = self.get_utility(&midpoint)?;
711 let worst_utility = u_a.min(u_b);
712
713 if self.config.strict_convexity {
714 let distance_squared: f64 = a.iter().zip(b).map(|(x, y)| (x - y).powi(2)).sum();
715 if distance_squared > self.config.epsilon
716 && u_mid <= worst_utility + self.config.tolerance
717 {
718 return Err(PreferenceError::config(format!(
719 "Strict Convexity Violated: Averages not strictly \
720 preferred to extremes between {:?} and {:?}",
721 a, b
722 )));
723 }
724 } else if u_mid < worst_utility - self.config.tolerance {
725 return Err(PreferenceError::config(format!(
726 "Weak Convexity Violated: Preference for extremes detected \
727 between {:?} and {:?}",
728 a, b
729 )));
730 }
731 }
732 Ok(())
733 }
734}
735
736fn validate_bounds(min_bounds: &[f64], max_bounds: &[f64]) -> Result<(), String> {
737 if min_bounds.is_empty() || min_bounds.len() != max_bounds.len() {
738 return Err("Bounds must be non-empty and of the same length".into());
739 }
740
741 for (min, max) in min_bounds.iter().zip(max_bounds) {
742 if min > max {
743 return Err("max_bounds must be greater than or equal to min_bounds".into());
744 }
745 }
746
747 Ok(())
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753
754 fn valid_linear_utility(bundle: &[f64]) -> f64 {
756 bundle.iter().sum()
757 }
758
759 fn valid_cobb_douglas(bundle: &[f64]) -> f64 {
761 bundle.iter().product::<f64>().sqrt()
762 }
763
764 fn invalid_monotonicity_utility(bundle: &[f64]) -> f64 {
766 -(bundle[0] + bundle[1]) }
768
769 fn invalid_convexity_utility(bundle: &[f64]) -> f64 {
771 bundle[0].powi(2) + bundle[1].powi(2)
772 }
773
774 fn invalid_continuity_utility(bundle: &[f64]) -> f64 {
776 let base = bundle[0] + bundle[1];
777 if bundle[0] > 5.0 { base + 100.0 } else { base }
778 }
779
780 #[test]
781 fn test_new_valid_bounds_returns_ok() {
782 let min_bounds = vec![0.0, 0.0];
783 let max_bounds = vec![10.0, 10.0];
784 let pref = Preference::new(valid_linear_utility, min_bounds, max_bounds);
785
786 assert!(
787 pref.is_ok(),
788 "Failed to create valid preference with defaults"
789 );
790 }
791
792 #[test]
793 fn test_new_mismatched_bounds_raises_err() {
794 let min_bounds = vec![0.0, 0.0];
795 let max_bounds = vec![10.0];
796 let pref = Preference::new(valid_linear_utility, min_bounds, max_bounds);
797
798 assert!(
799 pref.is_err(),
800 "Should fail when bounds lengths are different"
801 );
802 if let Err(e) = pref {
803 assert!(e.contains("same length"));
804 }
805 }
806
807 #[test]
808 fn test_new_min_greater_than_max_raises_err() {
809 let min_bounds = vec![10.0, 0.0];
810 let max_bounds = vec![0.0, 10.0];
811 let pref = Preference::new(valid_linear_utility, min_bounds, max_bounds);
812
813 assert!(pref.is_err(), "Should fail when min bound > max bound");
814 }
815
816 #[test]
817 fn test_new_invalid_monotonicity_utility_raises_err() {
818 let min_bounds = vec![0.0, 0.0];
819 let max_bounds = vec![10.0, 10.0];
820 let pref = Preference::new(invalid_monotonicity_utility, min_bounds, max_bounds);
821
822 assert!(pref.is_err());
823 if let Err(e) = pref {
824 assert!(e.contains("Monotonicity Violated"));
825 }
826 }
827
828 #[test]
829 fn test_new_invalid_convexity_utility_raises_err() {
830 let min_bounds = vec![0.0, 0.0];
831 let max_bounds = vec![10.0, 10.0];
832 let pref = Preference::new(invalid_convexity_utility, min_bounds, max_bounds);
833
834 assert!(pref.is_err());
835 if let Err(e) = pref {
836 assert!(e.contains("Convexity Violated"));
837 }
838 }
839
840 #[test]
841 fn test_new_invalid_continuity_utility_raises_err() {
842 let min_bounds = vec![0.0, 0.0];
843 let max_bounds = vec![10.0, 10.0];
844
845 let pref = Preference::new(invalid_continuity_utility, min_bounds, max_bounds);
846
847 assert!(pref.is_err());
848 if let Err(e) = pref {
849 assert!(
850 e.contains("Continuity Violated"),
851 "Expected Continuity Violated, got: {}",
852 e
853 );
854 }
855 }
856
857 #[test]
858 fn test_with_config_strict_axioms_returns_ok() {
859 let min_bounds = vec![0.1, 0.1];
863 let max_bounds = vec![10.0, 10.0];
864
865 let config = ValidationConfig {
866 strict_monotonicity: true,
867 strict_convexity: true,
868 ..ValidationConfig::default()
869 };
870
871 let pref = Preference::with_config(
872 valid_cobb_douglas,
873 min_bounds,
874 max_bounds,
875 config,
876 CalcConfig::default(),
877 );
878
879 if let Err(e) = &pref {
880 println!("Strict Axioms Failed With: {}", e);
881 }
882 assert!(pref.is_ok(), "Strict axioms failed on a valid function");
883 }
884
885 #[test]
886 fn test_with_config_validation_disabled_invalid_utility_returns_ok() {
887 let min_bounds = vec![0.0, 0.0];
888 let max_bounds = vec![10.0, 10.0];
889
890 let config = ValidationConfig {
891 validate: false,
892 ..ValidationConfig::default()
893 };
894
895 let pref = Preference::with_config(
896 invalid_monotonicity_utility,
897 min_bounds,
898 max_bounds,
899 config,
900 CalcConfig::default(),
901 );
902
903 assert!(
904 pref.is_ok(),
905 "Should succeed when validation is disabled, even for an invalid utility function"
906 );
907 }
908
909 #[test]
910 fn test_with_config_custom_seed_valid_utility_returns_ok() {
911 let min_bounds = vec![0.0, 0.0];
912 let max_bounds = vec![10.0, 10.0];
913
914 let config = ValidationConfig {
915 seed: 42,
916 samples: 100,
917 ..ValidationConfig::default()
918 };
919
920 let pref = Preference::with_config(
921 valid_linear_utility,
922 min_bounds,
923 max_bounds,
924 config,
925 CalcConfig::default(),
926 );
927
928 assert!(pref.is_ok(), "Should succeed with a custom Sobol seed");
929 }
930
931 #[test]
932 fn test_get_mu_linear_utility_returns_expected_val() {
933 let pref = Preference::new(valid_linear_utility, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
935
936 let bundle = vec![5.0, 5.0];
937 let mu = pref.get_mu(&bundle, 0);
938 assert!((mu - 1.0).abs() < 1e-5, "Expected MU ~= 1.0, got {}", mu);
939 }
940
941 #[test]
942 fn test_get_mrs_linear_utility_returns_expected_val() {
943 let pref = Preference::new(valid_linear_utility, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
945
946 let bundle = vec![5.0, 5.0];
947 let mrs = pref.get_mrs(&bundle, 0, 1).unwrap();
948 assert!((mrs - 1.0).abs() < 1e-5, "Expected MRS ~= 1.0, got {}", mrs);
949 }
950
951 #[test]
952 fn test_get_mrs_zero_mu_denominator_raises_err() {
953 let pref =
955 Preference::new(|bundle: &[f64]| bundle[0], vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
956
957 let bundle = vec![5.0, 5.0];
958 let result = pref.get_mrs(&bundle, 0, 1);
959 assert!(
960 result.is_err(),
961 "Expected error when MU of denominator good is zero"
962 );
963 }
964
965 #[test]
966 fn test_standard_config_set_updates_default_constructor_behaviour() {
967 let original = StandardConfig::get();
968 let mut standard = original.clone();
969 standard.preference.samples = 100;
970 standard.preference.seed = 42;
971 standard.preference.calc_epsilon = 1e-5;
972 StandardConfig::set(standard);
973
974 let pref = Preference::with_validation(
975 valid_linear_utility,
976 vec![0.0, 0.0],
977 vec![10.0, 10.0],
978 false,
979 );
980
981 StandardConfig::set(original);
982
983 assert!(
984 pref.is_ok(),
985 "Expected global standard config to remain usable"
986 );
987 }
988}