Skip to main content

dahl_salso/
optimize.rs

1use crate::clustering::{Clusterings, WorkingClustering};
2use crate::log2cache::Log2Cache;
3use crate::loss::*;
4use crate::*;
5use dahl_partition::*;
6use ndarray::{Array2, Array3, Axis};
7use rand::distr::{Distribution, Uniform};
8use rand::seq::SliceRandom;
9use rand::{Rng, SeedableRng};
10use rand_pcg::Pcg64Mcg;
11use std::sync::mpsc;
12use std::time::Instant;
13
14// **************************************************************
15// Implementation of SALSO for losses based on confusion matrices
16// **************************************************************
17
18pub trait CMLossComputer {
19    #[allow(unused_variables)]
20    fn initialize(&mut self, state: &WorkingClustering, cms: &Array3<CountType>) {}
21
22    fn compute_loss(&self, state: &WorkingClustering, cms: &Array3<CountType>) -> f64;
23
24    fn change_in_loss(
25        &self,
26        item_index: usize,
27        to_label: LabelType,
28        from_label_option: Option<LabelType>,
29        state: &WorkingClustering,
30        cms: &Array3<CountType>,
31        draws: &Clusterings,
32    ) -> f64;
33
34    #[allow(unused_variables)]
35    fn decision_callback(
36        &mut self,
37        item_index: usize,
38        to_label_option: Option<LabelType>,
39        from_label_option: Option<LabelType>,
40        state: &WorkingClustering,
41        cms: &Array3<CountType>,
42        draws: &Clusterings,
43    ) {
44    }
45}
46
47// Expectation of the Binder loss
48
49pub struct BinderCMLossComputer {
50    a: f64,
51}
52
53impl BinderCMLossComputer {
54    pub fn new(a: f64) -> Self {
55        Self { a }
56    }
57
58    pub fn n_squared(x: CountType) -> f64 {
59        let x = x as f64;
60        x * x
61    }
62}
63
64impl CMLossComputer for BinderCMLossComputer {
65    fn compute_loss(&self, state: &WorkingClustering, cms: &Array3<CountType>) -> f64 {
66        let n_draws = cms.len_of(Axis(2));
67        let sum2 = state
68            .occupied_clusters()
69            .iter()
70            .map(|i| BinderCMLossComputer::n_squared(state.size_of(*i)))
71            .sum::<f64>()
72            * n_draws as f64;
73        let mut sum1 = 0.0;
74        let mut sum3 = 0.0;
75        for draw_index in 0..n_draws {
76            for other_index in 0..cms.len_of(Axis(1)) {
77                let n = cms[(0, other_index, draw_index)];
78                if n > 0 {
79                    sum1 += BinderCMLossComputer::n_squared(cms[(0, other_index, draw_index)]);
80                    for main_label in state.occupied_clusters().iter() {
81                        sum3 += BinderCMLossComputer::n_squared(
82                            cms[(*main_label as usize + 1, other_index, draw_index)],
83                        );
84                    }
85                }
86            }
87        }
88        (self.a * sum1 + (2.0 - self.a) * sum2 - 2.0 * sum3)
89            / (n_draws as f64 * BinderCMLossComputer::n_squared(state.n_items()))
90    }
91
92    fn change_in_loss(
93        &self,
94        item_index: usize,
95        to_label: LabelType,
96        from_label_option: Option<LabelType>,
97        state: &WorkingClustering,
98        cms: &Array3<CountType>,
99        draws: &Clusterings,
100    ) -> f64 {
101        let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
102            1
103        } else {
104            0
105        };
106        let n_draws = cms.len_of(Axis(2));
107        let sum2 = (n_draws as f64) * ((state.size_of(to_label) - offset) as f64);
108        let to_index = to_label as usize + 1;
109        let mut sum3 = 0.0;
110        for draw_index in 0..n_draws {
111            let other_index = draws.label(draw_index, item_index) as usize;
112            sum3 += (cms[(to_index, other_index, draw_index)] - offset) as f64;
113        }
114        (2.0 - self.a) * sum2 - 2.0 * sum3
115    }
116}
117
118// Expectation of one minus the adjusted Rand index loss
119
120pub struct OMARICMLossComputer {
121    n: CountType,
122    sum: f64,
123    sums: Array2<f64>,
124}
125
126impl OMARICMLossComputer {
127    pub fn new(n_draws: usize) -> Self {
128        Self {
129            n: 0,
130            sum: 0.0,
131            sums: Array2::<f64>::zeros((n_draws, 2)),
132        }
133    }
134
135    pub fn n_choose_2_times_2(x: CountType) -> f64 {
136        let x = x as f64;
137        x * (x - 1.0)
138    }
139}
140
141impl CMLossComputer for OMARICMLossComputer {
142    fn initialize(&mut self, state: &WorkingClustering, cms: &Array3<CountType>) {
143        self.n = state.n_items();
144        self.sum = state
145            .occupied_clusters()
146            .iter()
147            .map(|i| OMARICMLossComputer::n_choose_2_times_2(state.size_of(*i)))
148            .sum();
149        let n_draws = cms.len_of(Axis(2));
150        for draw_index in 0..n_draws {
151            for other_index in 0..cms.len_of(Axis(1)) {
152                let n = cms[(0, other_index, draw_index)];
153                if n > 0 {
154                    self.sums[(draw_index, 0)] += OMARICMLossComputer::n_choose_2_times_2(n);
155                    for main_label in state.occupied_clusters().iter() {
156                        self.sums[(draw_index, 1)] += OMARICMLossComputer::n_choose_2_times_2(
157                            cms[(*main_label as usize + 1, other_index, draw_index)],
158                        );
159                    }
160                }
161            }
162        }
163    }
164
165    fn compute_loss(&self, _state: &WorkingClustering, _cms: &Array3<CountType>) -> f64 {
166        let mut sum = 0.0;
167        let sum2 = self.sum;
168        let sum2_div_denom = sum2 / OMARICMLossComputer::n_choose_2_times_2(self.n);
169        let n_draws = self.sums.len_of(Axis(0));
170        for draw_index in 0..n_draws {
171            let sum1 = self.sums[(draw_index, 0)];
172            let offset = sum1 * sum2_div_denom;
173            let denominator = 0.5 * (sum1 + sum2) - offset;
174            if denominator > 0.0 {
175                let numerator = self.sums[(draw_index, 1)] - offset;
176                sum += numerator / denominator;
177            }
178        }
179        1.0 - sum / (n_draws as f64)
180    }
181
182    fn change_in_loss(
183        &self,
184        item_index: usize,
185        to_label: LabelType,
186        from_label_option: Option<LabelType>,
187        state: &WorkingClustering,
188        cms: &Array3<CountType>,
189        draws: &Clusterings,
190    ) -> f64 {
191        let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
192            1
193        } else {
194            0
195        };
196        let mut sum2 = if offset == 0 {
197            self.sum + 2.0 * state.size_of(to_label) as f64
198        } else {
199            self.sum
200        };
201        let to_index = to_label as usize + 1;
202        let mut n = self.n;
203        let from_index = match from_label_option {
204            Some(from_label) => {
205                if offset == 0 {
206                    sum2 -= 2.0 * (state.size_of(from_label) - 1) as f64;
207                }
208                from_label as usize + 1
209            }
210            None => {
211                n += 1;
212                0
213            }
214        };
215        let sum2_div_denom = sum2 / OMARICMLossComputer::n_choose_2_times_2(n);
216        let mut sum = 0.0;
217        let n_draws = self.sums.len_of(Axis(0));
218        for draw_index in 0..n_draws {
219            let other_index = draws.label(draw_index, item_index) as usize;
220            let sum1 = self.sums[(draw_index, 0)]
221                + if from_label_option.is_none() {
222                    2.0 * n as f64
223                } else {
224                    0.0
225                };
226            let chance_adjustment = sum1 * sum2_div_denom;
227            let denominator = 0.5 * (sum1 + sum2) - chance_adjustment;
228            if denominator > 0.0 {
229                let mut unadjusted_numerator = self.sums[(draw_index, 1)];
230                if offset == 0 {
231                    unadjusted_numerator += 2.0 * cms[(to_index, other_index, draw_index)] as f64;
232                    if from_label_option.is_some() {
233                        unadjusted_numerator -=
234                            2.0 * (cms[(from_index, other_index, draw_index)] - 1) as f64;
235                    }
236                }
237                let numerator = unadjusted_numerator - chance_adjustment;
238                sum += numerator / denominator;
239            }
240        }
241        -sum
242    }
243
244    fn decision_callback(
245        &mut self,
246        item_index: usize,
247        to_label_option: Option<LabelType>,
248        from_label_option: Option<LabelType>,
249        state: &WorkingClustering,
250        cms: &Array3<CountType>,
251        draws: &Clusterings,
252    ) {
253        let to_index = match to_label_option {
254            Some(to_label) => {
255                self.sum += 2.0 * state.size_of(to_label) as f64;
256                to_label as usize + 1
257            }
258            None => {
259                self.n -= 1;
260                0
261            }
262        };
263        let from_index = match from_label_option {
264            Some(from_label) => {
265                self.sum -= 2.0 * (state.size_of(from_label) - 1) as f64;
266                from_label as usize + 1
267            }
268            None => {
269                self.n += 1;
270                0
271            }
272        };
273        let n_draws = draws.n_clusterings();
274        for draw_index in 0..n_draws {
275            let other_index = draws.label(draw_index, item_index) as usize;
276            if from_label_option.is_some() {
277                self.sums[(draw_index, 1)] -=
278                    2.0 * (cms[(from_index, other_index, draw_index)] - 1) as f64;
279            } else {
280                self.sums[(draw_index, 0)] += 2.0 * cms[(0, other_index, draw_index)] as f64;
281            }
282            if to_label_option.is_some() {
283                self.sums[(draw_index, 1)] += 2.0 * cms[(to_index, other_index, draw_index)] as f64;
284            } else {
285                self.sums[(draw_index, 0)] -= 2.0 * (cms[(0, other_index, draw_index)] - 1) as f64;
286            }
287        }
288    }
289}
290
291// Expectation of variation of information loss
292
293pub struct VICMLossComputer<'a> {
294    a: f64,
295    cache: &'a Log2Cache,
296}
297
298impl<'a> VICMLossComputer<'a> {
299    pub fn new(a: f64, cache: &'a Log2Cache) -> Self {
300        Self { a, cache }
301    }
302}
303
304impl CMLossComputer for VICMLossComputer<'_> {
305    fn compute_loss(&self, state: &WorkingClustering, cms: &Array3<CountType>) -> f64 {
306        let sum2: f64 = state
307            .occupied_clusters()
308            .iter()
309            .map(|i| self.cache.nlog2n(state.size_of(*i)))
310            .sum();
311        let n_draws = cms.len_of(Axis(2));
312        let mut sum = 0.0;
313        for draw_index in 0..n_draws {
314            let mut sum1 = 0.0;
315            let mut sum3 = 0.0;
316            for other_index in 0..cms.len_of(Axis(1)) {
317                let n = cms[(0, other_index, draw_index)];
318                if n > 0 {
319                    sum1 += self.cache.nlog2n(cms[(0, other_index, draw_index)]);
320                    for main_label in state.occupied_clusters().iter() {
321                        sum3 += self
322                            .cache
323                            .nlog2n(cms[(*main_label as usize + 1, other_index, draw_index)]);
324                    }
325                }
326            }
327            sum += self.a * sum1 - 2.0 * sum3;
328        }
329        ((2.0 - self.a) * sum2 + (sum / (n_draws as f64))) / (state.n_items() as f64)
330    }
331
332    fn change_in_loss(
333        &self,
334        item_index: usize,
335        to_label: LabelType,
336        from_label_option: Option<LabelType>,
337        state: &WorkingClustering,
338        cms: &Array3<CountType>,
339        draws: &Clusterings,
340    ) -> f64 {
341        let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
342            1
343        } else {
344            0
345        };
346        let n_draws = cms.len_of(Axis(2));
347        assert_eq!(n_draws, draws.n_clusterings(), "cms and draws disagree");
348        let sum2 = (n_draws as f64)
349            * self
350                .cache
351                .nlog2n_difference(state.size_of(to_label) - offset);
352        let to_index = to_label as usize + 1;
353        let mut sum3 = 0.0;
354        // Zipping a range with a contiguous slice keeps per-draw bounds checks out of this hot loop.
355        for (draw_index, other_index) in (0..n_draws).zip(draws.labels_of_item(item_index)) {
356            sum3 += self
357                .cache
358                .nlog2n_difference(cms[(to_index, *other_index as usize, draw_index)] - offset);
359        }
360        (2.0 - self.a) * sum2 - 2.0 * sum3
361    }
362}
363
364// Expectation of general information based loss
365
366pub struct GeneralInformationBasedCMLossComputer<'a, T>
367where
368    T: InformationBasedLoss,
369{
370    cache: &'a Log2Cache,
371    n: CountType,
372    sum: f64,
373    sums: Array2<f64>,
374    ibloss: T,
375}
376
377impl<'a, T> GeneralInformationBasedCMLossComputer<'a, T>
378where
379    T: InformationBasedLoss,
380{
381    pub fn new(n_draws: usize, cache: &'a Log2Cache, ibloss: T) -> Self {
382        Self {
383            cache,
384            n: 0,
385            sum: 0.0,
386            sums: Array2::<f64>::zeros((n_draws, 2)),
387            ibloss,
388        }
389    }
390}
391
392impl<T> CMLossComputer for GeneralInformationBasedCMLossComputer<'_, T>
393where
394    T: InformationBasedLoss,
395{
396    fn initialize(&mut self, state: &WorkingClustering, cms: &Array3<CountType>) {
397        self.n = state.n_items();
398        self.sum = state
399            .occupied_clusters()
400            .iter()
401            .map(|i| self.cache.nlog2n(state.size_of(*i)))
402            .sum();
403        let n_draws = cms.len_of(Axis(2));
404        for draw_index in 0..n_draws {
405            for other_index in 0..cms.len_of(Axis(1)) {
406                let n = cms[(0, other_index, draw_index)];
407                if n > 0 {
408                    self.sums[(draw_index, 0)] +=
409                        self.cache.nlog2n(cms[(0, other_index, draw_index)]);
410                    for main_label in state.occupied_clusters().iter() {
411                        self.sums[(draw_index, 1)] += self
412                            .cache
413                            .nlog2n(cms[(*main_label as usize + 1, other_index, draw_index)]);
414                    }
415                }
416            }
417        }
418    }
419
420    fn compute_loss(&self, _state: &WorkingClustering, _cms: &Array3<CountType>) -> f64 {
421        let ni = self.n as f64;
422        let nlog2n = ni * ni.log2();
423        let u = self.sum;
424        let mut sum = 0.0;
425        let n_draws = self.sums.len_of(Axis(0));
426        for draw_index in 0..n_draws {
427            let v = self.sums[(draw_index, 0)];
428            let uv = self.sums[(draw_index, 1)];
429            sum += self.ibloss.compute_kernel(u, v, uv, nlog2n);
430        }
431        self.ibloss.finalize(sum, n_draws as f64, ni)
432    }
433
434    fn change_in_loss(
435        &self,
436        item_index: usize,
437        to_label: LabelType,
438        from_label_option: Option<LabelType>,
439        state: &WorkingClustering,
440        cms: &Array3<CountType>,
441        draws: &Clusterings,
442    ) -> f64 {
443        let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
444            1
445        } else {
446            0
447        };
448        let mut v = if offset == 0 {
449            self.sum + self.cache.nlog2n_difference(state.size_of(to_label))
450        } else {
451            self.sum
452        };
453        let to_index = to_label as usize + 1;
454        let (n, from_index) = match from_label_option {
455            Some(from_label) => {
456                if offset == 0 {
457                    v -= self.cache.nlog2n_difference(state.size_of(from_label) - 1);
458                }
459                (self.n, from_label as usize + 1)
460            }
461            None => (self.n + 1, 0),
462        };
463        let ni = n as f64;
464        let nlog2n = ni * ni.log2();
465        let mut sum = 0.0;
466        let n_draws = self.sums.len_of(Axis(0));
467        for draw_index in 0..n_draws {
468            let other_index = draws.label(draw_index, item_index) as usize;
469            let u = self.sums[(draw_index, 0)]
470                + if from_label_option.is_none() {
471                    self.cache
472                        .nlog2n_difference(cms[(0, other_index, draw_index)])
473                } else {
474                    0.0
475                };
476            let mut uv = self.sums[(draw_index, 1)];
477            if offset == 0 {
478                uv += self
479                    .cache
480                    .nlog2n_difference(cms[(to_index, other_index, draw_index)]);
481                if from_label_option.is_some() {
482                    uv -= self
483                        .cache
484                        .nlog2n_difference(cms[(from_index, other_index, draw_index)] - 1)
485                }
486            }
487            sum += self.ibloss.compute_kernel(u, v, uv, nlog2n);
488        }
489        self.ibloss.score(sum)
490    }
491
492    fn decision_callback(
493        &mut self,
494        item_index: usize,
495        to_label_option: Option<LabelType>,
496        from_label_option: Option<LabelType>,
497        state: &WorkingClustering,
498        cms: &Array3<CountType>,
499        draws: &Clusterings,
500    ) {
501        let to_index = match to_label_option {
502            Some(to_label) => {
503                self.sum += self.cache.nlog2n_difference(state.size_of(to_label));
504                to_label as usize + 1
505            }
506            None => {
507                self.n -= 1;
508                0
509            }
510        };
511        let from_index = match from_label_option {
512            Some(from_label) => {
513                self.sum -= self.cache.nlog2n_difference(state.size_of(from_label) - 1);
514                from_label as usize + 1
515            }
516            None => {
517                self.n += 1;
518                0
519            }
520        };
521        let n_draws = draws.n_clusterings();
522        for draw_index in 0..n_draws {
523            let other_index = draws.label(draw_index, item_index) as usize;
524            if from_label_option.is_some() {
525                self.sums[(draw_index, 1)] -= self
526                    .cache
527                    .nlog2n_difference(cms[(from_index, other_index, draw_index)] - 1);
528            } else {
529                self.sums[(draw_index, 0)] += self
530                    .cache
531                    .nlog2n_difference(cms[(0, other_index, draw_index)]);
532            }
533            if to_label_option.is_some() {
534                self.sums[(draw_index, 1)] += self
535                    .cache
536                    .nlog2n_difference(cms[(to_index, other_index, draw_index)]);
537            } else {
538                self.sums[(draw_index, 0)] -= self
539                    .cache
540                    .nlog2n_difference(cms[(0, other_index, draw_index)] - 1);
541            }
542        }
543    }
544}
545
546pub trait InformationBasedLoss {
547    fn compute_kernel(&self, u: f64, v: f64, uv: f64, nlog2n: f64) -> f64;
548    fn finalize(&self, sum: f64, n_draws: f64, n_items: f64) -> f64;
549    fn score(&self, sum: f64) -> f64;
550}
551
552// Expectation of normalized variation of information loss
553
554pub struct NVIInformationBasedLoss;
555
556impl InformationBasedLoss for NVIInformationBasedLoss {
557    fn compute_kernel(&self, u: f64, v: f64, uv: f64, nlog2n: f64) -> f64 {
558        // Identical partitions have zero loss, including when both entropies are zero.
559        if u == uv && v == uv {
560            return 0.0;
561        }
562        (u + v - 2.0 * uv) / (nlog2n - uv)
563    }
564    fn finalize(&self, sum: f64, n_draws: f64, _n_items: f64) -> f64 {
565        sum / n_draws
566    }
567    fn score(&self, sum: f64) -> f64 {
568        sum
569    }
570}
571
572// Expectation of information distance loss
573
574pub struct IDInformationBasedLoss;
575
576impl InformationBasedLoss for IDInformationBasedLoss {
577    fn compute_kernel(&self, u: f64, v: f64, uv: f64, _nlog2n: f64) -> f64 {
578        u + v - uv - u.min(v)
579    }
580    fn finalize(&self, sum: f64, n_draws: f64, n_items: f64) -> f64 {
581        sum / (n_draws * n_items)
582    }
583    fn score(&self, sum: f64) -> f64 {
584        sum
585    }
586}
587
588// Expectation of normalized information distance loss
589
590pub struct NIDInformationBasedLoss;
591
592impl InformationBasedLoss for NIDInformationBasedLoss {
593    fn compute_kernel(&self, u: f64, v: f64, uv: f64, nlog2n: f64) -> f64 {
594        // This kernel is similarity: finalize converts one to zero loss.
595        // Handle identical partitions before dividing by a possibly zero entropy.
596        if u == uv && v == uv {
597            return 1.0;
598        }
599        (nlog2n + uv - u - v) / (nlog2n - u.min(v))
600    }
601    fn finalize(&self, sum: f64, n_draws: f64, _n_items: f64) -> f64 {
602        1.0 - sum / n_draws
603    }
604    fn score(&self, sum: f64) -> f64 {
605        -sum
606    }
607}
608
609// Common
610
611fn find_label_of_minimum<I: Iterator<Item = (LabelType, f64)>>(pairs: I) -> LabelType {
612    let mut s0 = f64::INFINITY;
613    let mut l0 = 0;
614    for pair in pairs {
615        if pair.1 < s0 {
616            s0 = pair.1;
617            l0 = pair.0;
618        }
619    }
620    l0
621}
622
623fn allocation_scan<T: CMLossComputer>(
624    sweetening_scan: bool,
625    singletons_initialization: bool,
626    state: &mut WorkingClustering,
627    cms: &mut Array3<CountType>,
628    permutation: &[usize],
629    loss_computer: &mut T,
630    draws: &Clusterings,
631) -> bool {
632    let mut state_changed = false;
633    let mut chosen_label_option = None;
634    for item_index in permutation {
635        let item_index = *item_index;
636        let label_of_empty_cluster = state.label_of_empty_cluster();
637        let from_label_option = match sweetening_scan {
638            true => Some(state.get(item_index)),
639            false => None,
640        };
641        let to_label = match label_of_empty_cluster {
642            Some(loec) if !sweetening_scan && singletons_initialization => loec,
643            Some(_) | None => {
644                let iter = state
645                    .occupied_clusters()
646                    .iter()
647                    .chain(label_of_empty_cluster.iter())
648                    .map(|to_label| {
649                        (
650                            *to_label,
651                            loss_computer.change_in_loss(
652                                item_index,
653                                *to_label,
654                                from_label_option,
655                                state,
656                                cms,
657                                draws,
658                            ),
659                        )
660                    });
661                find_label_of_minimum(iter)
662            }
663        };
664        if !sweetening_scan {
665            state.assign(item_index, to_label, loss_computer, cms, draws);
666            if chosen_label_option.is_none() {
667                chosen_label_option = Some(to_label);
668                state_changed = state.size_of(to_label) != 1;
669            }
670            if to_label != chosen_label_option.unwrap() {
671                state_changed = true;
672            }
673        } else if to_label != from_label_option.unwrap() {
674            state.reassign(item_index, to_label, loss_computer, cms, draws);
675            state_changed = true;
676        }
677    }
678    state_changed
679}
680
681#[allow(clippy::too_many_arguments)]
682fn sweetening_scans<T: CMLossComputer, U: Rng>(
683    state: &mut WorkingClustering,
684    cms: &mut Array3<CountType>,
685    permutation: &mut [usize],
686    loss_computer: &mut T,
687    scan_counter: &mut u32,
688    draws: &Clusterings,
689    p: &SALSOParameters,
690    rng: &mut U,
691) -> bool {
692    let mut global_state_changed = false;
693    let mut state_changed = true;
694    while state_changed && *scan_counter < p.max_scans {
695        *scan_counter += 1;
696        permutation.shuffle(rng);
697        state_changed = allocation_scan(true, false, state, cms, permutation, loss_computer, draws);
698        global_state_changed = global_state_changed || state_changed;
699    }
700    global_state_changed
701}
702
703pub fn minimize_once_by_salso_v2<'a, T: CMLossComputer, U: Rng>(
704    loss_computer_factory: Box<dyn Fn() -> T + 'a>,
705    draws: &Clusterings,
706    p: &SALSOParameters,
707    seconds_target: f64,
708    rng: &mut U,
709) -> SALSOResults {
710    let n_items = draws.n_items();
711    let max_size = match p.max_size {
712        0 => draws.max_clusters(),
713        _ => p.max_size,
714    };
715    let mut permutation: Vec<usize> = (0..p.n_items).collect();
716    let mut best = SALSOResults::dummy(max_size);
717    let start_time = Instant::now();
718    let uniform_0_1 = Uniform::new(0.0, 1.0).unwrap();
719    for run_counter in 1..=p.n_runs {
720        let mut loss_computer = loss_computer_factory();
721        let singletons_initialization = uniform_0_1.sample(rng) < p.prob_singletons_initialization;
722        let (mut state, mut cms, initialization_method) =
723            if uniform_0_1.sample(rng) < p.prob_sequential_allocation {
724                let mut state = WorkingClustering::empty(n_items, max_size);
725                let mut cms = Array3::<CountType>::zeros((
726                    max_size as usize + 1,
727                    draws.max_clusters() as usize,
728                    draws.n_clusterings(),
729                ));
730                // Sequential allocation
731                permutation.shuffle(rng);
732                allocation_scan(
733                    false,
734                    singletons_initialization,
735                    &mut state,
736                    &mut cms,
737                    &permutation,
738                    &mut loss_computer,
739                    draws,
740                );
741                let initialization_method = if singletons_initialization {
742                    InitializationMethod::SequentialFromSingletons
743                } else {
744                    InitializationMethod::SequentialFromEmpty
745                };
746                (state, cms, initialization_method)
747            } else {
748                let state = if p.max_size_as_rf {
749                    WorkingClustering::random_as_rf(n_items, max_size, draws.max_clusters(), rng)
750                } else {
751                    WorkingClustering::random(n_items, max_size, rng)
752                };
753                let cms = draws.make_confusion_matrices(&state);
754                loss_computer.initialize(&state, &cms);
755                (
756                    state,
757                    cms,
758                    InitializationMethod::SampleOne2MaxWithReplacement,
759                )
760            };
761        let mut scan_counter = 0;
762        sweetening_scans(
763            &mut state,
764            &mut cms,
765            &mut permutation,
766            &mut loss_computer,
767            &mut scan_counter,
768            draws,
769            p,
770            rng,
771        );
772        let mut expected_loss = loss_computer.compute_loss(&state, &cms);
773        let (mut n_zealous_accepts, mut n_zealous_attempts) = (0, 0);
774        if n_zealous_attempts < p.max_zealous_updates {
775            let labels = {
776                let mut x = state.occupied_clusters().clone();
777                x.shuffle(rng);
778                x
779            };
780            for label in labels {
781                let s = state.size_of(label) as usize;
782                if s <= 1 {
783                    // Already covered by sweetening scans.
784                    continue;
785                }
786                n_zealous_attempts += 1;
787                if n_zealous_attempts > p.max_zealous_updates {
788                    break;
789                }
790                let mut active_items = Vec::with_capacity(s);
791                for item_index in 0..(state.n_items() as usize) {
792                    if state.get(item_index) == label {
793                        state.remove(item_index, &mut loss_computer, &mut cms, draws);
794                        active_items.push(item_index)
795                    }
796                    if active_items.len() == s {
797                        break;
798                    }
799                }
800                active_items.shuffle(rng);
801                let state_changed = allocation_scan(
802                    false,
803                    singletons_initialization,
804                    &mut state,
805                    &mut cms,
806                    &active_items,
807                    &mut loss_computer,
808                    draws,
809                );
810                if state_changed {
811                    let labels_for_undo = state.clone_labels();
812                    let state_changed = sweetening_scans(
813                        &mut state,
814                        &mut cms,
815                        &mut permutation,
816                        &mut loss_computer,
817                        &mut scan_counter,
818                        draws,
819                        p,
820                        rng,
821                    );
822                    let expected_loss_of_candidate = loss_computer.compute_loss(&state, &cms);
823                    if expected_loss_of_candidate < expected_loss {
824                        // Keep changes
825                        expected_loss = expected_loss_of_candidate;
826                        n_zealous_accepts += 1;
827                    } else {
828                        // Undo changes
829                        if state_changed {
830                            for (item_index, new_label) in labels_for_undo.iter().enumerate() {
831                                state.reassign(
832                                    item_index,
833                                    *new_label,
834                                    &mut loss_computer,
835                                    &mut cms,
836                                    draws,
837                                )
838                            }
839                        }
840                        for item_index in active_items {
841                            state.reassign(item_index, label, &mut loss_computer, &mut cms, draws)
842                        }
843                    }
844                }
845            }
846        }
847        // Tidy up
848        if expected_loss < best.expected_loss {
849            let clustering = state.standardize().iter().map(|x| *x as usize).collect();
850            best = SALSOResults {
851                clustering,
852                expected_loss,
853                n_scans: scan_counter,
854                n_zealous_accepts,
855                n_zealous_attempts,
856                initialization_method,
857                max_size: state.max_clusters(),
858                ..best
859            }
860        }
861        if seconds_target.is_finite() {
862            let seconds = start_time.elapsed().as_secs_f64();
863            let rc = run_counter as f64;
864            let seconds_per_run = seconds / rc;
865            let projected_seconds_if_another = (rc + 1.0) * seconds_per_run;
866            if projected_seconds_if_another >= seconds_target {
867                let prob_stop = (projected_seconds_if_another - seconds_target) / seconds_per_run;
868                if uniform_0_1.sample(rng) < prob_stop {
869                    return SALSOResults {
870                        n_runs: run_counter,
871                        seconds,
872                        ..best
873                    };
874                }
875            }
876        }
877    }
878    SALSOResults {
879        n_runs: p.n_runs,
880        seconds: start_time.elapsed().as_secs_f64(),
881        ..best
882    }
883}
884
885// ******************************************
886// Implementation of SALSO for general losses
887// ******************************************
888
889pub trait GeneralLossComputer {
890    fn expected_loss_kernel(&self) -> f64;
891    fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64;
892    fn new_subset(&mut self, partition: &mut Partition);
893    fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType);
894    fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType;
895}
896
897// Expectation of the Binder loss
898
899#[derive(Debug)]
900struct BinderSubsetCalculations {
901    committed_loss: f64,
902    speculative_loss: f64,
903}
904
905pub struct BinderGLossComputer<'a> {
906    subsets: Vec<BinderSubsetCalculations>,
907    psm: &'a SquareMatrixBorrower<'a>,
908}
909
910impl<'a> BinderGLossComputer<'a> {
911    pub fn new(psm: &'a SquareMatrixBorrower<'a>) -> Self {
912        Self {
913            subsets: Vec::new(),
914            psm,
915        }
916    }
917    fn expected_loss_from_kernel(psm: &'a SquareMatrixBorrower<'a>, kernel: f64) -> f64 {
918        let nif = psm.n_items() as f64;
919        (2.0 * kernel + psm.sum_of_triangle()) * 2.0 / (nif * nif)
920    }
921}
922
923impl GeneralLossComputer for BinderGLossComputer<'_> {
924    fn expected_loss_kernel(&self) -> f64 {
925        self.subsets
926            .iter()
927            .fold(0.0, |s, subset| s + subset.committed_loss)
928    }
929
930    fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64 {
931        self.subsets[subset_index as usize].speculative_loss = partition.subsets()
932            [subset_index as usize]
933            .items()
934            .iter()
935            .fold(0.0, |s, j| {
936                s + 0.5 - unsafe { *self.psm.get_unchecked((i, *j)) }
937            });
938        self.subsets[subset_index as usize].speculative_loss
939    }
940
941    fn new_subset(&mut self, partition: &mut Partition) {
942        partition.new_subset();
943        self.subsets.push(BinderSubsetCalculations {
944            committed_loss: 0.0,
945            speculative_loss: 0.0,
946        });
947    }
948
949    fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType) {
950        self.subsets[subset_index as usize].committed_loss +=
951            self.subsets[subset_index as usize].speculative_loss;
952        partition.add_with_index(i, subset_index as usize);
953    }
954
955    fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType {
956        let subset_index = partition.label_of(i).unwrap();
957        self.subsets[subset_index].committed_loss -= partition.subsets()[subset_index]
958            .items()
959            .iter()
960            .fold(0.0, |s, j| {
961                let jj = *j;
962                s + if jj != i {
963                    0.5 - unsafe { *self.psm.get_unchecked((i, jj)) }
964                } else {
965                    0.0
966                }
967            });
968        partition.remove_clean_and_relabel(i, |killed_subset_index, moved_subset_index| {
969            self.subsets.swap_remove(killed_subset_index);
970            assert_eq!(moved_subset_index, self.subsets.len());
971        });
972        subset_index as LabelType
973    }
974}
975
976// First-order approximation of expectation of one minus the adjusted Rand index loss
977
978#[derive(Debug)]
979struct OMARIApproxSubsetCalculations {
980    committed_ip: f64,
981    committed_i: f64,
982    speculative_ip: f64,
983    speculative_i: f64,
984}
985
986pub struct OMARIApproxGLossComputer<'a> {
987    committed_n_items: usize,
988    committed_sum_psm: f64,
989    speculative_sum_psm: f64,
990    subsets: Vec<OMARIApproxSubsetCalculations>,
991    psm: &'a SquareMatrixBorrower<'a>,
992}
993
994impl<'a> OMARIApproxGLossComputer<'a> {
995    pub fn new(psm: &'a SquareMatrixBorrower<'a>) -> Self {
996        Self {
997            committed_n_items: 0,
998            committed_sum_psm: 0.0,
999            speculative_sum_psm: f64::NEG_INFINITY,
1000            subsets: Vec::new(),
1001            psm,
1002        }
1003    }
1004}
1005
1006impl OMARIApproxGLossComputer<'_> {
1007    fn engine(
1008        &self,
1009        speculative_n_items: usize,
1010        speculative_ip: f64,
1011        speculative_i: f64,
1012        speculative_sum_psm: f64,
1013    ) -> f64 {
1014        let n_items = speculative_n_items + self.committed_n_items;
1015        if n_items <= 1 {
1016            return f64::INFINITY;
1017        }
1018        let n_choose_2 = (n_items * (n_items - 1) / 2) as f64;
1019        let all_ip = speculative_ip + self.subsets.iter().fold(0.0, |s, c| s + c.committed_ip);
1020        let all_i = speculative_i + self.subsets.iter().fold(0.0, |s, c| s + c.committed_i);
1021        let all_p = speculative_sum_psm + self.committed_sum_psm;
1022        let subtractor = all_i * all_p / n_choose_2;
1023        let numerator = all_ip - subtractor;
1024        let denominator = 0.5 * (all_i + all_p) - subtractor;
1025        1.0 - numerator / denominator
1026    }
1027}
1028
1029impl GeneralLossComputer for OMARIApproxGLossComputer<'_> {
1030    fn expected_loss_kernel(&self) -> f64 {
1031        self.engine(0, 0.0, 0.0, 0.0)
1032    }
1033
1034    fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64 {
1035        let s = &partition.subsets()[subset_index as usize];
1036        self.subsets[subset_index as usize].speculative_ip = s
1037            .items()
1038            .iter()
1039            .fold(0.0, |s, j| s + unsafe { *self.psm.get_unchecked((i, *j)) });
1040        self.subsets[subset_index as usize].speculative_i = s.n_items() as f64;
1041        if self.speculative_sum_psm == f64::NEG_INFINITY {
1042            self.speculative_sum_psm = partition.subsets().iter().fold(0.0, |s, subset| {
1043                // We use the NEG_INFINITY flag to see if we need to do the computation.
1044                s + subset.items().iter().fold(0.0, |ss, j| {
1045                    ss + unsafe { *self.psm.get_unchecked((i, *j)) }
1046                })
1047            })
1048        }
1049        self.engine(
1050            1,
1051            self.subsets[subset_index as usize].speculative_ip,
1052            self.subsets[subset_index as usize].speculative_i,
1053            self.speculative_sum_psm,
1054        )
1055    }
1056
1057    fn new_subset(&mut self, partition: &mut Partition) {
1058        partition.new_subset();
1059        self.subsets.push(OMARIApproxSubsetCalculations {
1060            committed_ip: 0.0,
1061            committed_i: 0.0,
1062            speculative_ip: 0.0,
1063            speculative_i: 0.0,
1064        });
1065    }
1066
1067    fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType) {
1068        let sc = &mut self.subsets[subset_index as usize];
1069        sc.committed_ip += sc.speculative_ip;
1070        sc.committed_i += sc.speculative_i;
1071        self.committed_n_items += 1;
1072        self.committed_sum_psm += self.speculative_sum_psm;
1073        self.speculative_sum_psm = f64::NEG_INFINITY;
1074        partition.add_with_index(i, subset_index as usize);
1075    }
1076
1077    fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType {
1078        let subset_index = partition.label_of(i).unwrap();
1079        self.subsets[subset_index].committed_ip -= partition.subsets()[subset_index]
1080            .items()
1081            .iter()
1082            .fold(0.0, |s, j| {
1083                let jj = *j;
1084                s + if jj != i {
1085                    unsafe { *self.psm.get_unchecked((i, jj)) }
1086                } else {
1087                    0.0
1088                }
1089            });
1090        self.subsets[subset_index].committed_i -=
1091            (partition.subsets()[subset_index].n_items() - 1) as f64;
1092        self.committed_n_items -= 1;
1093        self.committed_sum_psm -= partition.subsets().iter().fold(0.0, |s, subset| {
1094            // We use the NEG_INFINITY flag to see if we need to do the computation.
1095            s + subset.items().iter().fold(0.0, |ss, j| {
1096                let jj = *j;
1097                ss + if jj != i {
1098                    unsafe { *self.psm.get_unchecked((i, jj)) }
1099                } else {
1100                    0.0
1101                }
1102            })
1103        });
1104        partition.remove_clean_and_relabel(i, |killed_subset_index, moved_subset_index| {
1105            self.subsets.swap_remove(killed_subset_index);
1106            assert_eq!(moved_subset_index, self.subsets.len());
1107        });
1108        subset_index as LabelType
1109    }
1110}
1111
1112// Lower bound of the expectation of variation of information loss
1113
1114#[derive(Debug)]
1115struct VILBCacheUnit {
1116    item: usize,
1117    committed_sum: f64,
1118    committed_contribution: f64,
1119    speculative_sum: f64,
1120    speculative_contribution: f64,
1121}
1122
1123#[derive(Debug)]
1124struct VILBSubsetCalculations {
1125    cached_units: Vec<VILBCacheUnit>,
1126    committed_loss: f64,
1127    speculative_loss: f64,
1128}
1129
1130pub struct VILBGLossComputer<'a> {
1131    subsets: Vec<VILBSubsetCalculations>,
1132    psm: &'a SquareMatrixBorrower<'a>,
1133}
1134
1135impl<'a> VILBGLossComputer<'a> {
1136    pub fn new(psm: &'a SquareMatrixBorrower<'a>) -> Self {
1137        Self {
1138            subsets: Vec::new(),
1139            psm,
1140        }
1141    }
1142    pub fn expected_loss_from_kernel(psm: &'a SquareMatrixBorrower<'a>, kernel: f64) -> f64 {
1143        let nif = psm.n_items() as f64;
1144        (kernel + vilb_expected_loss_constant(psm)) / nif
1145    }
1146}
1147
1148impl GeneralLossComputer for VILBGLossComputer<'_> {
1149    fn expected_loss_kernel(&self) -> f64 {
1150        self.subsets
1151            .iter()
1152            .fold(0.0, |s, subset| s + subset.committed_loss)
1153    }
1154
1155    fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64 {
1156        let subset_of_partition = &partition.subsets()[subset_index as usize];
1157        if subset_of_partition.n_items() == 0 {
1158            self.subsets[subset_index as usize]
1159                .cached_units
1160                .push(VILBCacheUnit {
1161                    item: i,
1162                    committed_sum: 0.0,
1163                    committed_contribution: 0.0,
1164                    speculative_sum: 1.0,
1165                    speculative_contribution: 0.0,
1166                });
1167            return 0.0;
1168        }
1169        for cu in self.subsets[subset_index as usize].cached_units.iter_mut() {
1170            cu.speculative_sum =
1171                cu.committed_sum + unsafe { *self.psm.get_unchecked((cu.item, i)) };
1172            cu.speculative_contribution = cu.speculative_sum.log2();
1173        }
1174        let sum = subset_of_partition
1175            .items()
1176            .iter()
1177            .fold(0.0, |s, j| s + unsafe { *self.psm.get_unchecked((i, *j)) })
1178            + 1.0; // Because self.psm[(i, i)] == 1;
1179        self.subsets[subset_index as usize]
1180            .cached_units
1181            .push(VILBCacheUnit {
1182                item: i,
1183                committed_sum: 0.0,
1184                committed_contribution: 0.0,
1185                speculative_sum: sum,
1186                speculative_contribution: sum.log2(),
1187            });
1188        let nif = subset_of_partition.n_items() as f64;
1189        let s1 = (nif + 1.0) * (nif + 1.0).log2();
1190        let s2 = self.subsets[subset_index as usize]
1191            .cached_units
1192            .iter()
1193            .fold(0.0, |s, cu| s + cu.speculative_contribution);
1194        self.subsets[subset_index as usize].speculative_loss = s1 - 2.0 * s2;
1195        self.subsets[subset_index as usize].speculative_loss
1196            - self.subsets[subset_index as usize].committed_loss
1197    }
1198
1199    fn new_subset(&mut self, partition: &mut Partition) {
1200        partition.new_subset();
1201        self.subsets.push(VILBSubsetCalculations {
1202            cached_units: Vec::new(),
1203            committed_loss: 0.0,
1204            speculative_loss: 0.0,
1205        })
1206    }
1207
1208    fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType) {
1209        for (index, subset) in self.subsets.iter_mut().enumerate() {
1210            if index == (subset_index as usize) {
1211                for cu in subset.cached_units.iter_mut() {
1212                    cu.committed_sum = cu.speculative_sum;
1213                    cu.committed_contribution = cu.speculative_contribution;
1214                }
1215            } else {
1216                subset.cached_units.pop();
1217            }
1218        }
1219        self.subsets[subset_index as usize].committed_loss =
1220            self.subsets[subset_index as usize].speculative_loss;
1221        partition.add_with_index(i, subset_index as usize);
1222    }
1223
1224    fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType {
1225        let subset_index = partition.label_of(i).unwrap();
1226        for cu in self.subsets[subset_index].cached_units.iter_mut() {
1227            cu.committed_sum -= unsafe { *self.psm.get_unchecked((cu.item, i)) };
1228            cu.committed_contribution = cu.committed_sum.log2();
1229        }
1230        let pos = self.subsets[subset_index]
1231            .cached_units
1232            .iter()
1233            .enumerate()
1234            .find(|cu| cu.1.item == i)
1235            .unwrap()
1236            .0;
1237        self.subsets[subset_index].cached_units.swap_remove(pos);
1238        self.subsets[subset_index].committed_loss =
1239            match partition.subsets()[subset_index].n_items() {
1240                0 => 0.0,
1241                ni => {
1242                    let nif = ni as f64;
1243                    nif * nif.log2()
1244                        - 2.0
1245                            * self.subsets[subset_index]
1246                                .cached_units
1247                                .iter()
1248                                .fold(0.0, |s, cu| s + cu.committed_contribution)
1249                }
1250            };
1251        partition.remove_clean_and_relabel(i, |killed_subset_index, moved_subset_index| {
1252            self.subsets.swap_remove(killed_subset_index);
1253            assert_eq!(moved_subset_index, self.subsets.len());
1254        });
1255        subset_index as LabelType
1256    }
1257}
1258
1259// Common
1260
1261fn label_of_empty_cluster<U: GeneralLossComputer>(
1262    partition: &mut Partition,
1263    computer: &mut U,
1264    max_label: LabelType,
1265) -> Option<LabelType> {
1266    match partition.subsets().last() {
1267        None => {
1268            computer.new_subset(partition);
1269            Some(0)
1270        }
1271        Some(last) => {
1272            if last.is_empty() {
1273                Some(partition.n_subsets() as LabelType - 1)
1274            } else if partition.n_subsets() <= max_label as usize {
1275                computer.new_subset(partition);
1276                Some(partition.n_subsets() as LabelType - 1)
1277            } else {
1278                None
1279            }
1280        }
1281    }
1282}
1283
1284fn micro_optimized_allocation<U: GeneralLossComputer>(
1285    partition: &mut Partition,
1286    computer: &mut U,
1287    i: usize,
1288    label_to_take: Option<LabelType>,
1289) -> LabelType {
1290    let max_label = partition.n_subsets() as LabelType - 1;
1291    let iter = (0..=max_label).map(|subset_index| {
1292        let value = computer.speculative_add(partition, i, subset_index as LabelType);
1293        match label_to_take {
1294            Some(label_to_take) => {
1295                if subset_index == label_to_take {
1296                    (subset_index, f64::NEG_INFINITY)
1297                } else {
1298                    (subset_index, f64::INFINITY)
1299                }
1300            }
1301            None => (subset_index, value),
1302        }
1303    });
1304    let subset_index = find_label_of_minimum(iter);
1305    computer.add_with_index(partition, i, subset_index);
1306    subset_index as LabelType
1307}
1308
1309pub fn minimize_once_by_salso<'a, T: Rng, U: GeneralLossComputer>(
1310    computer_factory: Box<dyn Fn() -> U + 'a>,
1311    p: &SALSOParameters,
1312    seconds_target: f64,
1313    rng: &mut T,
1314) -> SALSOResults {
1315    let start_time = Instant::now();
1316    let max_label = if p.max_size == 0 {
1317        MAX_LABEL
1318    } else {
1319        p.max_size.max(1) - 1
1320    };
1321    let mut best = SALSOResults::dummy(max_label + 1);
1322    let mut permutation: Vec<usize> = (0..p.n_items).collect();
1323    let uniform_0_1 = Uniform::new(0.0, 1.0).unwrap();
1324    let mut run_counter = 0;
1325    while run_counter < p.n_runs {
1326        let mut computer = computer_factory();
1327        let (mut partition, initialization_method) = if uniform_0_1.sample(rng)
1328            < p.prob_sequential_allocation
1329        {
1330            let mut partition = Partition::new(p.n_items);
1331            permutation.shuffle(rng);
1332            let singletons_initialization =
1333                uniform_0_1.sample(rng) < p.prob_singletons_initialization;
1334            // Sequential allocation
1335            for i in 0..p.n_items {
1336                let ii = unsafe { *permutation.get_unchecked(i) };
1337                let empty_label = label_of_empty_cluster(&mut partition, &mut computer, max_label);
1338                if singletons_initialization {
1339                    micro_optimized_allocation(&mut partition, &mut computer, ii, empty_label);
1340                } else {
1341                    micro_optimized_allocation(&mut partition, &mut computer, ii, None);
1342                }
1343            }
1344            let initialization_method = if singletons_initialization {
1345                InitializationMethod::SequentialFromSingletons
1346            } else {
1347                InitializationMethod::SequentialFromEmpty
1348            };
1349            (partition, initialization_method)
1350        } else {
1351            // Something about this code isn't working when max_label is small.
1352            // Since pairwise similarity based computations aren't recommend, it may not be worth
1353            // pursuing this further.  So, we'll just panic here and disable it in the R package.
1354            panic!("Disabled because it's problematic.");
1355            // let mut partition = Partition::new(p.n_items);
1356            // let destiny = {
1357            //     let mut v = Vec::with_capacity(p.n_items);
1358            //     v.resize_with(p.n_items, || rng.random_range(0, max_label + 1));
1359            //     Partition::from(&v) // Already canonicalized
1360            // };
1361            // for i in 0..p.n_items {
1362            //     let label_to_take = Some(destiny.label_of(i).unwrap() as LabelType);
1363            //     label_of_empty_cluster(&mut partition, &mut computer, max_label);
1364            //     micro_optimized_allocation(&mut partition, &mut computer, i, label_to_take);
1365            // }
1366            // (
1367            //     partition,
1368            //     InitializationMethod::SampleOne2MaxWithReplacement,
1369            // )
1370        };
1371        // Sweetening scans
1372        let mut n_scans = 0;
1373        let mut change = true;
1374        while change && n_scans < p.max_scans {
1375            change = false;
1376            n_scans += 1;
1377            permutation.shuffle(rng);
1378            for i in 0..p.n_items {
1379                label_of_empty_cluster(&mut partition, &mut computer, max_label);
1380                let ii = unsafe { *permutation.get_unchecked(i) };
1381                let previous_subset_index = computer.remove(&mut partition, ii);
1382                let subset_index =
1383                    micro_optimized_allocation(&mut partition, &mut computer, ii, None);
1384                if subset_index != previous_subset_index {
1385                    change = true;
1386                };
1387            }
1388        }
1389        let expected_loss = computer.expected_loss_kernel();
1390        if expected_loss < best.expected_loss {
1391            partition.canonicalize();
1392            best = SALSOResults {
1393                clustering: partition.labels_via_copying(),
1394                expected_loss,
1395                n_scans,
1396                initialization_method,
1397                ..best
1398            };
1399        }
1400        run_counter += 1;
1401        if seconds_target.is_finite() {
1402            let seconds = start_time.elapsed().as_secs_f64();
1403            let rc = run_counter as f64;
1404            let seconds_per_run = seconds / rc;
1405            let projected_seconds_if_another = (rc + 1.0) * seconds_per_run;
1406            if projected_seconds_if_another >= seconds_target {
1407                let prob_stop = (projected_seconds_if_another - seconds_target) / seconds_per_run;
1408                if uniform_0_1.sample(rng) < prob_stop {
1409                    return SALSOResults {
1410                        n_runs: run_counter,
1411                        seconds,
1412                        ..best
1413                    };
1414                }
1415            }
1416        }
1417    }
1418    SALSOResults {
1419        n_runs: p.n_runs,
1420        seconds: start_time.elapsed().as_secs_f64(),
1421        ..best
1422    }
1423}
1424
1425// Common implementation for SALSO.
1426
1427#[derive(Debug, Clone)]
1428pub struct SALSOParameters {
1429    pub n_items: usize,
1430    pub max_size: LabelType,
1431    pub max_size_as_rf: bool,
1432    pub max_scans: u32,
1433    pub max_zealous_updates: u32,
1434    pub n_runs: u32,
1435    pub prob_sequential_allocation: f64,
1436    pub prob_singletons_initialization: f64,
1437}
1438
1439pub struct SALSOResults {
1440    pub clustering: Vec<usize>,
1441    pub expected_loss: f64,
1442    pub n_scans: u32,
1443    pub n_zealous_accepts: u32,
1444    pub n_zealous_attempts: u32,
1445    pub initialization_method: InitializationMethod,
1446    pub n_runs: u32,
1447    pub max_size: LabelType,
1448    pub seconds: f64,
1449}
1450
1451impl SALSOResults {
1452    #[allow(clippy::too_many_arguments)]
1453    pub fn new(
1454        clustering: Vec<usize>,
1455        expected_loss: f64,
1456        n_scans: u32,
1457        n_zealous_accepts: u32,
1458        n_zealous_attempts: u32,
1459        initialization_method: InitializationMethod,
1460        n_runs: u32,
1461        max_size: LabelType,
1462        seconds: f64,
1463    ) -> Self {
1464        Self {
1465            clustering,
1466            expected_loss,
1467            n_scans,
1468            n_zealous_accepts,
1469            n_zealous_attempts,
1470            initialization_method,
1471            n_runs,
1472            max_size,
1473            seconds,
1474        }
1475    }
1476
1477    pub fn dummy(max_size: LabelType) -> Self {
1478        SALSOResults::new(
1479            vec![0usize; 0],
1480            f64::INFINITY,
1481            0,
1482            0,
1483            0,
1484            InitializationMethod::SequentialFromEmpty,
1485            0,
1486            max_size,
1487            0.0,
1488        )
1489    }
1490}
1491
1492pub fn minimize_by_salso<T: Rng>(
1493    pdi: PartitionDistributionInformation,
1494    loss_function: LossFunction,
1495    p: &SALSOParameters,
1496    seconds: f64,
1497    n_cores: u32,
1498    mut rng: &mut T,
1499) -> SALSOResults {
1500    let start_time = Instant::now();
1501    let cache = Log2Cache::new(match loss_function {
1502        LossFunction::VI(_) | LossFunction::NVI | LossFunction::ID | LossFunction::NID => p.n_items,
1503        _ => 0,
1504    });
1505    let result = if n_cores == 1 {
1506        match loss_function {
1507            LossFunction::BinderDraws(a) => minimize_once_by_salso_v2(
1508                Box::new(|| BinderCMLossComputer::new(a)),
1509                pdi.draws(),
1510                p,
1511                seconds,
1512                rng,
1513            ),
1514            LossFunction::BinderPSM => minimize_once_by_salso(
1515                Box::new(|| BinderGLossComputer::new(pdi.psm())),
1516                p,
1517                seconds,
1518                rng,
1519            ),
1520            LossFunction::OneMinusARI => minimize_once_by_salso_v2(
1521                Box::new(|| OMARICMLossComputer::new(pdi.draws().n_clusterings())),
1522                pdi.draws(),
1523                p,
1524                seconds,
1525                rng,
1526            ),
1527            LossFunction::OneMinusARIapprox => minimize_once_by_salso(
1528                Box::new(|| OMARIApproxGLossComputer::new(pdi.psm())),
1529                p,
1530                seconds,
1531                rng,
1532            ),
1533            LossFunction::VI(a) => minimize_once_by_salso_v2(
1534                Box::new(|| VICMLossComputer::new(a, &cache)),
1535                pdi.draws(),
1536                p,
1537                seconds,
1538                rng,
1539            ),
1540            LossFunction::VIlb => minimize_once_by_salso(
1541                Box::new(|| VILBGLossComputer::new(pdi.psm())),
1542                p,
1543                seconds,
1544                rng,
1545            ),
1546            LossFunction::NVI => minimize_once_by_salso_v2(
1547                Box::new(|| {
1548                    GeneralInformationBasedCMLossComputer::new(
1549                        pdi.draws().n_clusterings(),
1550                        &cache,
1551                        NVIInformationBasedLoss {},
1552                    )
1553                }),
1554                pdi.draws(),
1555                p,
1556                seconds,
1557                rng,
1558            ),
1559            LossFunction::ID => minimize_once_by_salso_v2(
1560                Box::new(|| {
1561                    GeneralInformationBasedCMLossComputer::new(
1562                        pdi.draws().n_clusterings(),
1563                        &cache,
1564                        IDInformationBasedLoss {},
1565                    )
1566                }),
1567                pdi.draws(),
1568                p,
1569                seconds,
1570                rng,
1571            ),
1572            LossFunction::NID => minimize_once_by_salso_v2(
1573                Box::new(|| {
1574                    GeneralInformationBasedCMLossComputer::new(
1575                        pdi.draws().n_clusterings(),
1576                        &cache,
1577                        NIDInformationBasedLoss {},
1578                    )
1579                }),
1580                pdi.draws(),
1581                p,
1582                seconds,
1583                rng,
1584            ),
1585        }
1586    } else {
1587        let (tx, rx) = mpsc::channel();
1588        let n_cores = if n_cores == 0 {
1589            std::thread::available_parallelism()
1590                .map(|x| x.get() as u32)
1591                .unwrap_or(1)
1592        } else {
1593            n_cores
1594        };
1595        let p = p.clone();
1596        let p = SALSOParameters {
1597            n_runs: p.n_runs.div_ceil(n_cores),
1598            ..p
1599        };
1600        let cache_ref = &cache;
1601        crossbeam::scope(|s| {
1602            for _ in 0..n_cores {
1603                let tx = mpsc::Sender::clone(&tx);
1604                let mut child_rng = Pcg64Mcg::from_rng(&mut rng);
1605                let p = p.clone();
1606                s.spawn(move |_| {
1607                    let result = match loss_function {
1608                        LossFunction::BinderDraws(a) => minimize_once_by_salso_v2(
1609                            Box::new(|| BinderCMLossComputer::new(a)),
1610                            pdi.draws(),
1611                            &p,
1612                            seconds,
1613                            &mut child_rng,
1614                        ),
1615                        LossFunction::BinderPSM => minimize_once_by_salso(
1616                            Box::new(|| BinderGLossComputer::new(pdi.psm())),
1617                            &p,
1618                            seconds,
1619                            &mut child_rng,
1620                        ),
1621                        LossFunction::OneMinusARI => minimize_once_by_salso_v2(
1622                            Box::new(|| OMARICMLossComputer::new(pdi.draws().n_clusterings())),
1623                            pdi.draws(),
1624                            &p,
1625                            seconds,
1626                            &mut child_rng,
1627                        ),
1628                        LossFunction::OneMinusARIapprox => minimize_once_by_salso(
1629                            Box::new(|| OMARIApproxGLossComputer::new(pdi.psm())),
1630                            &p,
1631                            seconds,
1632                            &mut child_rng,
1633                        ),
1634                        LossFunction::VI(a) => minimize_once_by_salso_v2(
1635                            Box::new(|| VICMLossComputer::new(a, cache_ref)),
1636                            pdi.draws(),
1637                            &p,
1638                            seconds,
1639                            &mut child_rng,
1640                        ),
1641                        LossFunction::VIlb => minimize_once_by_salso(
1642                            Box::new(|| VILBGLossComputer::new(pdi.psm())),
1643                            &p,
1644                            seconds,
1645                            &mut child_rng,
1646                        ),
1647                        LossFunction::NVI => minimize_once_by_salso_v2(
1648                            Box::new(|| {
1649                                GeneralInformationBasedCMLossComputer::new(
1650                                    pdi.draws().n_clusterings(),
1651                                    cache_ref,
1652                                    NVIInformationBasedLoss {},
1653                                )
1654                            }),
1655                            pdi.draws(),
1656                            &p,
1657                            seconds,
1658                            &mut child_rng,
1659                        ),
1660                        LossFunction::ID => minimize_once_by_salso_v2(
1661                            Box::new(|| {
1662                                GeneralInformationBasedCMLossComputer::new(
1663                                    pdi.draws().n_clusterings(),
1664                                    cache_ref,
1665                                    IDInformationBasedLoss {},
1666                                )
1667                            }),
1668                            pdi.draws(),
1669                            &p,
1670                            seconds,
1671                            &mut child_rng,
1672                        ),
1673                        LossFunction::NID => minimize_once_by_salso_v2(
1674                            Box::new(|| {
1675                                GeneralInformationBasedCMLossComputer::new(
1676                                    pdi.draws().n_clusterings(),
1677                                    cache_ref,
1678                                    NIDInformationBasedLoss {},
1679                                )
1680                            }),
1681                            pdi.draws(),
1682                            &p,
1683                            seconds,
1684                            &mut child_rng,
1685                        ),
1686                    };
1687                    tx.send(result).unwrap();
1688                });
1689            }
1690        })
1691        .unwrap();
1692        std::mem::drop(tx); // Because of the cloning in the loop.
1693        let mut best = SALSOResults::dummy(p.max_size);
1694        let mut run_counter = 0;
1695        for candidate in rx {
1696            run_counter += candidate.n_runs;
1697            if candidate.expected_loss < best.expected_loss {
1698                best = candidate;
1699            }
1700        }
1701        best.n_runs = run_counter;
1702        best
1703    };
1704    SALSOResults {
1705        expected_loss: match loss_function {
1706            LossFunction::BinderPSM => {
1707                BinderGLossComputer::expected_loss_from_kernel(pdi.psm(), result.expected_loss)
1708            }
1709            LossFunction::VIlb => {
1710                VILBGLossComputer::expected_loss_from_kernel(pdi.psm(), result.expected_loss)
1711            }
1712            _ => result.expected_loss,
1713        },
1714        seconds: start_time.elapsed().as_secs_f64(),
1715        ..result
1716    }
1717}
1718
1719pub fn minimize_by_enumeration(
1720    f: fn(&[LabelType], &SquareMatrixBorrower) -> f64,
1721    psm: &SquareMatrixBorrower,
1722) -> Vec<usize> {
1723    let (tx, rx) = mpsc::channel();
1724    crossbeam::scope(|s| {
1725        let n_cores = std::thread::available_parallelism()
1726            .map(|x| x.get() as u32)
1727            .unwrap_or(1);
1728        for iter in Partition::iter_sharded(n_cores, psm.n_items()) {
1729            let tx = mpsc::Sender::clone(&tx);
1730            s.spawn(move |_| {
1731                let mut working_minimum = f64::INFINITY;
1732                let mut working_minimizer = vec![0usize; psm.n_items()];
1733                for partition in iter {
1734                    let part: Vec<LabelType> = partition.iter().map(|x| *x as LabelType).collect();
1735                    let value = f(&part[..], psm);
1736                    if value < working_minimum {
1737                        working_minimum = value;
1738                        working_minimizer = partition;
1739                    }
1740                }
1741                tx.send(working_minimizer).unwrap();
1742            });
1743        }
1744    })
1745    .unwrap();
1746    std::mem::drop(tx); // Because of the cloning in the loop.
1747    let mut working_minimum = f64::INFINITY;
1748    let mut working_minimizer = vec![0usize; psm.n_items()];
1749    for partition in rx {
1750        let part: Vec<LabelType> = partition.iter().map(|x| *x as LabelType).collect();
1751        let value = f(&part[..], psm);
1752        if value < working_minimum {
1753            working_minimum = value;
1754            working_minimizer = partition;
1755        }
1756    }
1757    working_minimizer
1758}
1759
1760#[cfg(test)]
1761mod tests_optimize {
1762    use super::*;
1763    use rand::{RngExt, rng};
1764
1765    #[test]
1766    fn test_max_scan() {
1767        let n_items = 5;
1768        let mut psm = SquareMatrix::identity(n_items);
1769        let psm_view = &psm.view();
1770        let p = SALSOParameters {
1771            n_items,
1772            max_size: 2,
1773            max_size_as_rf: true,
1774            max_scans: 10,
1775            max_zealous_updates: 10,
1776            n_runs: 100,
1777            prob_sequential_allocation: 1.0,
1778            prob_singletons_initialization: 0.5,
1779        };
1780        minimize_by_salso(
1781            PartitionDistributionInformation::PairwiseSimilarityMatrix(psm_view),
1782            LossFunction::BinderPSM,
1783            &p,
1784            5.0,
1785            1,
1786            &mut rng(),
1787        );
1788    }
1789
1790    fn random_draws(n_items: usize, n_draws: usize) -> Clusterings {
1791        let mut rng = Pcg64Mcg::seed_from_u64(7);
1792        let labels: Vec<i32> = (0..n_items * n_draws)
1793            .map(|_| rng.random_range(0..4))
1794            .collect();
1795        Clusterings::from_i32_column_major_order(&labels, n_items)
1796    }
1797
1798    // Builds the loss computer's state from scratch by assigning items in order.
1799    fn build<T: CMLossComputer>(
1800        mut loss_computer: T,
1801        draws: &Clusterings,
1802        assignments: &[(usize, LabelType)],
1803    ) -> (T, WorkingClustering, Array3<CountType>) {
1804        let max_size = 6;
1805        let mut state = WorkingClustering::empty(draws.n_items(), max_size);
1806        let mut cms = Array3::<CountType>::zeros((
1807            max_size as usize + 1,
1808            draws.max_clusters() as usize,
1809            draws.n_clusterings(),
1810        ));
1811        for &(item_index, label) in assignments {
1812            state.assign(item_index, label, &mut loss_computer, &mut cms, draws);
1813        }
1814        (loss_computer, state, cms)
1815    }
1816
1817    // change_in_loss returns the loss after the move up to an affine map (`to_loss`), so it must
1818    // agree with compute_loss on a state built from scratch with the move applied.
1819    fn check_information_based_change_in_loss<T: InformationBasedLoss>(
1820        ibloss: impl Fn() -> T,
1821        to_loss: impl Fn(f64, f64, f64) -> f64,
1822    ) {
1823        let n_items = 12;
1824        let n_draws = 9;
1825        let draws = random_draws(n_items, n_draws);
1826        // Sized exactly, as in minimize_by_salso.
1827        let cache = Log2Cache::new(n_items);
1828        let new = || GeneralInformationBasedCMLossComputer::new(n_draws, &cache, ibloss());
1829        let actual = |assignments: &[(usize, LabelType)]| {
1830            let (loss_computer, state, cms) = build(new(), &draws, assignments);
1831            loss_computer.compute_loss(&state, &cms)
1832        };
1833        // Sequential allocation of item m, up to and including the last item.
1834        for m in 0..n_items {
1835            let assignments: Vec<_> = (0..m).map(|i| (i, (i % 3) as LabelType)).collect();
1836            let (loss_computer, state, cms) = build(new(), &draws, &assignments);
1837            for to_label in 0..4 {
1838                let score = loss_computer.change_in_loss(m, to_label, None, &state, &cms, &draws);
1839                let mut moved = assignments.clone();
1840                moved.push((m, to_label));
1841                assert_relative_eq!(
1842                    to_loss(score, n_draws as f64, (m + 1) as f64),
1843                    actual(&moved),
1844                    max_relative = 1e-12
1845                );
1846            }
1847        }
1848        // Reassignment of item m when all items are allocated.
1849        let assignments: Vec<_> = (0..n_items).map(|i| (i, (i % 3) as LabelType)).collect();
1850        let (loss_computer, state, cms) = build(new(), &draws, &assignments);
1851        for m in 0..n_items {
1852            for to_label in 0..4 {
1853                let from_label = Some(state.get(m));
1854                let score =
1855                    loss_computer.change_in_loss(m, to_label, from_label, &state, &cms, &draws);
1856                let mut moved = assignments.clone();
1857                moved[m].1 = to_label;
1858                assert_relative_eq!(
1859                    to_loss(score, n_draws as f64, n_items as f64),
1860                    actual(&moved),
1861                    max_relative = 1e-12
1862                );
1863            }
1864        }
1865    }
1866
1867    #[test]
1868    fn nvi_change_in_loss_matches_compute_loss() {
1869        check_information_based_change_in_loss(|| NVIInformationBasedLoss, |s, d, _| s / d);
1870    }
1871
1872    #[test]
1873    fn id_change_in_loss_matches_compute_loss() {
1874        check_information_based_change_in_loss(|| IDInformationBasedLoss, |s, d, n| s / (d * n));
1875    }
1876
1877    #[test]
1878    fn nid_change_in_loss_matches_compute_loss() {
1879        check_information_based_change_in_loss(|| NIDInformationBasedLoss, |s, d, _| 1.0 + s / d);
1880    }
1881
1882    fn check_normalized_information_loss<T: InformationBasedLoss>(ibloss: impl Fn() -> T) {
1883        for n_items in [1, 2, 4, 31] {
1884            let cache = Log2Cache::new(n_items);
1885            for one_cluster in [false, true] {
1886                let labels: Vec<_> = (0..n_items)
1887                    .map(|i| if one_cluster { 0 } else { i as i32 })
1888                    .collect();
1889                let draws = Clusterings::from_i32_column_major_order(&labels, n_items);
1890                for same_partition in [false, true] {
1891                    let assignments: Vec<_> = (0..n_items)
1892                        .map(|i| {
1893                            let label = if one_cluster == same_partition {
1894                                0
1895                            } else {
1896                                i as LabelType
1897                            };
1898                            (i, label)
1899                        })
1900                        .collect();
1901                    let new = || GeneralInformationBasedCMLossComputer::new(1, &cache, ibloss());
1902                    let state = WorkingClustering::from_vector(
1903                        assignments.iter().map(|x| x.1).collect(),
1904                        n_items as LabelType,
1905                    );
1906                    let cms = draws.make_confusion_matrices(&state);
1907                    let mut initialized = new();
1908                    initialized.initialize(&state, &cms);
1909                    let expected = if same_partition || n_items == 1 {
1910                        0.0
1911                    } else {
1912                        1.0
1913                    };
1914                    assert_relative_eq!(
1915                        initialized.compute_loss(&state, &cms),
1916                        expected,
1917                        epsilon = 1e-12
1918                    );
1919
1920                    let mut incremental = new();
1921                    let mut state = WorkingClustering::empty(n_items, n_items as LabelType);
1922                    let mut cms = Array3::zeros((n_items + 1, draws.max_clusters() as usize, 1));
1923                    for (item, label) in assignments {
1924                        state.assign(item, label, &mut incremental, &mut cms, &draws);
1925                    }
1926                    assert_relative_eq!(
1927                        incremental.compute_loss(&state, &cms),
1928                        expected,
1929                        epsilon = 1e-12
1930                    );
1931                    if one_cluster {
1932                        let mut rng = Pcg64Mcg::seed_from_u64(7);
1933                        for _ in 0..n_items * 10 {
1934                            let item = rng.random_range(0..n_items);
1935                            let label = rng.random_range(0..n_items) as LabelType;
1936                            state.reassign(item, label, &mut incremental, &mut cms, &draws);
1937                        }
1938                        for item in 0..n_items {
1939                            state.reassign(item, 0, &mut incremental, &mut cms, &draws);
1940                        }
1941                        assert_relative_eq!(
1942                            incremental.compute_loss(&state, &cms),
1943                            0.0,
1944                            epsilon = 1e-12
1945                        );
1946                    }
1947                }
1948            }
1949        }
1950    }
1951
1952    #[test]
1953    fn nvi_handles_one_cluster_and_singleton_partitions() {
1954        check_normalized_information_loss(|| NVIInformationBasedLoss);
1955    }
1956
1957    #[test]
1958    fn nid_handles_one_cluster_and_singleton_partitions() {
1959        check_normalized_information_loss(|| NIDInformationBasedLoss);
1960    }
1961
1962    #[test]
1963    fn salso_handles_zero_entropy_draws() {
1964        for n_items in [1, 4, 31] {
1965            let draws = Clusterings::from_i32_column_major_order(&vec![1; n_items * 3], n_items);
1966            for loss in [LossFunction::NVI, LossFunction::NID] {
1967                for prob_sequential_allocation in [0.0, 1.0] {
1968                    for n_cores in [1, 2] {
1969                        let p = SALSOParameters {
1970                            n_items,
1971                            max_size: 0,
1972                            max_size_as_rf: false,
1973                            max_scans: 10,
1974                            max_zealous_updates: 4,
1975                            n_runs: 4,
1976                            prob_sequential_allocation,
1977                            prob_singletons_initialization: 0.0,
1978                        };
1979                        let result = minimize_by_salso(
1980                            PartitionDistributionInformation::Draws(&draws),
1981                            loss,
1982                            &p,
1983                            f64::INFINITY,
1984                            n_cores,
1985                            &mut Pcg64Mcg::seed_from_u64(1),
1986                        );
1987                        assert_eq!(result.clustering, vec![0; n_items]);
1988                        assert_relative_eq!(result.expected_loss, 0.0, epsilon = 1e-12);
1989                    }
1990                }
1991            }
1992        }
1993    }
1994
1995    // For VI, change_in_loss is a change relative to an unspecified baseline, so compare
1996    // differences between candidate labels.
1997    #[test]
1998    fn vi_change_in_loss_differences_match_compute_loss() {
1999        let n_items = 12;
2000        let n_draws = 9;
2001        let draws = random_draws(n_items, n_draws);
2002        let cache = Log2Cache::new(n_items);
2003        let new = || VICMLossComputer::new(1.0, &cache);
2004        let actual = |assignments: &[(usize, LabelType)]| {
2005            let (loss_computer, state, cms) = build(new(), &draws, assignments);
2006            loss_computer.compute_loss(&state, &cms)
2007        };
2008        let scale = (n_draws * n_items) as f64;
2009        for m in 1..n_items {
2010            let assignments: Vec<_> = (0..m).map(|i| (i, (i % 3) as LabelType)).collect();
2011            let (loss_computer, state, cms) = build(new(), &draws, &assignments);
2012            let change =
2013                |to_label| loss_computer.change_in_loss(m, to_label, None, &state, &cms, &draws);
2014            let mut moved = assignments.clone();
2015            moved.push((m, 0));
2016            let baseline = actual(&moved);
2017            for to_label in 1..4 {
2018                moved[m].1 = to_label;
2019                assert_relative_eq!(
2020                    (change(to_label) - change(0)) / scale,
2021                    actual(&moved) - baseline,
2022                    epsilon = 1e-12
2023                );
2024            }
2025        }
2026        let assignments: Vec<_> = (0..n_items).map(|i| (i, (i % 3) as LabelType)).collect();
2027        let baseline = actual(&assignments);
2028        let (loss_computer, state, cms) = build(new(), &draws, &assignments);
2029        for m in 0..n_items {
2030            let from_label = state.get(m);
2031            let change = |to_label| {
2032                loss_computer.change_in_loss(m, to_label, Some(from_label), &state, &cms, &draws)
2033            };
2034            for to_label in 0..4 {
2035                let mut moved = assignments.clone();
2036                moved[m].1 = to_label;
2037                assert_relative_eq!(
2038                    (change(to_label) - change(from_label)) / scale,
2039                    actual(&moved) - baseline,
2040                    epsilon = 1e-12
2041                );
2042            }
2043        }
2044    }
2045
2046    #[test]
2047    #[should_panic(expected = "cms and draws disagree")]
2048    fn vi_change_in_loss_rejects_mismatched_draws() {
2049        let draws = random_draws(4, 3);
2050        let cache = Log2Cache::new(4);
2051        let state = WorkingClustering::empty(4, 2);
2052        let cms = Array3::<CountType>::zeros((3, draws.max_clusters() as usize, 4));
2053        let _ = VICMLossComputer::new(1.0, &cache).change_in_loss(0, 0, None, &state, &cms, &draws);
2054    }
2055
2056    #[test]
2057    fn salso_runs_with_every_draws_based_loss() {
2058        let n_items = 30;
2059        let draws = random_draws(n_items, 20);
2060        for loss_function in [
2061            LossFunction::BinderDraws(1.0),
2062            LossFunction::OneMinusARI,
2063            LossFunction::VI(1.0),
2064            LossFunction::NVI,
2065            LossFunction::ID,
2066            LossFunction::NID,
2067        ] {
2068            for prob_sequential_allocation in [0.0, 1.0] {
2069                let p = SALSOParameters {
2070                    n_items,
2071                    max_size: 0,
2072                    max_size_as_rf: true,
2073                    max_scans: 10,
2074                    max_zealous_updates: 10,
2075                    n_runs: 4,
2076                    prob_sequential_allocation,
2077                    prob_singletons_initialization: 0.0,
2078                };
2079                let result = minimize_by_salso(
2080                    PartitionDistributionInformation::Draws(&draws),
2081                    loss_function,
2082                    &p,
2083                    5.0,
2084                    1,
2085                    &mut Pcg64Mcg::seed_from_u64(1),
2086                );
2087                assert_eq!(result.clustering.len(), n_items);
2088                assert!(result.expected_loss.is_finite());
2089            }
2090        }
2091    }
2092}