Skip to main content

radiate_core/objectives/
front.rs

1use crate::objectives::{Objective, Scored, pareto};
2#[cfg(feature = "serde")]
3use serde::{Deserialize, Serialize};
4use std::{cmp::Ordering, ops::Range};
5
6const DEFAULT_ENTROPY_BINS: usize = 20;
7
8#[derive(Debug)]
9pub struct FrontAddResult {
10    pub added_count: usize,
11    pub removed_count: usize,
12    pub comparisons: usize,
13    pub filter_count: usize,
14    pub size: usize,
15}
16
17#[derive(Clone, Default)]
18struct FrontScratch {
19    remove_buff: Vec<usize>,
20    index_buff: Vec<usize>,
21    crowding_buff: Vec<f32>,
22    filter_buff: Vec<bool>,
23}
24
25#[derive(Clone)]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27pub struct Front<T: Scored> {
28    values: Vec<T>,
29    range: Range<usize>,
30    objective: Objective,
31
32    #[cfg_attr(feature = "serde", serde(skip))]
33    scratch: FrontScratch,
34}
35
36impl<T: Scored> Front<T> {
37    pub fn new(range: Range<usize>, objective: Objective) -> Self {
38        Front {
39            values: Vec::new(),
40            range,
41            objective,
42            scratch: FrontScratch::default(),
43        }
44    }
45
46    pub fn len(&self) -> usize {
47        self.values.len()
48    }
49
50    pub fn range(&self) -> Range<usize> {
51        self.range.clone()
52    }
53
54    pub fn objective(&self) -> Objective {
55        self.objective.clone()
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.values.is_empty()
60    }
61
62    pub fn values(&self) -> &[T] {
63        &self.values
64    }
65
66    pub fn crowding_distance(&mut self) -> Option<&[f32]> {
67        let scores = self
68            .values
69            .iter()
70            .filter_map(|v| v.score())
71            .collect::<Vec<_>>();
72
73        if scores.is_empty() {
74            return None;
75        }
76
77        self.scratch.crowding_buff.clear();
78        self.scratch.crowding_buff.resize(scores.len(), 0.0);
79
80        pareto::buffered_crowding_distance(&scores, &mut self.scratch.crowding_buff);
81
82        Some(&self.scratch.crowding_buff[..])
83    }
84
85    pub fn entropy(&mut self) -> Option<f32> {
86        let scores = self
87            .values
88            .iter()
89            .filter_map(|v| v.score())
90            .collect::<Vec<_>>();
91
92        if scores.is_empty() {
93            return None;
94        }
95
96        Some(pareto::entropy(scores.as_slice(), DEFAULT_ENTROPY_BINS))
97    }
98
99    /// Calculate the hypervolume of the front with respect to the `reference` point, using the
100    /// front's own objective directions. See [pareto::hypervolume] for details.
101    ///
102    /// Returns `None` if the front is empty or the reference point's dimensions don't match
103    /// the objective.
104    pub fn hypervolume(&self, reference: &[f32]) -> Option<f32> {
105        if self.values.is_empty() || reference.len() != self.objective.dims() {
106            return None;
107        }
108
109        let scores = self
110            .values
111            .iter()
112            .filter_map(|v| v.score())
113            .collect::<Vec<_>>();
114
115        Some(pareto::hypervolume(&scores, reference, &self.objective))
116    }
117
118    pub fn try_add_all<'a>(&mut self, items: impl Iterator<Item = &'a T>) -> FrontAddResult
119    where
120        T: Eq + Clone + 'static,
121    {
122        let mut added_count = 0;
123        let mut removed_count = 0;
124        let mut comparisons = 0;
125        let mut filter_count = 0;
126
127        for new_member in items.into_iter() {
128            self.scratch.remove_buff.clear();
129
130            let mut accept = true;
131            for (idx, existing) in self.values.iter().enumerate() {
132                if existing == new_member {
133                    accept = false;
134                    break;
135                }
136
137                match self.dom_cmp(existing, new_member) {
138                    Ordering::Greater => {
139                        // existing dominates new -> reject
140                        accept = false;
141                        comparisons += 1;
142                        break;
143                    }
144                    Ordering::Less => {
145                        // new dominates existing -> mark for removal
146                        self.scratch.remove_buff.push(idx);
147                        comparisons += 1;
148                    }
149                    Ordering::Equal => comparisons += 1,
150                }
151            }
152
153            if !accept {
154                continue;
155            }
156
157            // Remove dominated existing values efficiently (swap_remove).
158            // Need stable removal: remove in descending index order.
159            if !self.scratch.remove_buff.is_empty() {
160                self.scratch.remove_buff.sort_unstable();
161                self.scratch.remove_buff.dedup();
162
163                removed_count += self.scratch.remove_buff.len();
164
165                for &idx in self.scratch.remove_buff.iter().rev() {
166                    self.values.swap_remove(idx);
167                }
168            }
169
170            self.values.push(new_member.clone());
171            added_count += 1;
172
173            if self.values.len() > self.range.end {
174                self.fast_filter();
175                filter_count += 1;
176            }
177        }
178
179        FrontAddResult {
180            added_count,
181            removed_count,
182            comparisons,
183            filter_count,
184            size: self.values.len(),
185        }
186    }
187
188    /// Remove points with crowding distance in the top `trim` fraction.
189    /// Example: trim=0.02 removes the top 2% most isolated points.
190    #[inline]
191    pub fn remove_outliers(&mut self, trim: f32) -> Option<usize> {
192        if self.values.len() < 4 {
193            return None;
194        }
195
196        let trim = trim.clamp(0.0, 0.5);
197        if trim == 0.0 {
198            return None;
199        }
200
201        let (n, _) = self.score_dims()?;
202
203        let drop = ((n as f32) * trim).floor() as usize;
204        if drop == 0 {
205            return None;
206        }
207
208        let scores = self
209            .values
210            .iter()
211            .filter_map(|v| v.score())
212            .collect::<Vec<_>>();
213
214        self.scratch.crowding_buff.clear();
215        self.scratch.crowding_buff.resize(scores.len(), 0.0);
216
217        self.scratch.index_buff.clear();
218        self.scratch.index_buff.extend(0..scores.len());
219
220        pareto::buffered_crowding_distance(&scores, &mut self.scratch.crowding_buff);
221
222        self.scratch.index_buff.sort_unstable_by(|&i, &j| {
223            self.scratch.crowding_buff[j]
224                .partial_cmp(&self.scratch.crowding_buff[i])
225                .unwrap_or(Ordering::Equal)
226        });
227
228        self.scratch.index_buff.truncate(drop);
229        self.scratch.index_buff.sort_unstable();
230        self.scratch.index_buff.dedup();
231
232        let removed = self.scratch.index_buff.len();
233        for &idx in self.scratch.index_buff.iter().rev() {
234            self.values.swap_remove(idx);
235        }
236
237        Some(removed)
238    }
239
240    pub fn fronts(&mut self) -> Vec<Front<T>>
241    where
242        T: Clone + Eq + Send + Sync + 'static,
243    {
244        let mut fronts: Vec<Front<T>> = Vec::new();
245        for member in self.values.iter() {
246            let mut updated = false;
247
248            for front in fronts.iter_mut() {
249                let result = front.try_add_all(std::iter::once(member));
250
251                if result.added_count > 0 {
252                    updated = true;
253                    break;
254                }
255            }
256
257            if !updated {
258                let mut new_front = Front::new(self.range.clone(), self.objective.clone());
259                new_front.try_add_all(std::iter::once(member));
260                fronts.push(new_front);
261            }
262        }
263
264        fronts
265    }
266
267    fn fast_filter(&mut self) {
268        let keep = self.range.start.min(self.values.len());
269        if keep == 0 || self.values.len() <= keep {
270            return;
271        }
272
273        let scores = self
274            .values
275            .iter()
276            .filter_map(|v| v.score())
277            .collect::<Vec<_>>();
278
279        self.scratch.crowding_buff.clear();
280        self.scratch.crowding_buff.resize(scores.len(), 0.0);
281
282        self.scratch.index_buff.clear();
283        self.scratch.index_buff.extend(0..scores.len());
284
285        pareto::buffered_crowding_distance(&scores, &mut self.scratch.crowding_buff);
286
287        self.scratch
288            .index_buff
289            .select_nth_unstable_by(keep, |&a, &b| {
290                self.scratch.crowding_buff[b]
291                    .partial_cmp(&self.scratch.crowding_buff[a])
292                    .unwrap_or(Ordering::Equal)
293            });
294        self.scratch.index_buff.truncate(keep);
295
296        self.retain_indices();
297    }
298
299    #[inline]
300    fn dom_cmp(&self, one: &T, two: &T) -> Ordering {
301        let one_score = one.score();
302        let two_score = two.score();
303
304        if one_score.is_none() || two_score.is_none() {
305            return Ordering::Equal;
306        }
307
308        if let Some((a, b)) = one_score.zip(two_score) {
309            if pareto::dominance(a, b, &self.objective) {
310                return Ordering::Greater;
311            } else if pareto::dominance(b, a, &self.objective) {
312                return Ordering::Less;
313            }
314        }
315        Ordering::Equal
316    }
317
318    /// Keep only the elements at `indices`, in `self.values`' current order.
319    /// Reuses `scratch.keep_true` as scan-line scratch instead of allocating
320    /// a fresh mask on every call.
321    fn retain_indices(&mut self) {
322        self.scratch.filter_buff.clear();
323        self.scratch.filter_buff.resize(self.values.len(), false);
324
325        for &idx in self.scratch.index_buff.iter() {
326            self.scratch.filter_buff[idx] = true;
327        }
328
329        // Bind disjoint fields locally so the borrow checker sees this as
330        // two separate borrows of `self.values` and `self.scratch.keep_true`
331        // rather than one borrow of `self`.
332        let values = &mut self.values;
333        let keep_true = &self.scratch.filter_buff;
334
335        let mut idx = 0;
336        values.retain(|_| {
337            let retain = keep_true[idx];
338            idx += 1;
339            retain
340        });
341    }
342
343    #[inline]
344    fn score_dims(&self) -> Option<(usize, usize)> {
345        let n = self.values.len();
346
347        if n == 0 {
348            return None;
349        }
350
351        let first = self.values.iter().find_map(|v| v.score())?;
352        Some((n, first.len()))
353    }
354}
355
356impl<T> Default for Front<T>
357where
358    T: Scored,
359{
360    fn default() -> Self {
361        Front::new(0..0, Objective::default())
362    }
363}