Skip to main content

dbe_ct/
pref.rs

1use std::sync::{LazyLock, RwLock};
2
3/// Configurable options for numerical calculations (MU, MRS, etc.).
4#[derive(Clone, Debug)]
5pub struct CalcConfig {
6    /// Step size for central diff numerical differentiation (e.g. `1e-7`).
7    pub epsilon: f64,
8    /// Tolerance for f64 comparisons e.g. MRS zero denominator (e.g. `1e-9`).
9    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/// Standard consumer-theory configuration shared across default constructors.
22#[derive(Clone, Debug)]
23pub struct StandardConfig {
24    /// Shared defaults for preference construction and numerical evaluation.
25    pub preference: PreferenceConfig,
26    /// Shared defaults for indifference-curve tracing.
27    pub indifference: IndifferenceConfig,
28    /// Shared defaults for constrained bundle optimisation.
29    pub optimisation: OptimisationConfig,
30}
31
32/// Shared defaults for preference validation and local numerical evaluation.
33#[derive(Clone, Debug)]
34pub struct PreferenceConfig {
35    /// Number of Sobol sequence points to sample for the rationality checks.
36    pub samples: usize,
37    /// Seed for the Sobol sequence scrambling.
38    pub seed: u32,
39    /// Whether to enforce strict monotonicity (`U(x + ep) > U(x)`).
40    pub strict_monotonicity: bool,
41    /// Whether to enforce strict convexity (`U(midpoint) > min(U(A), U(B))`).
42    pub strict_convexity: bool,
43    /// The small increment used to compute directional changes (e.g. `1e-6`).
44    pub epsilon: f64,
45    /// Numerical tolerance for floating-point comparisons (e.g. `1e-9`).
46    pub tolerance: f64,
47    /// The threshold marginal utility (d_epsilon -> d_U) for a continuous function.
48    pub continuity_threshold: f64,
49    /// Step size for numerical differentiation (e.g. `1e-7`).
50    pub calc_epsilon: f64,
51    /// Tolerance for numerical calculations such as MU/MRS comparisons.
52    pub calc_tolerance: f64,
53}
54
55/// Shared defaults for indifference-curve tracing.
56#[derive(Clone, Debug)]
57pub struct IndifferenceConfig {
58    /// Number of points to trace along an indifference curve.
59    pub indiff_n_points: usize,
60    /// Bisection tolerance for indifference tracing.
61    pub indiff_tol: f64,
62}
63
64/// Shared defaults for constrained bundle optimisation.
65#[derive(Clone, Debug)]
66pub struct OptimisationConfig {
67    /// Initial barrier weight for bundle optimisation.
68    pub optim_mu_init: f64,
69    /// Barrier decay for bundle optimisation.
70    pub optim_mu_decay: f64,
71    /// Number of outer iterations for bundle optimisation.
72    pub optim_outer_iters: usize,
73    /// Number of inner iterations for bundle optimisation.
74    pub optim_inner_iters: usize,
75    /// Initial step size for bundle optimisation.
76    pub optim_step_size: f64,
77    /// Convergence tolerance for bundle optimisation.
78    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/// Configurable options for the Axioms of Rationality validations.
115#[derive(Clone, Debug)]
116pub struct ValidationConfig {
117    /// Number of Sobol sequence points to sample for the rationality checks.
118    pub samples: usize,
119    /// Seed for the Sobol sequence scrambling.
120    pub seed: u32,
121    /// Whether to enforce strict monotonicity (`U(x + ep) > U(x)`).
122    /// If false, requires weak monotonicity (`U(x + ep) >= U(x)`).
123    pub strict_monotonicity: bool,
124    /// Whether to enforce strict convexity (`U(midpoint) > min(U(A), U(B))`).
125    /// If false, requires weak convexity (`U(midpoint) >= min(U(A), U(B))`).
126    pub strict_convexity: bool,
127    /// The small increment used to compute directional changes (e.g. `1e-6`).
128    pub epsilon: f64,
129    /// Numerical tolerance for floating-point comparisons (e.g. `1e-9`).
130    pub tolerance: f64,
131    /// The threshold marginal utility (d_epsilon -> d_U) for a continuous function.
132    pub continuity_threshold: f64,
133    /// Whether to validate the Axioms of Rationality upon construction.
134    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    /// Replace the shared default consumer-theory configuration.
145    pub fn set(config: StandardConfig) {
146        *STANDARD_CONFIG.write().unwrap() = config;
147    }
148
149    /// Return a snapshot of the shared default consumer-theory configuration.
150    pub fn get() -> StandardConfig {
151        STANDARD_CONFIG.read().unwrap().clone()
152    }
153
154    /// Build a validation configuration from the shared defaults.
155    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    /// Build a numerical calculation configuration from the shared defaults.
169    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/// Error type for fallible preference evaluation paths.
178#[derive(Debug)]
179pub enum PreferenceError<E> {
180    /// Invalid configuration or optimisation input.
181    Config(String),
182    /// Evaluation failure raised by the caller-supplied utility function.
183    Eval(E),
184}
185
186impl<E> PreferenceError<E> {
187    fn config(message: impl Into<String>) -> Self {
188        Self::Config(message.into())
189    }
190}
191
192/// Consumer preference backed by an infallible utility function.
193pub struct Preference<F>
194where
195    F: Fn(&[f64]) -> f64,
196{
197    utility_func: F,      // Utility function U(x)
198    min_bounds: Vec<f64>, // Min limit (usually 0)
199    max_bounds: Vec<f64>, // Max limit (satiation)
200    config: ValidationConfig,
201    calc_config: CalcConfig,
202}
203
204/// Consumer preference backed by a fallible utility function.
205///
206/// This variant is intended for frontends that need utility evaluation to
207/// return errors instead of assuming the utility function is infallible.
208pub 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    /// Constructor - enforce the Axioms of Rationality upon creation on default config
224    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    /// Constructor using the shared standard config with per-instance validation control.
233    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    /// Constructor - enforce or not enforce the Axioms of Rationality on
250    /// customised config
251    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    /// Getter for the utility of a specific consumption bundle
276    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    /// Getter for marginal utility
289    ///
290    /// This method allows user to access the marginal utility for a specified
291    /// good, assuming consumption for all other goods in the bundle remains
292    /// equal:
293    ///
294    /// mu = (U(x + ep) - U(x - ep)) / 2
295    ///
296    /// # Arguments
297    /// * bundle - the bundle of goods in question
298    /// * good - index for the good to be evaluated
299    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    /// Collection of all Axioms of Rationality validations
319    fn validate_rationality(&self) -> Result<(), String> {
320        // Generate the sample points once and reuse them for all checks
321        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    /// Generates deterministic corner/edge points and Sobol quasi-random samples
330    fn generate_samples(&self) -> Vec<Vec<f64>> {
331        let mut points = Vec::new();
332        let dims = self.min_bounds.len();
333
334        // Always include the exact midpoint as a deterministic baseline
335        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        // Generate corner/edge points
344        // In highly dimensional spaces, generating *all* 2^N corners is prohibitive.
345        // A core subset of critical extremes are generated instead:
346
347        // E1: All minimum bounds
348        points.push(self.min_bounds.clone());
349
350        // E2: All maximum bounds
351        points.push(self.max_bounds.clone());
352
353        // E3: Points where exactly ONE dimension is max, and all others are min
354        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        // E4: Points where exactly ONE dimension is min, and all others are max
361        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    /// Sobol Sequence Sampling (Quasi-Random)
377    fn generate_sobol_samples(&self, points: &mut Vec<Vec<f64>>, n: usize) {
378        let dims = self.min_bounds.len();
379
380        // sobol_burley supports up to 65535 dimensions
381        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                    // Sample returns a value in [0, 1)
389                    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    /// Axiom: Continuity
398    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                // Add or subtract epsilon and ensure point stay within bounds
405                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; // Skip if bounds are too tight to perturb
411                }
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    /// Axiom: Monotonicity
427    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                // Check if we have room to add epsilon
434                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    /// Axiom: Convexity
462    fn check_convexity(&self, points: &[Vec<f64>]) -> Result<(), String> {
463        // Test pair points. Pair point `i` with point `len - 1 - i`
464        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    // A valid, simple linear utility function: U(x, y) = x + y
755    fn valid_linear_utility(bundle: &[f64]) -> f64 {
756        bundle.iter().sum()
757    }
758
759    // A valid Cobb-Douglas utility function: U(x, y) = x^0.5 * y^0.5
760    fn valid_cobb_douglas(bundle: &[f64]) -> f64 {
761        bundle.iter().product::<f64>().sqrt()
762    }
763
764    // A function that fails monotonicity (downward sloping)
765    fn invalid_monotonicity_utility(bundle: &[f64]) -> f64 {
766        -(bundle[0] + bundle[1]) // Utility drops as consumption increases
767    }
768
769    // A function that fails convexity (concave indifference curves like U = x^2 + y^2)
770    fn invalid_convexity_utility(bundle: &[f64]) -> f64 {
771        bundle[0].powi(2) + bundle[1].powi(2)
772    }
773
774    // A function that fails continuity (a jump in the middle)
775    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        // Restrict away from 0 to avoid Cobb-Douglas edge behaviour when either one of
860        // the goods in the bundle is set to 0 (relates more to mathematical behaviour
861        // of the function than violation of rationality axioms)
862        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        // For U(x, y) = x + y, MU of any good is always 1.0
934        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        // For U(x, y) = x + y, MRS = MU_x / MU_y = 1 / 1 = 1.0
944        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        // U(x, y) = x only - MU of good 1 (y) is always 0
954        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}