radiate_core/objectives/
pareto.rs

1use crate::objectives::{Objective, Optimize};
2
3/// Calculate the crowding distance for each score in a population.
4///
5/// The crowding distance is a measure of how close a score is to its neighbors
6/// in the objective space. Scores with a higher crowding distance are more
7/// desirable because they are more spread out. This is useful for selecting
8/// diverse solutions in a multi-objective optimization problem and is a
9/// key component of the NSGA-II algorithm.
10pub fn crowding_distance<T: AsRef<[f32]>>(scores: &[T]) -> Vec<f32> {
11    let indices = scores
12        .iter()
13        .enumerate()
14        .map(|(i, score)| (score.as_ref(), i))
15        .collect::<Vec<(&[f32], usize)>>();
16
17    let mut result = vec![0.0; scores.len()];
18
19    for i in 0..indices[0].0.len() {
20        let mut distance_values = indices.clone();
21        distance_values.sort_by(|a, b| a.0[i].partial_cmp(&b.0[i]).unwrap());
22
23        let min = indices[distance_values[0].1];
24        let max = indices[distance_values[distance_values.len() - 1].1];
25
26        let dm = distance(max.0, min.0, i);
27
28        if dm == 0.0 {
29            continue;
30        }
31
32        result[min.1] = f32::INFINITY;
33        result[max.1] = f32::INFINITY;
34
35        for j in 1..distance_values.len() - 1 {
36            let prev = indices[distance_values[j - 1].1];
37            let next = indices[distance_values[j + 1].1];
38            let dp = distance(next.0, prev.0, i);
39
40            result[distance_values[j].1] += dp;
41        }
42    }
43
44    result
45}
46
47pub fn non_dominated<T: AsRef<[f32]>>(population: &[T], objective: &Objective) -> Vec<usize> {
48    let mut dominated_counts = vec![0; population.len()];
49    let mut dominates = vec![Vec::new(); population.len()];
50
51    for i in 0..population.len() {
52        for j in (i + 1)..population.len() {
53            let score_one = &population[i];
54            let score_two = &population[j];
55            if dominance(score_one, score_two, objective) {
56                dominates[i].push(j);
57                dominated_counts[j] += 1;
58            } else if dominance(score_two, score_one, objective) {
59                dominates[j].push(i);
60                dominated_counts[i] += 1;
61            }
62        }
63    }
64
65    let mut non_dominated = Vec::new();
66    for i in 0..population.len() {
67        if dominated_counts[i] == 0 {
68            non_dominated.push(i);
69        }
70    }
71
72    non_dominated
73}
74
75/// Rank the population based on the NSGA-II algorithm. This assigns a rank to each
76/// individual in the population based on their dominance relationships with other
77/// individuals in the population. The result is a vector of ranks, where the rank
78/// of the individual at index `i` is `ranks[i]`.
79pub fn rank<T: AsRef<[f32]>>(population: &[T], objective: &Objective) -> Vec<usize> {
80    let mut dominated_counts = vec![0; population.len()];
81    let mut dominates = vec![Vec::new(); population.len()];
82    let mut current_front: Vec<usize> = Vec::new();
83    let mut dominance_matrix = vec![vec![0; population.len()]; population.len()];
84
85    for i in 0..population.len() {
86        for j in (i + 1)..population.len() {
87            let score_one = &population[i];
88            let score_two = &population[j];
89            if dominance(score_one, score_two, objective) {
90                dominance_matrix[i][j] = 1;
91                dominance_matrix[j][i] = -1;
92            } else if dominance(score_two, score_one, objective) {
93                dominance_matrix[i][j] = -1;
94                dominance_matrix[j][i] = 1;
95            }
96        }
97    }
98
99    for i in 0..population.len() {
100        for j in 0..population.len() {
101            if i != j {
102                if dominance_matrix[i][j] == 1 {
103                    dominates[i].push(j);
104                } else if dominance_matrix[i][j] == -1 {
105                    dominated_counts[i] += 1;
106                }
107            }
108        }
109
110        // If no one dominates this solution, it belongs to the first front
111        if dominated_counts[i] == 0 {
112            current_front.push(i);
113        }
114    }
115
116    // Assign ranks based on fronts
117    let mut ranks = vec![0; population.len()];
118    let mut front_idx = 0;
119
120    while !current_front.is_empty() {
121        let mut next_front = Vec::new();
122
123        for &p in &current_front {
124            ranks[p] = front_idx;
125
126            for &q in &dominates[p] {
127                dominated_counts[q] -= 1;
128                if dominated_counts[q] == 0 {
129                    next_front.push(q);
130                }
131            }
132        }
133
134        front_idx += 1;
135        current_front = next_front;
136    }
137
138    ranks
139}
140
141pub fn weights<T: AsRef<[f32]>>(scores: &[T], objective: &Objective) -> Vec<f32> {
142    let ranks = rank(scores, objective);
143    let distances = crowding_distance(scores);
144
145    let rank_weight = ranks
146        .iter()
147        .map(|r| 1.0 / (*r as f32))
148        .collect::<Vec<f32>>();
149    let max_crowding = distances.iter().cloned().fold(0.0, f32::max);
150    let crowding_weight = distances
151        .iter()
152        .map(|d| 1.0 - d / max_crowding)
153        .collect::<Vec<f32>>();
154
155    rank_weight
156        .iter()
157        .zip(crowding_weight.iter())
158        .map(|(r, c)| r * c)
159        .collect::<Vec<f32>>()
160}
161
162// Determine if one score dominates another score. A score `a` dominates a score `b`
163// if it is better in every objective and at least one objective is strictly better.
164pub fn dominance<K: PartialOrd, T: AsRef<[K]>>(
165    score_a: T,
166    score_b: T,
167    objective: &Objective,
168) -> bool {
169    let mut better_in_any = false;
170
171    match objective {
172        Objective::Single(opt) => {
173            for (a, b) in score_a.as_ref().iter().zip(score_b.as_ref().iter()) {
174                if opt == &Optimize::Minimize {
175                    if a > b {
176                        return false;
177                    }
178                    if a < b {
179                        better_in_any = true;
180                    }
181                } else {
182                    if a < b {
183                        return false;
184                    }
185                    if a > b {
186                        better_in_any = true;
187                    }
188                }
189            }
190        }
191        Objective::Multi(opts) => {
192            for ((a, b), opt) in score_a.as_ref().iter().zip(score_b.as_ref()).zip(opts) {
193                if opt == &Optimize::Minimize {
194                    if a > b {
195                        return false;
196                    }
197                    if a < b {
198                        better_in_any = true;
199                    }
200                } else {
201                    if a < b {
202                        return false;
203                    }
204                    if a > b {
205                        better_in_any = true;
206                    }
207                }
208            }
209        }
210    }
211
212    better_in_any
213}
214
215/// Calculate the Pareto front of a set of scores. The Pareto front is the set of
216/// scores that are not dominated by any other score in the set. This is useful
217/// for selecting the best solutions in a multi-objective optimization problem.
218pub fn pareto_front<K: PartialOrd, T: AsRef<[K]> + Clone>(
219    values: &[T],
220    objective: &Objective,
221) -> Vec<T> {
222    let mut front = Vec::new();
223    for score in values {
224        let mut dominated = false;
225        for other in values {
226            if dominance(other, score, objective) {
227                dominated = true;
228                break;
229            }
230        }
231        if !dominated {
232            front.push(score.clone());
233        }
234    }
235
236    front
237}
238
239/// Calculate the distance between two scores in the objective space. This is used
240/// to calculate the crowding distance for each score in a population.
241fn distance<T: AsRef<[f32]>>(a: T, b: T, index: usize) -> f32 {
242    (a.as_ref()[index] - b.as_ref()[index]).abs()
243}
244
245// pub fn dominance<K: PartialOrd, T: AsRef<[K]>>(
246//     score_a: T,
247//     score_b: T,
248//     objective: &Objective,
249// ) -> bool {
250//     let score_a_ref = score_a.as_ref();
251//     let score_b_ref = score_b.as_ref();
252
253//     if score_a_ref.len() != score_b_ref.len() {
254//         return false;
255//     }
256
257//     let mut better_in_any = false;
258//     let mut worse_in_any = false;
259
260//     match objective {
261//         Objective::Single(opt) => {
262//             for (a, b) in score_a_ref.iter().zip(score_b_ref.iter()) {
263//                 match opt {
264//                     Optimize::Minimize => {
265//                         if a > b {
266//                             worse_in_any = true;
267//                         } else if a < b {
268//                             better_in_any = true;
269//                         }
270//                     }
271//                     Optimize::Maximize => {
272//                         if a < b {
273//                             worse_in_any = true;
274//                         } else if a > b {
275//                             better_in_any = true;
276//                         }
277//                     }
278//                 }
279//             }
280//         }
281
282//         Objective::Multi(opts) => {
283//             for ((a, b), opt) in score_a_ref.iter().zip(score_b_ref.iter()).zip(opts) {
284//                 match opt {
285//                     Optimize::Minimize => {
286//                         if a > b {
287//                             worse_in_any = true;
288//                         } else if a < b {
289//                             better_in_any = true;
290//                         }
291//                     }
292//                     Optimize::Maximize => {
293//                         if a < b {
294//                             worse_in_any = true;
295//                         } else if a > b {
296//                             better_in_any = true;
297//                         }
298//                     }
299//                 }
300//             }
301//         }
302//     }
303
304//     // a dominates b if it's at least as good in all objectives and better in at least one
305//     better_in_any && !worse_in_any
306// }