1use crate::algorithms::depth2::ConTreeDepth2;
2use crate::algorithms::interval_pruner::{Bound, IntervalsPruner};
3use crate::algorithms::shared;
4use crate::algorithms::support_feasible_splits;
5use crate::caching::{Cache, Entry, SearchedUnder};
6use crate::common::{
7 classification_error, Budget, BudgetSchedule, FitOutcome, PassReport, PointSelector,
8 ScheduleBounds, ScheduleKind, SearchConfig, SearchError, SearchStatus, Statistics,
9};
10use crate::data::view::DataView;
11use crate::data::{Dataset, Feature};
12use crate::tree::Tree;
13use rand::rngs::StdRng;
14use rand::SeedableRng;
15use std::collections::VecDeque;
16use std::time::Instant;
17
18pub struct ConTreeLds {
27 config: SearchConfig,
28 statistics: Statistics,
29 cache: Cache,
30 specialized: ConTreeDepth2,
31 runtime: Instant,
32 rng: StdRng,
33 max_discrepancy: usize,
34 split_budget: usize,
35 schedule_kind: ScheduleKind,
36 schedule: Option<Box<dyn BudgetSchedule>>,
37 pass_report: PassReport,
39 status: SearchStatus,
40 trajectory: Vec<(f64, usize)>,
42}
43
44impl ConTreeLds {
45 pub fn with_config(config: SearchConfig) -> Self {
50 let mut solver = Self::new(
51 config.min_sup,
52 config.max_depth,
53 config.max_time,
54 config.max_error,
55 config.point_selector,
56 config.max_gap,
57 config.use_heuristic,
58 config.fast_d2,
59 );
60 solver.config = config;
61 solver
62 }
63
64 #[allow(clippy::too_many_arguments)]
66 pub fn new(
67 min_sup: usize,
68 max_depth: usize,
69 max_time: f64,
70 max_error: usize,
71 split_selection_strategy: PointSelector,
72 max_gap: usize,
73 use_heuristic: bool,
74 fast_d2: bool,
75 ) -> Self {
76 let config = SearchConfig::new(
77 min_sup,
78 max_depth,
79 max_time,
80 max_gap,
81 max_error,
82 use_heuristic,
83 fast_d2,
84 split_selection_strategy,
85 );
86
87 Self {
88 cache: Cache::default(),
89 config,
90 statistics: Statistics::default(),
91 specialized: ConTreeDepth2,
92 runtime: Instant::now(),
93 rng: StdRng::from_os_rng(),
94 max_discrepancy: usize::MAX,
95 split_budget: 1,
96 schedule_kind: ScheduleKind::default(),
97 schedule: None,
98 pass_report: PassReport::default(),
99 status: SearchStatus::Optimal,
100 trajectory: Vec::new(),
101 }
102 }
103
104 pub fn with_random_state(mut self, seed: u64) -> Self {
107 self.rng = StdRng::seed_from_u64(seed);
108 self
109 }
110
111 pub fn fit(&mut self, dataset: &Dataset) -> Result<FitOutcome, SearchError> {
118 crate::algorithms::validate(&self.config, dataset)?;
119
120 let root_view = DataView::root(dataset, self.config.use_heuristic);
121 while !self.partial_fit(&root_view) {}
122
123 let tree = self.get_solution_tree();
124 tree.validate()?;
125
126 Ok(FitOutcome {
127 tree,
128 statistics: self.statistics,
129 status: self.status,
130 })
131 }
132
133 pub fn with_schedule(mut self, schedule: ScheduleKind) -> Self {
135 self.schedule_kind = schedule;
136 self
137 }
138
139 pub fn schedule(&self) -> ScheduleKind {
141 self.schedule_kind
142 }
143
144 fn apply(&mut self, budget: Budget) {
145 self.config.budget = budget.discrepancy;
146 self.split_budget = budget.split_budget();
147 }
148
149 pub fn config(&self) -> &SearchConfig {
151 &self.config
152 }
153
154 pub fn trajectory(&self) -> &[(f64, usize)] {
157 &self.trajectory
158 }
159
160 pub fn status(&self) -> SearchStatus {
162 self.status
163 }
164
165 pub fn partial_fit(&mut self, root_view: &DataView<'_>) -> bool {
168 self.config.nb_runs += 1;
169 let mut root_index = 0;
170 let mut entry = Entry::default();
171 if self.config.nb_runs <= 1 {
172 self.cache = Cache::new(self.config.max_depth, root_view.total_instances);
173
174 self.max_discrepancy = self.max_discrepancy.min(Self::discrepancy_limit(
175 root_view.get_feature_number(),
176 self.config.max_depth,
177 ));
178 let mut schedule = self.schedule_kind.build(ScheduleBounds {
179 max_discrepancy: self.max_discrepancy,
180 max_splits: root_view.get_max_splits(),
181 split_applies: self.config.point_selector == PointSelector::First,
182 });
183 let budget = schedule.first();
184 self.apply(budget);
185 self.schedule = Some(schedule);
186 root_index = self.cache.init();
187
188 self.statistics.num_features = root_view.get_feature_number();
189 self.statistics.num_samples = root_view.total_instances;
190
191 let (error, label) = classification_error(root_view.get_labels_freqs());
192 entry.error = error;
193 entry.label = label;
194
195 if let Some(entry) = self.cache.get_mut(root_index) {
196 self.config.max_error = self.config.max_error.min(error);
197 entry.error = error;
198 entry.label = label;
199 entry.lower_bound = 0;
200 entry.is_valid = true;
201 }
202
203 self.runtime = Instant::now();
204 self.trajectory.clear();
205 }
206
207 let mut is_optimal = false;
208
209 if let Some(e) = self.cache.get(root_index) {
210 entry = *e
211 }
212 if entry.is_optimal {
213 return true;
214 }
215
216 let mut config = self.config;
217
218 config.discrepancy = 0;
219
220 self.pass_report = PassReport::default();
221 let error = entry.error;
222 let stopped = self.expand_node_with_view(root_view, &config, &mut entry, 0, true, error);
223 self.pass_report.improved = entry.error < error;
224
225 let next_budget = self
228 .schedule
229 .as_mut()
230 .and_then(|schedule| schedule.next(&self.pass_report));
231 let search_exhausted = next_budget.is_none();
232
233 if !stopped || !self.time_remains() || search_exhausted {
234 entry.is_optimal = true;
235 is_optimal = true;
236 self.status = if !stopped {
237 SearchStatus::Optimal
238 } else if !self.time_remains() {
239 SearchStatus::TimeLimit
240 } else {
241 SearchStatus::BudgetExhausted
242 };
243 }
244
245 if let Some(budget) = next_budget {
246 self.apply(budget);
247 }
248
249 self.statistics.error = entry.error;
250 self.statistics.cache_size = self.cache.len();
251 self.statistics.duration = self.elapsed_time();
252 is_optimal
253 }
254
255 fn cache_child(&mut self, view: &DataView<'_>, child_depth: usize) -> (bool, usize, Entry) {
261 let (is_new, index) = self.cache.insert(&view.bitset, child_depth);
262 let depth = self.config.max_depth - child_depth;
263 let mut entry = Entry::default();
264
265 if let Some(slot) = self.cache.get_mut(index) {
266 if is_new {
267 let (error, label) = classification_error(view.get_labels_freqs());
268 slot.error = error;
269 slot.label = label;
270 slot.depth = depth;
271 slot.lower_bound = 0;
272 slot.is_valid = true;
273 }
274 entry = *slot;
275 }
276 (is_new, index, entry)
277 }
278
279 fn expand_node_with_view(
280 &mut self,
281 view: &DataView<'_>,
282 config: &SearchConfig,
283 current_best: &mut Entry,
284 parent_index: usize,
285 is_new: bool,
286 upper_bound: usize,
287 ) -> bool {
288 if current_best.error == 0 || view.is_empty() {
289 if let Some(entry) = self.cache.get_mut(parent_index) {
290 entry.mark_exact();
291 *current_best = *entry;
292 }
293 return false;
294 }
295
296 let under = SearchedUnder {
299 discrepancy: config.budget.saturating_sub(config.discrepancy),
300 split_budget: if config.point_selector == PointSelector::First
301 || self.config.nb_runs <= 1
302 {
303 self.split_budget
304 } else {
305 usize::MAX
306 },
307 upper_bound,
308 truncated: false,
309 };
310
311 if !is_new {
312 if current_best.is_optimal {
313 self.statistics.cache_hits += 1;
314 return false;
315 }
316 if let Some(previous) = current_best.searched_under {
317 if !previous.truncated && current_best.lower_bound >= upper_bound {
321 self.statistics.cache_hits += 1;
322 return false;
323 }
324 if previous.covers(&under, current_best.is_valid) {
328 self.statistics.cache_hits += 1;
329 return previous.truncated;
330 }
331 }
332 }
333
334 if config.max_depth == 0 {
335 current_best.is_leaf = true;
336 current_best.mark_exact();
337
338 if let Some(entry) = self.cache.get_mut(parent_index) {
339 entry.is_leaf = true;
340 entry.mark_exact();
341 *current_best = *entry;
342 }
343
344 return false;
345 }
346
347 if current_best.error <= config.max_gap || view.len() <= 1 {
348 return false;
349 }
350
351 if config.fast_d2 && config.max_depth == 2 {
354 let tree =
358 self.specialized
359 .fit(view, config, current_best, usize::MAX, &mut self.statistics);
360 current_best.mark_exact();
361 let tree_index = self.cache.insert_tree(tree);
362 if let Some(entry) = self.cache.get_mut(parent_index) {
363 *entry = *current_best;
364 entry.tree_idx = Some(tree_index);
365 }
366 self.statistics.specialized_solver_call += 1;
367
368 return false;
369 }
370
371 let num_features = view.get_feature_number();
372 let heuristics_data = view.features_best_score();
373 debug_assert!(
374 num_features == heuristics_data.len(),
375 "Missmatch with heuristics and features number"
376 );
377 let mut stopped = false;
378 for (it, &(_, feat)) in heuristics_data.iter().take(num_features).enumerate() {
379 let feat_discrepancy = config.discrepancy + it;
380 if feat_discrepancy > config.budget {
381 self.pass_report.cut_by_discrepancy = true;
384 stopped = true;
385 break;
386 }
387
388 let mut node_config = *config;
389 node_config.discrepancy = feat_discrepancy;
390
391 stopped |= self.expand_on_feature(
392 view,
393 feat,
394 parent_index,
395 &node_config,
396 current_best,
397 upper_bound.min(current_best.error),
398 );
399
400 if current_best.error == 0 {
401 current_best.mark_exact();
402 if let Some(entry) = self.cache.get_mut(parent_index) {
403 entry.mark_exact();
404 }
405
406 return false;
407 }
408
409 if !self.time_remains() {
410 return true;
411 }
412 }
413 current_best.finalize_lower_bound(upper_bound);
417 current_best.searched_under = Some(SearchedUnder {
418 truncated: stopped,
419 ..under
420 });
421 if let Some(entry) = self.cache.get_mut(parent_index) {
422 entry.lower_bound = current_best.lower_bound;
423 entry.is_valid = current_best.is_valid;
424 entry.is_optimal = !stopped && current_best.is_valid;
425 entry.searched_under = current_best.searched_under;
426 }
427 current_best.is_optimal = !stopped && current_best.is_valid;
428
429 stopped
430 }
431
432 fn expand_on_feature(
433 &mut self,
434 view: &DataView<'_>,
435 feature_index: usize,
436 cache_index: usize,
437 config: &SearchConfig,
438 current_best: &mut Entry,
439 upper_bound: usize,
440 ) -> bool {
441 let feature_column = view.get_sorted_feature(feature_index);
442 let feature_column_ids = view.get_feature_indices(feature_index);
443
444 if config.point_selector == PointSelector::First || self.config.nb_runs <= 1 {
445 let stopped = self.expand_on_feature_gini_priority(
446 view,
447 feature_index,
448 cache_index,
449 config,
450 current_best,
451 upper_bound,
452 );
453 return stopped;
454 }
455
456 let possible_index_split = view.get_possible_split_indices(feature_index);
457 if possible_index_split.is_empty() {
458 return false;
459 }
460
461 let feasible =
465 support_feasible_splits(possible_index_split, view.len(), self.config.min_sup);
466 if feasible.is_empty() {
467 return false;
468 }
469
470 let mut pruner = IntervalsPruner::new(possible_index_split, config.max_gap, config.min_sup);
471 let mut queue = VecDeque::new();
472 let init_bound = Bound::new(feasible.start, feasible.end - 1, None, None);
473 queue.push_back(init_bound);
474
475 let mut stopped = false;
476
477 while let Some(mut current_bound) = queue.pop_front() {
478 if !self.time_remains() {
479 return true;
480 }
481
482 if pruner.subinterval_pruning(¤t_bound, current_best.error.min(upper_bound)) {
486 continue;
487 }
488
489 pruner.interval_shrinking(&mut current_bound, current_best.error.min(upper_bound));
490 if !current_bound.is_valid() {
491 continue;
492 }
493
494 let selected_point = self.select_point(config, ¤t_bound);
495 let split_point = possible_index_split[selected_point];
496 let int_half_distance = split_point
497 .saturating_sub(possible_index_split[current_bound.left_bound])
498 .max(possible_index_split[current_bound.right_bound].saturating_sub(split_point));
499
500 let threshold_value = if selected_point > 0 {
501 let previous = feature_column_ids[possible_index_split[selected_point - 1]];
502 let point = feature_column_ids[split_point];
503 shared::threshold_between(
504 feature_column[previous].value(),
505 feature_column[point].value(),
506 )
507 } else {
508 let point = feature_column_ids[split_point];
509 shared::threshold_between(
510 feature_column[feature_column_ids[0]].value(),
511 feature_column[point].value(),
512 )
513 };
514
515 let (left_view, right_view) = view.split(feature_index, split_point);
516
517 if left_view.len() < self.config.min_sup || right_view.len() < self.config.min_sup {
518 continue;
519 }
520
521 let process_left_first = left_view.len() >= right_view.len();
524
525 let left_config = config.derive_left();
526 let mut left_entry = Entry::default();
527 let mut right_entry = Entry::default();
528
529 let (mut left_index, mut right_index) = (0, 0);
532 let (mut left_is_new, mut right_is_new);
533
534 let larger_upper_bound = current_best.error.min(upper_bound.saturating_add(1));
535 self.statistics.general_solver_call += 1;
536
537 if process_left_first {
538 (left_is_new, left_index, left_entry) =
539 self.cache_child(&left_view, left_config.max_depth);
540
541 stopped |= self.expand_node_with_view(
542 &left_view,
543 &left_config,
544 &mut left_entry,
545 left_index,
546 left_is_new,
547 larger_upper_bound,
548 );
549 left_entry.finalize_lower_bound(larger_upper_bound);
550 } else {
551 (right_is_new, right_index, right_entry) =
552 self.cache_child(&right_view, left_config.max_depth);
553
554 stopped |= self.expand_node_with_view(
555 &right_view,
556 &left_config,
557 &mut right_entry,
558 right_index,
559 right_is_new,
560 larger_upper_bound,
561 );
562 right_entry.finalize_lower_bound(larger_upper_bound);
563 }
564
565 let larger_error = if process_left_first {
566 left_entry.lower_bound
567 } else {
568 right_entry.lower_bound
569 };
570 let budget =
574 current_best.error.min(upper_bound.saturating_add(1)) as i64 - larger_error as i64;
575 let smaller_ub = budget + int_half_distance as i64;
576 let smaller_upper_bound = smaller_ub.max(0) as usize;
577 let mut right_error: Option<usize> = None;
580
581 if smaller_ub > 0 || budget == 0 {
585 self.statistics.general_solver_call += 1;
586 let right_config = config.derive_right(left_config.max_gap);
587
588 if process_left_first {
589 (right_is_new, right_index, right_entry) =
590 self.cache_child(&right_view, right_config.max_depth);
591
592 stopped |= self.expand_node_with_view(
593 &right_view,
594 &right_config,
595 &mut right_entry,
596 right_index,
597 right_is_new,
598 smaller_upper_bound,
599 );
600 right_entry.finalize_lower_bound(smaller_upper_bound);
601 } else {
602 (left_is_new, left_index, left_entry) =
603 self.cache_child(&left_view, right_config.max_depth);
604
605 stopped |= self.expand_node_with_view(
606 &left_view,
607 &right_config,
608 &mut left_entry,
609 left_index,
610 left_is_new,
611 smaller_upper_bound,
612 );
613 left_entry.finalize_lower_bound(smaller_upper_bound);
614 }
615
616 right_error = Some(right_entry.lower_bound);
619
620 let feature_best = left_entry.lower_bound + right_entry.lower_bound;
621 if left_entry.is_valid && right_entry.is_valid && feature_best < current_best.error
622 {
623 current_best.error = feature_best;
624 if config.is_root {
625 self.trajectory.push((self.elapsed_time(), feature_best));
626 }
627 current_best.feature = feature_index;
628 current_best.split = threshold_value;
629 current_best.left = left_index;
630 current_best.right = right_index;
631
632 if let Some(entry) = self.cache.get_mut(cache_index) {
633 *entry = *current_best;
634 }
635
636 if feature_best == 0 {
637 return false;
638 }
639 }
640 }
641
642 let left_score = left_entry.is_valid.then_some(left_entry.lower_bound);
646 pruner.add_result(selected_point, left_score, right_error);
647 if current_bound.left_bound == current_bound.right_bound {
648 continue;
649 }
650
651 let score_difference = left_entry
652 .lower_bound
653 .saturating_add(right_error.unwrap_or(0))
654 .saturating_sub(current_best.error.min(upper_bound));
655
656 let (left_bound, right_bound) = pruner.neighbourhood_pruning(
657 score_difference,
658 current_bound.left_bound,
659 current_bound.right_bound,
660 selected_point,
661 );
662
663 if left_bound <= current_bound.right_bound {
664 queue.push_back(Bound {
665 left_bound,
666 right_bound: current_bound.right_bound,
667 last_split_left_index: Some(selected_point),
668 last_split_right_index: current_bound.last_split_right_index,
669 });
670 }
671
672 if current_bound.left_bound <= right_bound {
673 queue.push_back(Bound {
674 left_bound: current_bound.left_bound,
675 right_bound,
676 last_split_left_index: current_bound.last_split_left_index,
677 last_split_right_index: Some(selected_point),
678 });
679 }
680 }
681 stopped
682 }
683
684 fn expand_on_feature_gini_priority(
688 &mut self,
689 view: &DataView<'_>,
690 feature_index: usize,
691 cache_index: usize,
692 config: &SearchConfig,
693 current_best: &mut Entry,
694 upper_bound: usize,
695 ) -> bool {
696 let feature_column = view.get_sorted_feature(feature_index);
697 let feature_column_ids = view.get_feature_indices(feature_index);
698
699 let possible_splits = view.get_possible_split_indices(feature_index);
700
701 if possible_splits.is_empty() {
702 return false;
703 }
704
705 let mut pruner = IntervalsPruner::new(possible_splits, config.max_gap, config.min_sup);
706
707 let mut pruned = vec![false; possible_splits.len()];
709 let mut stopped = false;
710
711 let local_split_budget = possible_splits.len().min(self.split_budget);
712
713 if config.use_heuristic {
714 let sorted_indices = view.ordered_possible_splits(feature_index);
715 for (idx, &split_idx) in sorted_indices.iter().enumerate() {
716 if self.config.nb_runs <= 1 && idx > 0 {
717 self.pass_report.cut_by_split = true;
718 return true;
719 }
720
721 if idx > local_split_budget - 1 {
722 self.pass_report.cut_by_split = true;
723 return true;
724 }
725
726 if !self.time_remains() {
727 return true;
728 }
729
730 if pruned[split_idx] {
731 continue;
732 }
733
734 stopped |= self.evaluate_split_gini_priority(
735 view,
736 feature_index,
737 split_idx,
738 possible_splits,
739 feature_column,
740 feature_column_ids,
741 config,
742 current_best,
743 cache_index,
744 upper_bound,
745 &mut pruner,
746 &mut pruned,
747 );
748
749 if current_best.error == 0 {
750 return false;
751 }
752 }
753 } else {
754 for split_idx in 0..possible_splits.len() {
755 if self.config.nb_runs <= 1 && split_idx > 0 {
756 self.pass_report.cut_by_split = true;
757 return true;
758 }
759
760 if split_idx > local_split_budget - 1 {
761 self.pass_report.cut_by_split = true;
762 return true;
763 }
764
765 if !self.time_remains() {
766 return true;
767 }
768
769 if pruned[split_idx] {
770 continue;
771 }
772
773 stopped |= self.evaluate_split_gini_priority(
774 view,
775 feature_index,
776 split_idx,
777 possible_splits,
778 feature_column,
779 feature_column_ids,
780 config,
781 current_best,
782 cache_index,
783 upper_bound,
784 &mut pruner,
785 &mut pruned,
786 );
787
788 if current_best.error == 0 {
789 return false;
790 }
791 }
792 }
793
794 if local_split_budget < possible_splits.len() {
795 self.pass_report.cut_by_split = true;
796 stopped = true;
797 }
798
799 stopped
800 }
801
802 #[inline]
803 #[allow(clippy::too_many_arguments)]
804 fn evaluate_split_gini_priority(
805 &mut self,
806 view: &DataView<'_>,
807 feature_index: usize,
808 split_idx: usize,
809 possible_splits: &[usize],
810 feature_column: &Feature,
811 feature_column_ids: &[usize],
812 config: &SearchConfig,
813 current_best: &mut Entry,
814 cache_index: usize,
815 upper_bound: usize,
816 pruner: &mut IntervalsPruner<'_>,
817 pruned: &mut [bool],
818 ) -> bool {
819 let mut current_left = split_idx;
822 while current_left > 0 && pruned[current_left - 1] {
823 current_left -= 1;
824 }
825 current_left = current_left.saturating_sub(1);
826
827 let mut current_right = split_idx;
828 while current_right < possible_splits.len() - 1 && pruned[current_right + 1] {
829 current_right += 1;
830 }
831 if current_right < possible_splits.len() - 1 {
832 current_right += 1;
833 }
834
835 let split_point = possible_splits[split_idx];
836
837 let threshold_value = if split_idx > 0 {
838 let previous = feature_column_ids[possible_splits[split_idx - 1]];
839 let point = feature_column_ids[split_point];
840 shared::threshold_between(
841 feature_column[previous].value(),
842 feature_column[point].value(),
843 )
844 } else {
845 let point = feature_column_ids[split_point];
846 shared::threshold_between(
847 feature_column[feature_column_ids[0]].value(),
848 feature_column[point].value(),
849 )
850 };
851
852 let (left_view, right_view) = view.split(feature_index, split_point);
853
854 if left_view.len() < self.config.min_sup || right_view.len() < self.config.min_sup {
855 pruned[split_idx] = true;
856 return false;
857 }
858
859 let process_left_first = left_view.len() >= right_view.len();
862
863 let left_config = config.derive_left();
864 let mut left_entry = Entry::default();
865 let mut right_entry = Entry::default();
866
867 let (mut left_index, mut right_index) = (0, 0);
870 let (mut left_is_new, mut right_is_new);
871 let mut stopped = false;
872
873 let int_half_distance = split_point
874 .saturating_sub(possible_splits[0])
875 .max(possible_splits[possible_splits.len() - 1].saturating_sub(split_point));
876
877 let larger_upper_bound = current_best.error.min(upper_bound.saturating_add(1));
878 self.statistics.general_solver_call += 1;
879
880 if process_left_first {
881 (left_is_new, left_index, left_entry) =
882 self.cache_child(&left_view, left_config.max_depth);
883
884 stopped |= self.expand_node_with_view(
885 &left_view,
886 &left_config,
887 &mut left_entry,
888 left_index,
889 left_is_new,
890 larger_upper_bound,
891 );
892 left_entry.finalize_lower_bound(larger_upper_bound);
893 } else {
894 (right_is_new, right_index, right_entry) =
895 self.cache_child(&right_view, left_config.max_depth);
896
897 stopped |= self.expand_node_with_view(
898 &right_view,
899 &left_config,
900 &mut right_entry,
901 right_index,
902 right_is_new,
903 larger_upper_bound,
904 );
905 right_entry.finalize_lower_bound(larger_upper_bound);
906 }
907
908 let larger_error = if process_left_first {
909 left_entry.lower_bound
910 } else {
911 right_entry.lower_bound
912 };
913 let budget =
917 current_best.error.min(upper_bound.saturating_add(1)) as i64 - larger_error as i64;
918 let smaller_ub = budget + int_half_distance as i64;
919 let smaller_upper_bound = smaller_ub.max(0) as usize;
920 let mut right_error: Option<usize> = None;
921
922 if smaller_ub > 0 || budget == 0 {
925 self.statistics.general_solver_call += 1;
926 let right_config = config.derive_right(left_config.max_gap);
927
928 if process_left_first {
929 (right_is_new, right_index, right_entry) =
930 self.cache_child(&right_view, right_config.max_depth);
931
932 stopped |= self.expand_node_with_view(
933 &right_view,
934 &right_config,
935 &mut right_entry,
936 right_index,
937 right_is_new,
938 smaller_upper_bound,
939 );
940 right_entry.finalize_lower_bound(smaller_upper_bound);
941 } else {
942 (left_is_new, left_index, left_entry) =
943 self.cache_child(&left_view, right_config.max_depth);
944
945 stopped |= self.expand_node_with_view(
946 &left_view,
947 &right_config,
948 &mut left_entry,
949 left_index,
950 left_is_new,
951 smaller_upper_bound,
952 );
953 left_entry.finalize_lower_bound(smaller_upper_bound);
954 }
955
956 right_error = Some(right_entry.lower_bound);
959
960 let feature_best = left_entry.lower_bound + right_entry.lower_bound;
961 if left_entry.is_valid && right_entry.is_valid && feature_best < current_best.error {
962 current_best.error = feature_best;
963 if config.is_root {
964 self.trajectory.push((self.elapsed_time(), feature_best));
965 }
966 current_best.feature = feature_index;
967 current_best.split = threshold_value;
968 current_best.left = left_index;
969 current_best.right = right_index;
970
971 if let Some(entry) = self.cache.get_mut(cache_index) {
972 *entry = *current_best;
973 }
974
975 if feature_best == 0 {
976 return false;
977 }
978 }
979 }
980
981 let left_score = left_entry.is_valid.then_some(left_entry.lower_bound);
985 pruner.add_result(split_idx, left_score, right_error);
986 pruned[split_idx] = true;
987
988 let score_difference = left_entry
989 .lower_bound
990 .saturating_add(right_error.unwrap_or(0))
991 .saturating_sub(current_best.error.min(upper_bound));
992 let (new_left_bound, new_right_bound) =
993 pruner.neighbourhood_pruning(score_difference, current_left, current_right, split_idx);
994
995 let skip_from = new_right_bound.saturating_add(1).max(current_left);
999 if skip_from < split_idx {
1000 pruned[skip_from..split_idx].fill(true);
1001 }
1002 let skip_to = new_left_bound.min(current_right + 1);
1003 if split_idx + 1 < skip_to {
1004 pruned[split_idx + 1..skip_to].fill(true);
1005 }
1006 stopped
1007 }
1008
1009 fn discrepancy_limit(nb_candidates: usize, remaining_depth: usize) -> usize {
1010 let mut max_discrepancy = nb_candidates;
1011 for i in 1..remaining_depth {
1012 max_discrepancy += nb_candidates.saturating_sub(i);
1013 }
1014
1015 max_discrepancy
1016 }
1017
1018 pub fn statistics(&self) -> &Statistics {
1019 &self.statistics
1020 }
1021
1022 fn time_remains(&self) -> bool {
1023 shared::time_remains(&self.runtime, self.config.max_time)
1024 }
1025
1026 fn elapsed_time(&self) -> f64 {
1027 shared::elapsed_time(&self.runtime)
1028 }
1029
1030 pub fn select_point(&mut self, config: &SearchConfig, bound: &Bound) -> usize {
1031 shared::select_point(config.point_selector, &mut self.rng, bound)
1032 }
1033
1034 pub fn get_solution_tree(&mut self) -> Tree {
1036 shared::build_solution_tree(&self.cache)
1037 }
1038}
1039
1040#[cfg(test)]
1041mod contree_lds_test {
1042 use crate::algorithms::{ConTree, ConTreeLds};
1043 use crate::common::PointSelector;
1044 use crate::reader::data_reader::DataReader;
1045 use crate::reader::DataReaderError;
1046 use crate::tests::fixture;
1047
1048 #[test]
1049 fn lds_is_never_better_than_the_exhaustive_optimum() -> Result<(), DataReaderError> {
1050 let reader = DataReader::default();
1051 let mut dataset = reader.read_file(&fixture("hepatitis.txt"))?;
1052 dataset.sort_features();
1053
1054 let mut exhaustive: ConTree =
1055 ConTree::new(1, 2, 100.0, usize::MAX, PointSelector::Mid, 0, false, true);
1056 exhaustive.fit(&dataset).expect("fit");
1057
1058 let mut lds: ConTreeLds =
1059 ConTreeLds::new(1, 2, 100.0, usize::MAX, PointSelector::Mid, 0, false, true);
1060 lds.fit(&dataset).expect("fit");
1061
1062 assert!(
1066 lds.statistics.error >= exhaustive.statistics().error,
1067 "LDS reported {} against an exhaustive optimum of {}",
1068 lds.statistics.error,
1069 exhaustive.statistics().error
1070 );
1071 assert!(lds.cache.len() > 0, "the cache was never written to");
1072
1073 Ok(())
1074 }
1075
1076 #[test]
1077 fn the_anytime_loop_terminates_without_leaning_on_the_time_limit() {
1078 let reader = DataReader::default();
1081 let mut dataset = reader.read_file(&fixture("small.txt")).unwrap();
1082 dataset.sort_features();
1083
1084 let mut lds: ConTreeLds = ConTreeLds::new(
1085 1,
1086 3,
1087 f64::INFINITY,
1088 usize::MAX,
1089 PointSelector::Mid,
1090 0,
1091 false,
1092 false,
1093 );
1094 lds.fit(&dataset).expect("fit");
1095
1096 assert!(lds.statistics.error <= dataset.count());
1097 }
1098}