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 ¤t_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// }