mahf/problems/objective/
multi.rs

1//! Objective vector with multiple objectives.
2
3use std::{cmp::Ordering, fmt::Debug};
4
5use serde::Serialize;
6
7use crate::problems::objective::{IllegalObjective, Objective};
8
9/// Represents multiple real-valued objectives and can be used to
10/// represent an objective vector in multi-objective optimization.
11///
12/// This objective type is used for [`MultiObjectiveProblem`]s.
13///
14/// # Restrictions
15///
16/// This is a wrapper around [`Vec<f64>`], which can't take NaN values,
17/// and therefore can implement [`Eq`].
18///
19/// For details, see [`IllegalObjective`].
20///
21/// The [PartialOrd] implementation returns the Pareto ordering.
22/// As the Pareto ordering leaves some vectors as incomparable, [`Ord`] is not implemented.
23///
24/// [`MultiObjectiveProblem`]: crate::problems::MultiObjectiveProblem
25#[derive(Clone, Serialize, PartialEq)]
26pub struct MultiObjective(Vec<f64>);
27
28impl MultiObjective {
29    /// Checks if the objective values contain `+Inf` (positive infinity) or not.
30    pub fn is_finite(&self) -> bool {
31        self.0.iter().all(|o| o.is_finite())
32    }
33
34    /// Returns the objective value as a slice of floats.
35    pub fn value(&self) -> &[f64] {
36        &self.0
37    }
38}
39
40impl Debug for MultiObjective {
41    /// Returns a string representation of the objective values.
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{:?}", self.0)
44    }
45}
46
47impl Eq for MultiObjective {}
48
49impl PartialOrd for MultiObjective {
50    /// Orders two objective value vectors using Pareto-domination.
51    ///
52    /// Note that [Ordering::Less] means that `self` dominates `other`:
53    /// ```
54    /// use std::cmp::Ordering;
55    ///
56    /// use mahf::MultiObjective;
57    ///
58    /// let a = MultiObjective::try_from(vec![0., 0.]).unwrap();
59    /// let b = MultiObjective::try_from(vec![1., 1.]).unwrap();
60    ///
61    /// // `a` dominates `b` (minimization).
62    /// assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
63    /// ```
64    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
65        // Use Eq checking for equality
66        if self == other {
67            return Some(Ordering::Equal);
68        }
69
70        // No comparison possible if length doesn't match.
71        if self.value().len() != other.value().len() {
72            return None;
73        }
74
75        // Check all dimensions.
76        let mut has_better = false;
77        let mut has_worse = false;
78
79        for (own, other) in self.value().iter().zip(other.value()) {
80            if own < other {
81                has_better = true;
82            } else if own > other {
83                has_worse = true;
84            }
85        }
86
87        match (has_better, has_worse) {
88            // `self` dominates on all indices.
89            (true, false) => Some(Ordering::Less),
90            // `other` dominates on all indices.
91            (false, true) => Some(Ordering::Greater),
92            // `self´ and `other` are incomparable.
93            _ => None,
94        }
95    }
96}
97
98impl Objective for MultiObjective {}
99
100impl From<MultiObjective> for Vec<f64> {
101    fn from(objective: MultiObjective) -> Self {
102        objective.0
103    }
104}
105
106impl TryFrom<Vec<f64>> for MultiObjective {
107    type Error = IllegalObjective;
108
109    /// Tries to convert a vector of floats into a `MultiObjective` value.
110    ///
111    /// See [`IllegalObjective`] for more information about illegal values.
112    fn try_from(value: Vec<f64>) -> Result<Self, IllegalObjective> {
113        match value {
114            x if x.iter().any(|o| o.is_nan()) => Err(IllegalObjective::NaN),
115            x if x.iter().any(|o| o.is_infinite() && o.is_sign_negative()) => {
116                Err(IllegalObjective::NegativeInfinity)
117            }
118            _ => Ok(MultiObjective(value)),
119        }
120    }
121}
122
123impl TryFrom<&[f64]> for MultiObjective {
124    type Error = IllegalObjective;
125
126    /// Tries to convert a vector of floats into a `MultiObjective` value.
127    ///
128    /// See [`IllegalObjective`] for more information about illegal values.
129    fn try_from(value: &[f64]) -> Result<Self, IllegalObjective> {
130        match value {
131            x if x.iter().any(|o| o.is_nan()) => Err(IllegalObjective::NaN),
132            x if x.iter().any(|o| o.is_infinite() && o.is_sign_negative()) => {
133                Err(IllegalObjective::NegativeInfinity)
134            }
135            _ => Ok(MultiObjective(value.into())),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use std::cmp::Ordering;
143
144    use super::MultiObjective;
145
146    #[test]
147    fn is_finite_returns_true_for_all_finite() {
148        assert!(MultiObjective(vec![0., 0.]).is_finite());
149        assert!(MultiObjective(vec![-1., 1.]).is_finite());
150    }
151
152    #[test]
153    fn is_finite_returns_false_for_any_infinite() {
154        assert!(!MultiObjective(vec![0., f64::INFINITY]).is_finite());
155        assert!(!MultiObjective(vec![f64::INFINITY, f64::INFINITY]).is_finite());
156    }
157
158    #[test]
159    fn partial_ord_implements_pareto_domination() {
160        let a = MultiObjective(vec![0., 0.]);
161        let b = MultiObjective(vec![1., 1.]);
162        let c = MultiObjective(vec![1., 0.]);
163        let d = MultiObjective(vec![0., 1.]);
164
165        // All are equal to self.
166        assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
167        assert_eq!(b.partial_cmp(&b), Some(Ordering::Equal));
168        assert_eq!(c.partial_cmp(&c), Some(Ordering::Equal));
169        assert_eq!(d.partial_cmp(&d), Some(Ordering::Equal));
170
171        // `a` dominates `b`, `c`, and `d`.
172        assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
173        assert_eq!(a.partial_cmp(&c), Some(Ordering::Less));
174        assert_eq!(a.partial_cmp(&d), Some(Ordering::Less));
175
176        // `b` is dominated by `a`, `c`, and `d`.
177        assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
178        assert_eq!(b.partial_cmp(&c), Some(Ordering::Greater));
179        assert_eq!(b.partial_cmp(&d), Some(Ordering::Greater));
180
181        // `c` is dominated by `a`, dominates `b` and incomparable with `d`.
182        assert_eq!(c.partial_cmp(&a), Some(Ordering::Greater));
183        assert_eq!(c.partial_cmp(&b), Some(Ordering::Less));
184        assert_eq!(c.partial_cmp(&d), None);
185
186        // `d` is dominated by `a`, dominates `b` and incomparable with `c`.
187        assert_eq!(d.partial_cmp(&a), Some(Ordering::Greater));
188        assert_eq!(d.partial_cmp(&b), Some(Ordering::Less));
189        assert_eq!(d.partial_cmp(&c), None);
190    }
191
192    #[test]
193    fn try_from_invalid_is_err() {
194        assert!(MultiObjective::try_from(vec![f64::NAN, 0.]).is_err());
195        assert!(MultiObjective::try_from(vec![f64::NAN, f64::NEG_INFINITY]).is_err());
196
197        assert!(MultiObjective::try_from(vec![f64::NEG_INFINITY, 0.]).is_err());
198        assert!(MultiObjective::try_from(vec![f64::NEG_INFINITY, f64::NEG_INFINITY]).is_err());
199    }
200}