bulwark_decision/
decision.rs

1use crate::ThresholdError;
2use strum_macros::{Display, EnumString};
3use validator::{Validate, ValidationError};
4
5// While tag vectors are closely related to the Decision type, by not
6// including them as fields and handling them separately, we allow
7// the Decision type to be Copy-able and we avoid unnecessary cloning
8// that would otherwise need to be done when operating on a Decision.
9// It also potentially allows for Decision to be used in other contexts.
10
11/// Represents a value from a continuous range taken from the [`pignistic`](Decision::pignistic)
12/// transformation as a category that can be used to select a response to an operation.
13#[derive(PartialEq, Eq, Clone, Copy, Debug, Display, EnumString)]
14#[strum(serialize_all = "snake_case")]
15pub enum Outcome {
16    Trusted,
17    Accepted,
18    Suspected,
19    Restricted,
20}
21
22/// A `Decision` represents evidence in favor of either accepting or restricting an operation under consideration.
23///
24/// It is composed of three values: `accept`, `restrict` and `unknown`. Each must be between 0.0 and 1.0 inclusive
25/// and the sum of all three must equal 1.0. The `unknown` value represents uncertainty about the evidence, with
26/// a 1.0 `unknown` value indicating total uncertainty or a "no opinion" verdict. Similarly, a 1.0 `accept` or
27/// `restrict` value indicates total certainty that the verdict should be to accept or to restrict, respectively.
28///
29/// This representation allows for a fairly intuitive way of characterizing evidence in favor of or against
30/// blocking an operation, while still capturing any uncertainty. Limiting to two states rather than a wider range of
31/// classification possibilities allows for better performance optimizations, simplifies code readability, and
32/// enables useful transformations like reweighting a `Decision`.
33///
34/// This data structure is a two-state [Dempster-Shafer](https://en.wikipedia.org/wiki/Dempster%E2%80%93Shafer_theory)
35/// mass function, with the power set represented by the `unknown` value. This enables the use of combination rules
36/// to aggregate decisions from multiple sources. However, knowledge of Dempster-Shafer theory should not be necessary.
37#[derive(Debug, Validate, Copy, Clone, PartialEq)]
38#[validate(schema(function = "validate_sum", skip_on_field_errors = false))]
39pub struct Decision {
40    #[validate(range(min = 0.0, max = 1.0))]
41    pub accept: f64,
42    #[validate(range(min = 0.0, max = 1.0))]
43    pub restrict: f64,
44    #[validate(range(min = 0.0, max = 1.0))]
45    pub unknown: f64,
46}
47
48impl Default for Decision {
49    /// The default [`Decision`] assigns nothing to the `accept` and `restrict` components and everything
50    /// to the `unknown` component.
51    fn default() -> Self {
52        UNKNOWN
53    }
54}
55
56/// A decision representing acceptance with full certainty.
57pub const ACCEPT: Decision = Decision {
58    accept: 1.0,
59    restrict: 0.0,
60    unknown: 0.0,
61};
62
63/// A decision representing restriction with full certainty.
64pub const RESTRICT: Decision = Decision {
65    accept: 0.0,
66    restrict: 1.0,
67    unknown: 0.0,
68};
69
70/// A decision representing full uncertainty.
71pub const UNKNOWN: Decision = Decision {
72    accept: 0.0,
73    restrict: 0.0,
74    unknown: 1.0,
75};
76
77/// Validates that a `Decision`'s components correctly sum to 1.0.
78fn validate_sum(decision: &Decision) -> Result<(), ValidationError> {
79    let sum = decision.accept + decision.restrict + decision.unknown;
80    if sum < 0.0 - 2.0 * f64::EPSILON {
81        return Err(ValidationError::new("sum cannot be negative"));
82    } else if sum > 1.0 + 2.0 * f64::EPSILON {
83        return Err(ValidationError::new("sum cannot be greater than one"));
84    } else if !(sum > 1.0 - 2.0 * f64::EPSILON && sum < 1.0 + 2.0 * f64::EPSILON) {
85        return Err(ValidationError::new("sum should be equal to one"));
86    }
87
88    Ok(())
89}
90
91impl Decision {
92    /// Converts a simple scalar value into a `Decision` using the value as the `accept` component.
93    ///
94    /// This function is sugar for `Decision { accept, 0.0, 0.0 }.scale())`.
95    ///
96    /// # Arguments
97    ///
98    /// * `accept` - The `accept` value to set.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use approx::assert_relative_eq;
104    /// use bulwark_decision::Decision;
105    ///
106    /// assert_relative_eq!(Decision::accepted(1.0), Decision { accept: 1.0, restrict: 0.0, unknown: 0.0 });
107    /// assert_relative_eq!(Decision::accepted(0.5), Decision { accept: 0.5, restrict: 0.0, unknown: 0.5 });
108    /// assert_relative_eq!(Decision::accepted(0.0), Decision { accept: 0.0, restrict: 0.0, unknown: 1.0 });
109    /// ```
110    pub fn accepted(accept: f64) -> Self {
111        Self {
112            accept,
113            restrict: 0.0,
114            unknown: 0.0,
115        }
116        .scale()
117    }
118
119    /// Converts a simple scalar value into a `Decision` using the value as the `restrict` component.
120    ///
121    /// This function is sugar for `Decision { 0.0, restrict, 0.0 }.scale())`.
122    ///
123    /// # Arguments
124    ///
125    /// * `restrict` - The `restrict` value to set.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use approx::assert_relative_eq;
131    /// use bulwark_decision::Decision;
132    ///
133    /// assert_relative_eq!(Decision::restricted(1.0), Decision { accept: 0.0, restrict: 1.0, unknown: 0.0 });
134    /// assert_relative_eq!(Decision::restricted(0.5), Decision { accept: 0.0, restrict: 0.5, unknown: 0.5 });
135    /// assert_relative_eq!(Decision::restricted(0.0), Decision { accept: 0.0, restrict: 0.0, unknown: 1.0 });
136    /// ```
137    pub fn restricted(restrict: f64) -> Self {
138        Self {
139            accept: 0.0,
140            restrict,
141            unknown: 0.0,
142        }
143        .scale()
144    }
145
146    /// Reassigns unknown mass evenly to accept and restrict.
147    ///
148    /// This function is used to convert to a form that is useful in producing a final outcome.
149    pub fn pignistic(&self) -> Self {
150        Self {
151            accept: self.accept + self.unknown / 2.0,
152            restrict: self.restrict + self.unknown / 2.0,
153            unknown: 0.0,
154        }
155    }
156
157    /// Checks the [`accept`](Decision::accept) value after [`pignistic`](Decision::pignistic)
158    /// transformation against a threshold value. `true` if above the threshold.
159    ///
160    /// # Arguments
161    ///
162    /// * `threshold` - The minimum value required to accept a [`Decision`].
163    pub fn is_accepted(&self, threshold: f64) -> bool {
164        let p = self.pignistic();
165        p.accept >= threshold
166    }
167
168    /// Checks that the [`unknown`](Decision::unknown) value is non-zero while
169    /// [`accept`](Decision::accept) and [`restrict`](Decision::restrict) are both zero.
170    pub fn is_unknown(&self) -> bool {
171        self.unknown >= f64::EPSILON
172            && self.accept >= 0.0
173            && self.accept <= f64::EPSILON
174            && self.restrict >= 0.0
175            && self.restrict <= f64::EPSILON
176    }
177
178    /// Checks the [`restrict`](Decision::restrict) value after [`pignistic`](Decision::pignistic)
179    /// transformation against several threshold values.
180    ///
181    /// The [`Outcome`]s are arranged in ascending order: `Trusted` < `Accepted` < `Suspected` < `Restricted`
182    ///
183    /// Does not take an `accept` threshold to simplify validation. Returns [`ThresholdError`] if threshold values are
184    /// either out-of-order or out-of-range. Thresholds must be between 0.0 and 1.0.
185    ///
186    /// # Arguments
187    ///
188    /// * `trust` - The `trust` threshold is an upper-bound threshold. If the `restrict` value is below it, the
189    ///     operation is `Trusted`.
190    /// * `suspicious` - The `suspicious` threshold is a lower-bound threshold that also defines the accepted range.
191    ///     If the `restrict` value is above the `trust` threshold and below the `suspicious` threshold, the operation
192    ///     is `Accepted`. If the `restrict` value is above the `suspicious` threshold but below the `restrict`
193    ///     threshold, the operation is `Suspected`.
194    /// * `restrict` -  The `restricted` threshold is a lower-bound threshold. If the `restrict` value is above it,
195    ///     the operation is `Restricted`.
196    pub fn outcome(
197        &self,
198        trust: f64,
199        suspicious: f64,
200        restrict: f64,
201    ) -> Result<Outcome, ThresholdError> {
202        let p = self.pignistic();
203        if trust > suspicious || suspicious > restrict {
204            return Err(ThresholdError::ThresholdOutOfOrder);
205        }
206        if !(0.0..=1.0).contains(&trust) {
207            return Err(ThresholdError::ThresholdOutOfRange(trust));
208        }
209        if !(0.0..=1.0).contains(&suspicious) {
210            return Err(ThresholdError::ThresholdOutOfRange(suspicious));
211        }
212        if !(0.0..=1.0).contains(&restrict) {
213            return Err(ThresholdError::ThresholdOutOfRange(restrict));
214        }
215        match p.restrict {
216            x if x <= trust => Ok(Outcome::Trusted),
217            x if x < suspicious => Ok(Outcome::Accepted),
218            x if x >= restrict => Ok(Outcome::Restricted),
219            // x >= suspicious && x < restrict
220            _ => Ok(Outcome::Suspected),
221        }
222    }
223
224    /// Clamps all values to the 0.0 to 1.0 range.
225    ///
226    /// Does not guarantee that values will sum to 1.0.
227    pub fn clamp(&self) -> Self {
228        self.clamp_min_unknown(0.0)
229    }
230
231    /// Clamps all values to the 0.0 to 1.0 range, guaranteeing that the unknown value will be at least `min`.
232    ///
233    /// Does not guarantee that values will sum to 1.0.
234    ///
235    /// # Arguments
236    ///
237    /// * `min` - The minimum [`unknown`](Decision::unknown) value.
238    pub fn clamp_min_unknown(&self, min: f64) -> Self {
239        let accept: f64 = self.accept.clamp(0.0, 1.0);
240        let restrict: f64 = self.restrict.clamp(0.0, 1.0);
241        let unknown: f64 = self.unknown.clamp(min.max(0.0), 1.0);
242
243        Self {
244            accept,
245            restrict,
246            unknown,
247        }
248    }
249
250    /// If the component values sum to less than 1.0, assigns the remainder to the
251    /// [`unknown`](Decision::unknown) value.
252    pub fn fill_unknown(&self) -> Self {
253        // Check for negative unknown values, much of this function's behavior becomes unintuitive otherwise.
254        let unknown = if self.unknown + f64::EPSILON >= 0.0 {
255            self.unknown
256        } else {
257            0.0
258        };
259        let sum = self.accept + self.restrict + unknown;
260        Self {
261            accept: self.accept,
262            restrict: self.restrict,
263            unknown: if sum - f64::EPSILON <= 1.0 {
264                1.0 - self.accept - self.restrict
265            } else {
266                unknown
267            },
268        }
269    }
270
271    /// Rescales a [`Decision`] to ensure all component values are in the 0.0-1.0 range and sum to 1.0.
272    ///
273    /// It will preserve the relative relationship between [`accept`](Decision::accept) and
274    /// [`restrict`](Decision::restrict).
275    pub fn scale(&self) -> Self {
276        self.scale_min_unknown(0.0)
277    }
278
279    /// Rescales a [`Decision`] to ensure all component values are in the 0.0-1.0 range and sum to 1.0 while
280    /// ensuring that the [`unknown`](Decision::unknown) value is at least `min`.
281    ///
282    /// It will preserve the relative relationship between [`accept`](Decision::accept) and
283    /// [`restrict`](Decision::restrict).
284    ///
285    /// # Arguments
286    ///
287    /// * `min` - The minimum [`unknown`](Decision::unknown) value.
288    pub fn scale_min_unknown(&self, min: f64) -> Self {
289        let d = self.fill_unknown().clamp();
290        let mut sum = d.accept + d.restrict + d.unknown;
291        let mut accept = d.accept;
292        let mut restrict = d.restrict;
293        let mut unknown = d.unknown;
294
295        if sum > 0.0 {
296            accept /= sum;
297            restrict /= sum;
298            unknown /= sum
299        }
300        if unknown < min {
301            unknown = min
302        }
303        sum = 1.0 - unknown;
304        if sum > 0.0 {
305            let denominator = accept + restrict;
306            accept = sum * (accept / denominator);
307            restrict = sum * (restrict / denominator)
308        }
309        Self {
310            accept,
311            restrict,
312            unknown,
313        }
314    }
315
316    /// Multiplies the [`accept`](Decision::accept) and [`restrict`](Decision::restrict) by the `factor`
317    /// parameter, replacing the [`unknown`](Decision::unknown) value with the remainder.
318    ///
319    /// Weights below 1.0 will reduce the weight of a [`Decision`], while weights above 1.0 will increase it.
320    /// A 1.0 weight has no effect on the result, aside from scaling it to a valid range if necessary.
321    ///
322    /// # Arguments
323    ///
324    /// * `factor` - A scale factor used to multiply the [`accept`](Decision::accept) and
325    ///     [`restrict`](Decision::restrict) values.
326    pub fn weight(&self, factor: f64) -> Self {
327        Self {
328            accept: self.accept * factor,
329            restrict: self.restrict * factor,
330            unknown: 0.0,
331        }
332        .scale()
333    }
334
335    /// Performs the conjunctive combination of two decisions.
336    ///
337    /// It is a helper function for [`combine`](Decision::combine).
338    ///
339    /// # Arguments
340    ///
341    /// * `left` - The first [`Decision`] of the pair.
342    /// * `right` - The second [`Decision`] of the pair.
343    fn pairwise_combine(left: &Self, right: &Self, normalize: bool) -> Self {
344        // The mass assigned to the null hypothesis due to non-intersection.
345        let nullh = if normalize {
346            left.accept * right.restrict + left.restrict * right.accept
347        } else {
348            // If normalization is disabled, just ignore the null hypothesis.
349            0.0
350        };
351
352        Self {
353            // These are essentially an unrolled loop over the power set.
354            // Each focal element from the left is multiplied by each on the right
355            // and then appended to the intersection.
356            // Finally, each focal element is normalized with respect to whatever
357            // was assigned to the null hypothesis.
358            accept: (left.accept * right.accept
359                + left.accept * right.unknown
360                + left.unknown * right.accept)
361                / (1.0 - nullh),
362            restrict: (left.restrict * right.restrict
363                + left.restrict * right.unknown
364                + left.unknown * right.restrict)
365                / (1.0 - nullh),
366            unknown: (left.unknown * right.unknown) / (1.0 - nullh),
367        }
368    }
369
370    /// Calculates the conjunctive combination of a set of decisions, returning a new [`Decision`] as the result.
371    ///
372    /// Unlike [`combine_murphy`](Decision::combine_murphy), `combine_conjunctive` will produce a `NaN` result under
373    /// high conflict.
374    ///
375    /// # Arguments
376    ///
377    /// * `decisions` - The `Decision`s to be combined.
378    pub fn combine_conjunctive<'a, I>(decisions: I) -> Self
379    where
380        Self: 'a,
381        I: IntoIterator<Item = &'a Self>,
382    {
383        let mut d = Self {
384            accept: 0.0,
385            restrict: 0.0,
386            unknown: 1.0,
387        };
388        for m in decisions {
389            d = Self::pairwise_combine(&d, m, true);
390        }
391        d
392    }
393
394    /// Calculates the Murphy average of a set of decisions, returning a new [`Decision`] as the result.
395    ///
396    /// The Murphy average rule[^1] takes the mean value of each focal element across
397    /// all mass functions to create a new mass function. This new mass function
398    /// is then combined conjunctively with itself N times where N is the total
399    /// number of functions that were averaged together.
400    ///
401    /// # Arguments
402    ///
403    /// * `decisions` - The `Decision`s to be combined.
404    ///
405    /// [^1]: Catherine K. Murphy. 2000. Combining belief functions when evidence conflicts.
406    ///     Decision Support Systems 29, 1 (2000), 1-9. DOI:<https://doi.org/10.1016/s0167-9236(99)00084-6>
407    pub fn combine_murphy<'a, I>(decisions: I) -> Self
408    where
409        Self: 'a,
410        I: IntoIterator<Item = &'a Self>,
411    {
412        let mut sum_a = 0.0;
413        let mut sum_d = 0.0;
414        let mut sum_u = 0.0;
415        let mut length: usize = 0;
416        for m in decisions {
417            sum_a += m.accept;
418            sum_d += m.restrict;
419            sum_u += m.unknown;
420            length += 1;
421        }
422        let avg_d = Self {
423            accept: sum_a / length as f64,
424            restrict: sum_d / length as f64,
425            unknown: sum_u / length as f64,
426        };
427        let mut d = Self {
428            accept: 0.0,
429            restrict: 0.0,
430            unknown: 1.0,
431        };
432        for _ in 0..length {
433            d = Self::pairwise_combine(&d, &avg_d, true);
434        }
435        d
436    }
437
438    /// Calculates the degree of conflict between a set of Decisions.
439    ///
440    /// # Arguments
441    ///
442    /// * `decisions` - The `Decision`s to measure conflict for.
443    pub fn conflict<'a, I>(decisions: I) -> f64
444    where
445        Self: 'a,
446        I: IntoIterator<Item = &'a Self>,
447    {
448        let mut d = Self {
449            accept: 0.0,
450            restrict: 0.0,
451            unknown: 1.0,
452        };
453        for m in decisions {
454            d = Self::pairwise_combine(&d, m, false);
455        }
456        let diff = d.accept + d.restrict + d.unknown;
457        if diff > 0.0 {
458            -diff.ln()
459        } else {
460            f64::INFINITY
461        }
462    }
463}
464
465impl approx::AbsDiffEq for Decision {
466    type Epsilon = <f64 as approx::AbsDiffEq>::Epsilon;
467
468    fn default_epsilon() -> Self::Epsilon {
469        <f64 as approx::AbsDiffEq>::default_epsilon()
470    }
471
472    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
473        f64::abs_diff_eq(&self.accept, &other.accept, epsilon)
474            && f64::abs_diff_eq(&self.restrict, &other.restrict, epsilon)
475            && f64::abs_diff_eq(&self.unknown, &other.unknown, epsilon)
476    }
477}
478
479impl approx::RelativeEq for Decision {
480    fn default_max_relative() -> Self::Epsilon {
481        <f64 as approx::RelativeEq>::default_max_relative()
482    }
483
484    fn relative_eq(
485        &self,
486        other: &Self,
487        epsilon: Self::Epsilon,
488        max_relative: Self::Epsilon,
489    ) -> bool {
490        f64::relative_eq(&self.accept, &other.accept, epsilon, max_relative)
491            && f64::relative_eq(&self.restrict, &other.restrict, epsilon, max_relative)
492            && f64::relative_eq(&self.unknown, &other.unknown, epsilon, max_relative)
493    }
494}
495
496impl approx::UlpsEq for Decision {
497    fn default_max_ulps() -> u32 {
498        <f64 as approx::UlpsEq>::default_max_ulps()
499    }
500
501    fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool {
502        f64::ulps_eq(&self.accept, &other.accept, epsilon, max_ulps)
503            && f64::ulps_eq(&self.restrict, &other.restrict, epsilon, max_ulps)
504            && f64::ulps_eq(&self.unknown, &other.unknown, epsilon, max_ulps)
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    macro_rules! test_decision {
513        ($name:ident, $dec:expr, $v:expr $(, $attr:ident = $val:expr)*) => {
514            #[test]
515            fn $name() {
516                $(assert_relative_eq!($dec.$attr, $val, epsilon = 2.0 * f64::EPSILON);)*
517                // test suite repeated, this time with validation
518                if $v {
519                    match $dec.validate() {
520                        Ok(_) => assert!(true),
521                        Err(e) => assert!(false, "decision should have validated: {}", e),
522                    }
523                } else {
524                    match $dec.validate() {
525                        Ok(_) => assert!(false, "decision should not validate"),
526                        Err(_) => assert!(true),
527                    }
528                }
529            }
530        }
531    }
532
533    test_decision!(
534        validate_zero,
535        Decision {
536            accept: 0.0,
537            restrict: 0.0,
538            unknown: 0.0,
539        },
540        false,
541        accept = 0.0,
542        restrict = 0.0,
543        unknown = 0.0
544    );
545
546    test_decision!(
547        validate_simple,
548        Decision {
549            accept: 0.25,
550            restrict: 0.25,
551            unknown: 0.50,
552        },
553        true,
554        accept = 0.25,
555        restrict = 0.25,
556        unknown = 0.50
557    );
558
559    test_decision!(
560        validate_negative,
561        Decision {
562            accept: -0.25,
563            restrict: 0.75,
564            unknown: 0.50,
565        },
566        false,
567        accept = -0.25,
568        restrict = 0.75,
569        unknown = 0.50
570    );
571
572    test_decision!(
573        pignistic_simple,
574        Decision {
575            accept: 0.25,
576            restrict: 0.25,
577            unknown: 0.50,
578        }
579        .pignistic(),
580        true,
581        accept = 0.5,
582        restrict = 0.5,
583        unknown = 0.0
584    );
585
586    test_decision!(
587        clamp_zero,
588        Decision {
589            accept: 0.0,
590            restrict: 0.0,
591            unknown: 0.0,
592        }
593        .clamp(),
594        false,
595        accept = 0.0,
596        restrict = 0.0,
597        unknown = 0.0
598    );
599
600    test_decision!(
601        clamp_three_halves,
602        Decision {
603            accept: 0.50,
604            restrict: 0.50,
605            unknown: 0.50,
606        }
607        .clamp(),
608        false,
609        accept = 0.50,
610        restrict = 0.50,
611        unknown = 0.50
612    );
613
614    test_decision!(
615        clamp_three_whole,
616        Decision {
617            accept: 1.0,
618            restrict: 1.0,
619            unknown: 1.0,
620        }
621        .clamp(),
622        false,
623        accept = 1.0,
624        restrict = 1.0,
625        unknown = 1.0
626    );
627
628    test_decision!(
629        clamp_negative,
630        Decision {
631            accept: -1.0,
632            restrict: -1.0,
633            unknown: -1.0,
634        }
635        .clamp(),
636        false,
637        accept = 0.0,
638        restrict = 0.0,
639        unknown = 0.0
640    );
641
642    test_decision!(
643        clamp_triple_double,
644        Decision {
645            accept: 2.0,
646            restrict: 2.0,
647            unknown: 2.0,
648        }
649        .clamp(),
650        false,
651        accept = 1.0,
652        restrict = 1.0,
653        unknown = 1.0
654    );
655
656    test_decision!(
657        clamp_min_unknown,
658        Decision {
659            accept: 2.0,
660            restrict: 2.0,
661            unknown: -1.0,
662        }
663        .clamp_min_unknown(0.3),
664        false,
665        accept = 1.0,
666        restrict = 1.0,
667        unknown = 0.3
668    );
669
670    test_decision!(
671        fill_unknown_zero,
672        Decision {
673            accept: 0.0,
674            restrict: 0.0,
675            unknown: 0.0,
676        }
677        .fill_unknown(),
678        true,
679        accept = 0.0,
680        restrict = 0.0,
681        unknown = 1.0
682    );
683
684    test_decision!(
685        fill_unknown_normal,
686        Decision {
687            accept: 0.25,
688            restrict: 0.25,
689            unknown: 0.1,
690        }
691        .fill_unknown(),
692        true,
693        accept = 0.25,
694        restrict = 0.25,
695        unknown = 0.5
696    );
697
698    test_decision!(
699        fill_unknown_over_one,
700        Decision {
701            accept: 0.5,
702            restrict: 0.5,
703            unknown: 0.5,
704        }
705        .fill_unknown(),
706        false,
707        accept = 0.5,
708        restrict = 0.5,
709        unknown = 0.5
710    );
711
712    test_decision!(
713        fill_unknown_mixed,
714        Decision {
715            accept: 2.0,
716            restrict: 2.0,
717            unknown: -3.5,
718        }
719        .fill_unknown(),
720        false,
721        accept = 2.0,
722        restrict = 2.0,
723        unknown = 0.0
724    );
725
726    test_decision!(
727        scale_zero,
728        Decision {
729            accept: 0.0,
730            restrict: 0.0,
731            unknown: 0.0,
732        }
733        .scale(),
734        true,
735        accept = 0.0,
736        restrict = 0.0,
737        unknown = 1.0
738    );
739
740    test_decision!(
741        scale_unknown,
742        Decision {
743            accept: 0.0,
744            restrict: 0.0,
745            unknown: 1.0,
746        }
747        .scale(),
748        true,
749        accept = 0.0,
750        restrict = 0.0,
751        unknown = 1.0
752    );
753
754    test_decision!(
755        scale_negative,
756        Decision {
757            accept: -1.0,
758            restrict: -1.0,
759            unknown: -1.0,
760        }
761        .scale(),
762        true,
763        accept = 0.0,
764        restrict = 0.0,
765        unknown = 1.0
766    );
767
768    test_decision!(
769        scale_double,
770        Decision {
771            accept: 2.0,
772            restrict: 2.0,
773            unknown: 2.0,
774        }
775        .scale(),
776        true,
777        accept = 0.3333333333333333,
778        restrict = 0.3333333333333333,
779        unknown = 0.3333333333333333
780    );
781
782    test_decision!(
783        scale_mixed,
784        Decision {
785            accept: 2.0,
786            restrict: 2.0,
787            unknown: -3.5,
788        }
789        .scale(),
790        true,
791        accept = 0.5,
792        restrict = 0.5,
793        unknown = 0.0
794    );
795
796    test_decision!(
797        weight_zero_by_zero,
798        Decision {
799            accept: 0.0,
800            restrict: 0.0,
801            unknown: 0.0,
802        }
803        .weight(0.0),
804        true,
805        accept = 0.0,
806        restrict = 0.0,
807        unknown = 1.0
808    );
809
810    test_decision!(
811        weight_zero_by_one,
812        Decision {
813            accept: 0.0,
814            restrict: 0.0,
815            unknown: 0.0,
816        }
817        .weight(1.0),
818        true,
819        accept = 0.0,
820        restrict = 0.0,
821        unknown = 1.0
822    );
823
824    test_decision!(
825        weight_one_by_zero,
826        Decision {
827            accept: 0.0,
828            restrict: 0.0,
829            unknown: 1.0,
830        }
831        .weight(0.0),
832        true,
833        accept = 0.0,
834        restrict = 0.0,
835        unknown = 1.0
836    );
837
838    test_decision!(
839        weight_one_by_one,
840        Decision {
841            accept: 0.0,
842            restrict: 0.0,
843            unknown: 1.0,
844        }
845        .weight(1.0),
846        true,
847        accept = 0.0,
848        restrict = 0.0,
849        unknown = 1.0
850    );
851
852    test_decision!(
853        weight_negative,
854        Decision {
855            accept: -1.0,
856            restrict: -1.0,
857            unknown: -1.0,
858        }
859        .weight(1.0),
860        true,
861        accept = 0.0,
862        restrict = 0.0,
863        unknown = 1.0
864    );
865
866    test_decision!(
867        weight_two_by_one,
868        Decision {
869            accept: 2.0,
870            restrict: 2.0,
871            unknown: 2.0,
872        }
873        .weight(1.0),
874        true,
875        accept = 0.50,
876        restrict = 0.50,
877        unknown = 0.0
878    );
879
880    test_decision!(
881        weight_two_by_two,
882        Decision {
883            accept: 2.0,
884            restrict: 2.0,
885            unknown: 2.0,
886        }
887        .weight(2.0),
888        true,
889        accept = 0.50,
890        restrict = 0.50,
891        unknown = 0.0
892    );
893
894    test_decision!(
895        weight_two_by_one_eighth,
896        Decision {
897            accept: 2.0,
898            restrict: 2.0,
899            unknown: 2.0,
900        }
901        .weight(0.125),
902        true,
903        accept = 0.25,
904        restrict = 0.25,
905        unknown = 0.5
906    );
907
908    test_decision!(
909        weight_one_eighth_by_two,
910        Decision {
911            accept: 0.125,
912            restrict: 0.125,
913            unknown: 0.75,
914        }
915        .weight(2.0),
916        true,
917        accept = 0.25,
918        restrict = 0.25,
919        unknown = 0.50
920    );
921
922    test_decision!(
923        pairwise_combine_simple,
924        Decision::pairwise_combine(
925            &Decision {
926                accept: 0.25,
927                restrict: 0.5,
928                unknown: 0.25,
929            },
930            &Decision {
931                accept: 0.25,
932                restrict: 0.1,
933                unknown: 0.65,
934            },
935            true,
936        ),
937        true,
938        accept = 0.338235294117647,
939        restrict = 0.4705882352941177,
940        unknown = 0.1911764705882353
941    );
942
943    test_decision!(
944        pairwise_combine_factored_out,
945        Decision::pairwise_combine(
946            &Decision {
947                accept: 0.25,
948                restrict: 0.5,
949                unknown: 0.25,
950            },
951            &Decision {
952                accept: 0.0,
953                restrict: 0.0,
954                unknown: 1.0,
955            },
956            true,
957        ),
958        true,
959        accept = 0.25,
960        restrict = 0.5,
961        unknown = 0.25
962    );
963
964    test_decision!(
965        pairwise_combine_certainty,
966        Decision::pairwise_combine(
967            &Decision {
968                accept: 0.25,
969                restrict: 0.5,
970                unknown: 0.25,
971            },
972            &Decision {
973                accept: 1.0,
974                restrict: 0.0,
975                unknown: 0.0,
976            },
977            true,
978        ),
979        true,
980        accept = 1.0,
981        restrict = 0.0,
982        unknown = 0.0
983    );
984
985    test_decision!(
986        combine_conjunctive_simple_with_unknown,
987        Decision::combine_conjunctive(&[
988            Decision {
989                accept: 0.35,
990                restrict: 0.20,
991                unknown: 0.45,
992            },
993            Decision {
994                accept: 0.0,
995                restrict: 0.0,
996                unknown: 1.0,
997            }
998        ]),
999        true,
1000        accept = 0.35,
1001        restrict = 0.2,
1002        unknown = 0.45
1003    );
1004
1005    test_decision!(
1006        combine_murphy_simple_with_unknown,
1007        Decision::combine_murphy(&[
1008            Decision {
1009                accept: 0.35,
1010                restrict: 0.20,
1011                unknown: 0.45,
1012            },
1013            Decision {
1014                accept: 0.0,
1015                restrict: 0.0,
1016                unknown: 1.0,
1017            }
1018        ]),
1019        true,
1020        accept = 0.2946891191709844,
1021        restrict = 0.16062176165803108,
1022        unknown = 0.5446891191709845
1023    );
1024
1025    #[test]
1026    fn test_combine_conjunctive_high_conflict() {
1027        let d = Decision::combine_conjunctive(&[
1028            Decision {
1029                accept: 1.0,
1030                restrict: 0.0,
1031                unknown: 0.0,
1032            },
1033            Decision {
1034                accept: 0.0,
1035                restrict: 1.0,
1036                unknown: 0.0,
1037            },
1038        ]);
1039        assert!(d.accept.is_nan());
1040        assert!(d.restrict.is_nan());
1041        assert!(d.unknown.is_nan());
1042    }
1043
1044    test_decision!(
1045        combine_murphy_high_conflict,
1046        Decision::combine_murphy(&[
1047            Decision {
1048                accept: 1.0,
1049                restrict: 0.0,
1050                unknown: 0.0,
1051            },
1052            Decision {
1053                accept: 0.0,
1054                restrict: 1.0,
1055                unknown: 0.0,
1056            }
1057        ]),
1058        true,
1059        accept = 0.5,
1060        restrict = 0.5,
1061        unknown = 0.0
1062    );
1063
1064    #[test]
1065    fn decision_is_accepted() {
1066        let d = Decision {
1067            accept: 0.25,
1068            restrict: 0.2,
1069            unknown: 0.55,
1070        };
1071        assert!(d.is_accepted(0.5));
1072
1073        let d = Decision {
1074            accept: 0.25,
1075            restrict: 0.25,
1076            unknown: 0.50,
1077        };
1078        assert!(d.is_accepted(0.5));
1079
1080        let d = Decision {
1081            accept: 0.2,
1082            restrict: 0.25,
1083            unknown: 0.55,
1084        };
1085        assert!(!d.is_accepted(0.5));
1086    }
1087
1088    #[test]
1089    fn decision_is_unknown() {
1090        let d = Decision {
1091            accept: 0.0,
1092            restrict: 0.2,
1093            unknown: 0.8,
1094        };
1095        assert!(!d.is_unknown());
1096
1097        let d = Decision {
1098            accept: 0.2,
1099            restrict: 0.0,
1100            unknown: 0.8,
1101        };
1102        assert!(!d.is_unknown());
1103
1104        let d = Decision {
1105            accept: 0.0,
1106            restrict: 0.0,
1107            unknown: 1.0,
1108        };
1109        assert!(d.is_unknown());
1110
1111        let d = Decision {
1112            accept: 0.0,
1113            restrict: 0.0,
1114            unknown: 0.1,
1115        };
1116        assert!(d.is_unknown());
1117    }
1118
1119    #[test]
1120    fn decision_outcome() -> Result<(), Box<dyn std::error::Error>> {
1121        let d = Decision {
1122            accept: 0.65,
1123            restrict: 0.0,
1124            unknown: 0.35,
1125        };
1126        let outcome = d.outcome(0.2, 0.4, 0.8)?;
1127        assert_eq!(outcome, Outcome::Trusted);
1128
1129        let d = Decision {
1130            accept: 0.45,
1131            restrict: 0.05,
1132            unknown: 0.5,
1133        };
1134        let outcome = d.outcome(0.2, 0.4, 0.8)?;
1135        assert_eq!(outcome, Outcome::Accepted);
1136
1137        let d = Decision {
1138            accept: 0.25,
1139            restrict: 0.2,
1140            unknown: 0.55,
1141        };
1142        let outcome = d.outcome(0.2, 0.4, 0.8)?;
1143        assert_eq!(outcome, Outcome::Suspected);
1144
1145        let d = Decision {
1146            accept: 0.05,
1147            restrict: 0.65,
1148            unknown: 0.3,
1149        };
1150        let outcome = d.outcome(0.2, 0.4, 0.8)?;
1151        assert_eq!(outcome, Outcome::Restricted);
1152
1153        Ok(())
1154    }
1155
1156    #[test]
1157    fn decision_conflict() -> Result<(), Box<dyn std::error::Error>> {
1158        // Maximum possible conflict
1159        let decisions = &[
1160            Decision {
1161                accept: 1.0,
1162                restrict: 0.0,
1163                unknown: 0.0,
1164            },
1165            Decision {
1166                accept: 0.0,
1167                restrict: 1.0,
1168                unknown: 0.0,
1169            },
1170        ];
1171        let conflict = Decision::conflict(decisions);
1172        assert_eq!(conflict, f64::INFINITY);
1173
1174        // Perfect agreement
1175        let decisions = &[
1176            Decision {
1177                accept: 1.0,
1178                restrict: 0.0,
1179                unknown: 0.0,
1180            },
1181            Decision {
1182                accept: 0.25,
1183                restrict: 0.0,
1184                unknown: 0.75,
1185            },
1186            Decision {
1187                accept: 0.5,
1188                restrict: 0.0,
1189                unknown: 0.5,
1190            },
1191            Decision {
1192                accept: 0.75,
1193                restrict: 0.0,
1194                unknown: 0.25,
1195            },
1196            Decision {
1197                accept: 0.0,
1198                restrict: 0.0,
1199                unknown: 1.0,
1200            },
1201        ];
1202        let conflict = Decision::conflict(decisions);
1203        assert_relative_eq!(conflict, 0.0, epsilon = 2.0 * f64::EPSILON);
1204
1205        // Simple two-decision conflict
1206        let decisions = &[
1207            Decision {
1208                accept: 0.25,
1209                restrict: 0.0,
1210                unknown: 0.75,
1211            },
1212            Decision {
1213                accept: 0.0,
1214                restrict: 0.5,
1215                unknown: 0.5,
1216            },
1217        ];
1218        let conflict = Decision::conflict(decisions);
1219        // null hypothesis = 0.125, conflict = -ln(1.0 - nullh)
1220        assert_relative_eq!(conflict, 0.13353139262452263, epsilon = 2.0 * f64::EPSILON);
1221
1222        // Complex multi-way conflict
1223        let decisions = &[
1224            Decision {
1225                accept: 0.25,
1226                restrict: 0.25,
1227                unknown: 0.50,
1228            },
1229            Decision {
1230                accept: 0.25,
1231                restrict: 0.0,
1232                unknown: 0.75,
1233            },
1234            Decision {
1235                accept: 0.0,
1236                restrict: 0.5,
1237                unknown: 0.5,
1238            },
1239            Decision {
1240                accept: 0.35,
1241                restrict: 0.20,
1242                unknown: 0.45,
1243            },
1244        ];
1245        let conflict = Decision::conflict(decisions);
1246        // null hypothesis = 0.41875, conflict = -ln(1.0 - nullh)
1247        assert_relative_eq!(conflict, 0.5425743220805709, epsilon = 2.0 * f64::EPSILON);
1248
1249        // Internal conflict
1250        let decisions = &[
1251            Decision {
1252                accept: 0.35,
1253                restrict: 0.20,
1254                unknown: 0.45,
1255            },
1256            Decision {
1257                accept: 0.35,
1258                restrict: 0.20,
1259                unknown: 0.45,
1260            },
1261            Decision {
1262                accept: 0.35,
1263                restrict: 0.20,
1264                unknown: 0.45,
1265            },
1266            Decision {
1267                accept: 0.35,
1268                restrict: 0.20,
1269                unknown: 0.45,
1270            },
1271        ];
1272        let conflict = Decision::conflict(decisions);
1273        // null hypothesis = 0.4529, conflict = -ln(1.0 - nullh)
1274        assert_relative_eq!(conflict, 0.6031236779123568, epsilon = 2.0 * f64::EPSILON);
1275
1276        Ok(())
1277    }
1278}