Skip to main content

dtrees_rs/algorithms/optimal/dl85/
mod.rs

1use crate::algorithms::common::errors::ErrorWrapper;
2use crate::algorithms::common::heuristics::Heuristic;
3use crate::algorithms::common::types::{
4    BranchingChoice, BranchingPolicy, FitError, NodeDataType, RuleType, SearchResult,
5    SearchStatistics,
6};
7use crate::algorithms::optimal::depth2::OptimalDepth2Tree;
8use crate::algorithms::optimal::dl85::config::DL85Config;
9use crate::algorithms::optimal::rules::common::{SimilarityLowerBoundRule, TimeLimitRule};
10use crate::algorithms::optimal::rules::{
11    DiscrepancyRule, GainRule, Rule, RuleContext, RuleManager,
12};
13use crate::algorithms::optimal::Reason;
14use crate::algorithms::TreeSearchAlgorithm;
15use crate::caching::{CacheEntry, CacheKey, Caching, Index, SearchPath, EXACT};
16use crate::cover::similarities::SimilarityCover;
17use crate::cover::Cover;
18use crate::globals::{attribute, float_is_null, item};
19use crate::tree::{NodeInfos, Tree, TreeNode};
20
21mod builder;
22pub mod config;
23
24pub use builder::DL85Builder;
25
26/// DL8.5: optimal decision trees over binary features by dynamic programming
27/// with branch-and-bound and a cache of subproblems.
28///
29/// A subproblem is the set of instances reaching a node, identified by the
30/// itemset of tests on the path to it. The search explores features at each
31/// node, prunes children with the upper bound left by their sibling, and
32/// caches every solved subproblem so it is solved once.
33///
34/// The search is also anytime. Relaxable search rules (see
35/// [`rules`](crate::algorithms::optimal::rules)) restrict each pass, and the
36/// search restarts with widened budgets until a pass completes, which proves
37/// the tree optimal.
38///
39/// Aglin, Nijssen and Schaus, *Learning Optimal Decision Trees Using Caching
40/// Branch-and-Bound Search* (AAAI 2020). Build one with [`DL85Builder`].
41pub struct DL85<C, D, E, H>
42where
43    C: Caching + ?Sized,
44    D: OptimalDepth2Tree + ?Sized,
45    E: ErrorWrapper + ?Sized,
46    H: Heuristic + ?Sized,
47{
48    config: DL85Config,
49    cache: Box<C>,
50    error_fn: Box<E>,
51    depth2_search: Box<D>,
52    heuristic_fn: Box<H>,
53    search_rules: RuleManager,
54    node_rules: RuleManager,
55    time_rule: TimeLimitRule,
56    similarity_rule: SimilarityLowerBoundRule,
57    statistics: SearchStatistics,
58    tree: Tree,
59    root_candidates: Vec<usize>,
60    gain_gap: f64,
61    incumbents: Vec<(f64, f64)>,
62}
63
64impl<C, D, E, H> TreeSearchAlgorithm for DL85<C, D, E, H>
65where
66    C: Caching + ?Sized,
67    D: OptimalDepth2Tree + ?Sized,
68    E: ErrorWrapper + ?Sized,
69    H: Heuristic + ?Sized,
70{
71    /// Runs passes until one completes or the time limit is reached.
72    fn fit(&mut self, cover: &mut Cover) -> Result<(), FitError> {
73        let mut result = SearchResult {
74            reason: Reason::RuleReason,
75            ..Default::default()
76        };
77
78        while result.reason == Reason::RuleReason && !self.time_rule.exhausted() {
79            result = self.partial_fit(cover);
80        }
81        Ok(())
82    }
83
84    fn tree(&self) -> &Tree {
85        &self.tree
86    }
87}
88
89impl<C, D, E, H> DL85<C, D, E, H>
90where
91    C: Caching + ?Sized,
92    D: OptimalDepth2Tree + ?Sized,
93    E: ErrorWrapper + ?Sized,
94    H: Heuristic + ?Sized,
95{
96    /// Assembles a search from its parts. Prefer [`DL85Builder`].
97    #[allow(clippy::too_many_arguments)]
98    pub fn new(
99        config: DL85Config,
100        cache: Box<C>,
101        depth2_search: Box<D>,
102        error_fn: Box<E>,
103        heuristic_fn: Box<H>,
104        node_rules: RuleManager,
105        search_rules: RuleManager,
106        time_rule: TimeLimitRule,
107    ) -> Self {
108        Self {
109            config,
110            cache,
111            error_fn,
112            depth2_search,
113            heuristic_fn,
114            search_rules,
115            node_rules,
116            time_rule,
117            similarity_rule: SimilarityLowerBoundRule::new(),
118            statistics: SearchStatistics::default(),
119            tree: Tree::default(),
120            root_candidates: vec![],
121            gain_gap: 0.0,
122            incumbents: vec![],
123        }
124    }
125
126    /// The configuration of the search.
127    pub fn config(&self) -> DL85Config {
128        self.config
129    }
130
131    /// Runs one pass of the search and rebuilds the tree.
132    ///
133    /// The first call initialises the cache and the rules. When the result's
134    /// reason is [`Reason::RuleReason`], a rule cut the pass short and the
135    /// rules are relaxed for the next one; any other reason means the search
136    /// is over.
137    pub fn partial_fit(&mut self, cover: &mut Cover) -> SearchResult {
138        self.statistics.increment_restarts();
139
140        let mut root_context = RuleContext::default();
141
142        if self.statistics.restarts() <= 1 {
143            self.cache.init();
144            self.statistics.num_attributes = cover.num_attributes;
145            self.statistics.num_samples = cover.count();
146
147            if let Some(discrepancy_rule) = self.search_rules.get_rule_mut::<DiscrepancyRule>() {
148                discrepancy_rule.update_to_true_limit(
149                    self.statistics.num_attributes,
150                    self.config.base.max_depth,
151                );
152            }
153
154            let (error, label) = self.compute_leaf_error(cover);
155            self.cache.update_root().map(|updater| {
156                updater
157                    .leaf_error(error)
158                    .output(label)
159                    .size(self.statistics.num_samples)
160            });
161
162            let mut candidates =
163                self.get_candidates(cover, self.config.base.min_support, None, None);
164            self.heuristic_fn.compute(cover, &mut candidates);
165            self.root_candidates = candidates;
166
167            let bound = <f64>::min(error, self.config.base.max_error);
168
169            let branch_item = usize::MAX;
170            root_context.item(branch_item);
171            root_context.position(0);
172            root_context.discrepancy(0);
173            root_context.upper_bound(bound);
174            root_context.error(bound);
175
176            self.time_rule.activate();
177            self.node_rules.activate_all();
178            self.search_rules.activate_all();
179            if self.config.use_similarity_lb() {
180                self.similarity_rule.activate();
181            }
182        } else {
183            // Keep `max_error` as bound when the previous pass found no tree.
184            let bound = <f64>::min(self.statistics.tree_error, self.config.base.max_error);
185            root_context.upper_bound(bound);
186            root_context.error(bound);
187        }
188        let root_index = self.cache.root_index();
189        let node_ub = self
190            .cache
191            .root()
192            .map_or(f64::INFINITY, |node| node.upper_bound());
193
194        root_context.node_upper_bound(node_ub);
195        root_context.leaf_error(
196            self.cache
197                .root()
198                .map_or(f64::INFINITY, |node| node.leaf_error()),
199        );
200        root_context.support(self.statistics.num_samples);
201
202        let mut similarity = SimilarityCover::default();
203        let mut search_path = SearchPath::new();
204        let candidates = std::mem::take(&mut self.root_candidates);
205
206        let mut result = self.recursive_search(
207            cover,
208            &mut search_path,
209            &candidates,
210            0,
211            usize::MAX,
212            root_index,
213            true,
214            &mut similarity,
215            &mut root_context,
216        );
217
218        self.root_candidates = candidates;
219
220        if self.statistics.restarts() <= 1 || self.gain_gap <= 0.0 {
221            if let Some(gain_rule) = self.search_rules.get_rule_mut::<GainRule>() {
222                gain_rule.update_gap_delta(self.gain_gap);
223            }
224        }
225
226        if result.reason == Reason::RuleReason {
227            self.node_rules.relax_all();
228            self.search_rules.relax_all();
229        }
230
231        if !self.node_rules.is_active() && !self.search_rules.is_active() {
232            result.reason = Reason::Done;
233        }
234
235        self.statistics.duration = self.time_rule.elapsed_seconds();
236        self.statistics.cache_size = self.cache.size();
237        self.statistics.tree_error = self.build_solution_tree(cover).unwrap_or(result.error);
238        result
239    }
240
241    /// Searches the best subtree for the node reached by `parent_item`.
242    /// `intersected` tells whether `cover` is already on the node's instances.
243    #[allow(clippy::too_many_arguments)]
244    fn recursive_search(
245        &mut self,
246        cover: &mut Cover,
247        path: &mut SearchPath,
248        candidates: &[usize],
249        depth: usize,
250        parent_item: usize,
251        parent_index: Index,
252        intersected: bool,
253        similarity: &mut SimilarityCover,
254        parent_context: &mut RuleContext,
255    ) -> SearchResult {
256        self.statistics.increment_search_space();
257
258        let mut subtree_upper_bound = parent_context.upper_bound;
259        let parent_key = parent_index.to_cache_key();
260        let result = self.evaluate(parent_context, &parent_key, RuleType::Node);
261
262        if !result.0 {
263            return SearchResult {
264                error: result.2,
265                has_intersected: intersected,
266                reason: result.1,
267            };
268        }
269
270        if !intersected {
271            cover.branch_on(parent_item);
272        }
273
274        if self.config.use_similarity_lb() {
275            let similarity = similarity.compute_similarity(cover.sparse());
276            parent_context.node_lower_bound = similarity.max(parent_context.node_lower_bound);
277            let result = self.evaluate_node(parent_context, &parent_key, RuleType::Similarity);
278            if !result.0 {
279                return SearchResult {
280                    error: result.2,
281                    has_intersected: true,
282                    reason: result.1,
283                };
284            }
285        }
286
287        let mut node_candidates = self.get_candidates(
288            cover,
289            self.config.base.min_support,
290            Some(candidates),
291            Some(attribute(parent_item)),
292        );
293
294        if node_candidates.is_empty() {
295            let error = self
296                .cache
297                .update_node(&parent_key)
298                .map_or(f64::INFINITY, |updater| {
299                    updater.leaf().upper_bound(EXACT).optimal().get_error()
300                });
301            return SearchResult {
302                error,
303                has_intersected: true,
304                reason: Reason::NoCandidates,
305            };
306        }
307
308        if self.config.use_depth2_optimization() && self.config.base.max_depth - depth <= 2 {
309            let result = self.apply_specialized_depth2_search(
310                cover,
311                &node_candidates,
312                parent_index,
313                subtree_upper_bound,
314                path,
315                self.config.base.max_depth - depth,
316            );
317
318            match result {
319                Err(_) => {}
320                Ok(search_result) => return search_result,
321            }
322        }
323
324        // A tree must also beat the best one found so far for the node.
325        if let Some(node) = self.cache.node(&parent_key) {
326            subtree_upper_bound = subtree_upper_bound.min(node.error());
327        }
328
329        let mut scores = vec![];
330        if self.config.always_sort {
331            scores = self.heuristic_fn.compute(cover, &mut node_candidates);
332        }
333
334        let mut subtree_similarity_data = SimilarityCover::default();
335        let mut min_lower_bound = <f64>::INFINITY;
336
337        let mut rule_pruned = false;
338        for (position, &child) in node_candidates.iter().enumerate() {
339            let mut branch_context = RuleContext::default();
340            branch_context.discrepancy(parent_context.discrepancy + position);
341            branch_context.position(position);
342
343            if scores.len() > 1 {
344                branch_context.gain(parent_context.gain + (scores[0] - scores[position]));
345                if self.gain_gap <= 0.0
346                    || (self.statistics.restarts() <= 1 && branch_context.gain < self.gain_gap)
347                {
348                    self.gain_gap = branch_context.gain;
349                }
350            }
351
352            let search_result = self.evaluate_node(&branch_context, &parent_key, RuleType::Search);
353
354            if !search_result.0 {
355                return SearchResult {
356                    error: search_result.2,
357                    has_intersected: true,
358                    reason: search_result.1,
359                };
360            }
361
362            let (first_branch, first_lb, second_lb) =
363                self.determine_branch_strategy(child, path, cover, &subtree_similarity_data);
364            branch_context.depth(depth + 1);
365            let branch_item = item(child, first_branch);
366            branch_context.item(branch_item);
367            branch_context.node_lower_bound(first_lb);
368            branch_context.upper_bound(subtree_upper_bound);
369
370            let (first_result, branch_key) = self.process_branch(
371                cover,
372                path,
373                &mut branch_context,
374                &mut subtree_similarity_data,
375                &node_candidates,
376                depth,
377            );
378
379            rule_pruned |= first_result.reason == Reason::RuleReason;
380            let first_lb = self.known_lower_bound(&branch_key);
381
382            // The second branch only has what the first one left of the
383            // upper bound; if that cannot cover its lower bound, skip it.
384            if first_result.error >= subtree_upper_bound - second_lb {
385                min_lower_bound = min_lower_bound.min(first_lb + second_lb);
386                self.statistics.increment_sibling_pruning();
387                continue;
388            }
389
390            let mut branch_context = RuleContext::default();
391            let right_ub = subtree_upper_bound - first_result.error;
392            let branch_item = item(child, 1 - first_branch);
393            branch_context.item(branch_item);
394            branch_context.depth(depth + 1);
395            branch_context.position(position);
396            branch_context.discrepancy(parent_context.discrepancy + position);
397            branch_context.node_lower_bound(second_lb);
398            branch_context.upper_bound(right_ub);
399
400            let (second_result, second_key) = self.process_branch(
401                cover,
402                path,
403                &mut branch_context,
404                &mut subtree_similarity_data,
405                &node_candidates,
406                depth,
407            );
408
409            rule_pruned |= second_result.reason == Reason::RuleReason;
410
411            let subtree_error = first_result.error + second_result.error;
412            if subtree_error < subtree_upper_bound {
413                subtree_upper_bound = subtree_error;
414                if depth == 0 {
415                    self.incumbents
416                        .push((self.time_rule.elapsed_seconds(), subtree_error));
417                }
418                let optimal = self
419                    .cache
420                    .update_node(&parent_key)
421                    .is_some_and(|mut updater| {
422                        updater = updater.error(subtree_error).test(child);
423
424                        if float_is_null(updater.get_lower_bound() - subtree_error) {
425                            updater.upper_bound(EXACT).optimal();
426                            return true;
427                        }
428                        false
429                    });
430
431                if optimal {
432                    return SearchResult {
433                        error: subtree_error,
434                        has_intersected: true,
435                        reason: Reason::Done,
436                    };
437                }
438            } else {
439                min_lower_bound =
440                    min_lower_bound.min(first_lb + self.known_lower_bound(&second_key));
441            }
442        }
443
444        let error = self
445            .cache
446            .update_node(&parent_key)
447            .map_or(f64::INFINITY, |mut updater| {
448                if rule_pruned {
449                    return updater.upper_bound(f64::INFINITY).get_error();
450                }
451                updater = updater.optimal().upper_bound(parent_context.upper_bound);
452                let error = updater.get_error();
453                if error.is_infinite() {
454                    let lb = updater
455                        .get_lower_bound()
456                        .max(min_lower_bound.max(parent_context.upper_bound));
457                    updater.lower_bound(lb);
458                }
459                error
460            });
461
462        SearchResult {
463            error,
464            has_intersected: true,
465            reason: if rule_pruned {
466                Reason::RuleReason
467            } else {
468                Reason::Done
469            },
470        }
471    }
472
473    /// Branches on `branch_context.item`, looks the child up in the cache
474    /// (seeding it when new), searches it and backtracks.
475    fn process_branch(
476        &mut self,
477        cover: &mut Cover,
478        path: &mut SearchPath,
479        branch_context: &mut RuleContext,
480        similarity_cover: &mut SimilarityCover,
481        candidates: &[usize],
482        current_depth: usize,
483    ) -> (SearchResult, CacheKey) {
484        path.push(branch_context.item);
485        let mut size = self
486            .cache
487            .keyed_by_cover()
488            .then(|| cover.branch_on(branch_context.item));
489        let branch_index = self.cache.insert(&path.to_sorted_vec(), cover);
490        let branch_key = branch_index.to_cache_key();
491
492        if branch_index.is_new() {
493            let size = *size.get_or_insert_with(|| cover.branch_on(branch_context.item));
494            branch_context.support(size);
495            let error = self.compute_leaf_error(cover);
496            branch_context.leaf_error(error.0);
497            branch_context.node_upper_bound(f64::INFINITY);
498
499            self.cache.update_node(&branch_key).map(|updater| {
500                updater
501                    .leaf_error(error.0)
502                    .output(error.1)
503                    .lower_bound(branch_context.node_lower_bound)
504                    .size(size)
505            });
506        } else {
507            self.statistics.increment_cache_hits();
508            if let Some(node) = self.cache.node(&branch_key) {
509                branch_context.error(node.error());
510                branch_context.support(node.size());
511                branch_context.node_upper_bound(node.upper_bound());
512                branch_context.leaf_error(node.leaf_error())
513            }
514        }
515        let first_result = self.recursive_search(
516            cover,
517            path,
518            candidates,
519            current_depth + 1,
520            branch_context.item,
521            branch_index,
522            size.is_some(),
523            similarity_cover,
524            branch_context,
525        );
526
527        self.backtrack(
528            cover,
529            path,
530            branch_index,
531            &branch_context.item,
532            &first_result,
533            similarity_cover,
534        );
535
536        (first_result, branch_key)
537    }
538
539    /// Evaluates the time limit, then the rules of `rule_type`. Returns
540    /// `(continue, reason, best error of the node)`.
541    fn evaluate(
542        &mut self,
543        context: &RuleContext,
544        key: &CacheKey,
545        rule_type: RuleType,
546    ) -> (bool, Reason, f64) {
547        let time_result = self.evaluate_node(context, key, RuleType::Time);
548        if !time_result.0 {
549            return time_result;
550        }
551        self.evaluate_node(context, key, rule_type)
552    }
553
554    /// Evaluates one set of rules and applies its decision to the cached node.
555    fn evaluate_node(
556        &mut self,
557        context: &RuleContext,
558        key: &CacheKey,
559        rule_type: RuleType,
560    ) -> (bool, Reason, f64) {
561        let result = match rule_type {
562            RuleType::Node => self.node_rules.evaluate(context),
563            RuleType::Search => self.search_rules.evaluate(context),
564            RuleType::Time => self.time_rule.evaluate(context),
565            RuleType::Similarity => self.similarity_rule.evaluate(context),
566        };
567
568        let mut error = f64::INFINITY;
569        if let Some(mut updater) = self.cache.update_node(key) {
570            if let Some(bound) = result.modified_bound {
571                updater = updater.upper_bound(bound);
572            }
573
574            if result.optimal.unwrap_or(false) {
575                updater = updater.optimal();
576            }
577
578            if result.leaf.unwrap_or(false) {
579                updater = updater.leaf();
580            }
581
582            if rule_type == RuleType::Similarity {
583                updater = updater.lower_bound(context.node_lower_bound)
584            }
585            error = updater.get_error();
586        }
587
588        (result.continue_search, result.reason, error)
589    }
590
591    /// `(error, prediction)` of the current node as a leaf, from its class
592    /// counts or its instance ids depending on the configuration.
593    fn compute_leaf_error(&self, cover: &mut Cover) -> (f64, f64) {
594        if self.config.data_type == NodeDataType::ClassesSupport {
595            return self.error_fn.compute(&cover.labels_count());
596        }
597        self.error_fn.compute(&cover.to_vec())
598    }
599
600    /// `(seconds, error)` of every improvement of the tree at the root, in
601    /// the order they were found.
602    pub fn incumbents(&self) -> &[(f64, f64)] {
603        &self.incumbents
604    }
605
606    /// The cache of subproblems, with the bounds the search stored.
607    pub fn cache(&self) -> &C {
608        &self.cache
609    }
610
611    /// Counters of the search.
612    pub fn statistics(&self) -> &SearchStatistics {
613        &self.statistics
614    }
615
616    /// Seconds since the search started.
617    pub fn elapsed_seconds(&self) -> f64 {
618        self.time_rule.elapsed_seconds()
619    }
620
621    /// Whether the time limit is reached.
622    pub fn time_is_exhausted(&self) -> bool {
623        self.time_rule.exhausted()
624    }
625
626    /// Chooses which branch of `attribute` to search first, and the lower
627    /// bounds of both. With dynamic branching, the branch with the higher
628    /// known lower bound goes first, so it leaves a tighter bound for the
629    /// other one.
630    fn determine_branch_strategy(
631        &self,
632        attribute: usize,
633        path: &mut SearchPath,
634        cover: &mut Cover,
635        similarity: &SimilarityCover,
636    ) -> BranchingChoice {
637        let mut branch_first = 0;
638        let mut bounds = [0.0, 0.0];
639
640        match self.config.branching_policy {
641            BranchingPolicy::Default => {}
642            BranchingPolicy::Dynamic => {
643                bounds = self.get_cached_branch_bounds(attribute, path, cover);
644                if self.config.use_similarity_lb() {
645                    self.enhance_bounds_with_similarity(&mut bounds, attribute, cover, similarity);
646                }
647                branch_first = (bounds[1] > bounds[0]) as usize;
648            }
649        }
650
651        let first_bound = bounds[branch_first];
652        let second_bound = bounds[1 - branch_first];
653
654        (branch_first, first_bound, second_bound)
655    }
656
657    /// Lower bounds of both branches of `attribute` from the cache: the error
658    /// of a solved branch, the recorded lower bound otherwise.
659    fn get_cached_branch_bounds(
660        &self,
661        attribute: usize,
662        path: &mut SearchPath,
663        cover: &mut Cover,
664    ) -> [f64; 2] {
665        let keyed_by_cover = self.cache.keyed_by_cover();
666        let mut bounds = [0.0; 2];
667        for (branch, lb) in bounds.iter_mut().enumerate() {
668            let branch_item = item(attribute, branch);
669            path.push(branch_item);
670            if keyed_by_cover {
671                cover.branch_on(branch_item);
672            }
673            *lb = self
674                .cache
675                .find(&path.to_sorted_vec(), cover)
676                .map_or(0.0, |node| node.known_lower_bound());
677            if keyed_by_cover {
678                cover.backtrack();
679            }
680            path.remove(&branch_item)
681        }
682        bounds
683    }
684
685    /// The best proven lower bound of a cached subproblem, 0 if unknown.
686    fn known_lower_bound(&self, key: &CacheKey) -> f64 {
687        self.cache
688            .node(key)
689            .map_or(0.0, |node| node.known_lower_bound())
690    }
691
692    /// Raises the branch lower bounds with the similarity bound.
693    fn enhance_bounds_with_similarity(
694        &self,
695        bounds: &mut [f64; 2],
696        attribute: usize,
697        cover: &mut Cover,
698        similarity: &SimilarityCover,
699    ) {
700        for (branch, lb) in bounds.iter_mut().enumerate() {
701            let branch_item = item(attribute, branch);
702            cover.branch_on(branch_item);
703            let similarity_lb = similarity.compute_similarity(cover.sparse());
704            *lb = lb.max(similarity_lb);
705            cover.backtrack()
706        }
707    }
708
709    /// Undoes the branching on `item`, recording the child for the similarity
710    /// bound when it was cut by its lower bound.
711    fn backtrack(
712        &mut self,
713        cover: &mut Cover,
714        path: &mut SearchPath,
715        index: Index,
716        item: &usize,
717        search_result: &SearchResult,
718        similarity: &mut SimilarityCover,
719    ) {
720        if !search_result.has_intersected {
721            cover.branch_on(*item);
722        }
723
724        if self.config.use_similarity_lb() && search_result.reason == Reason::LowerBoundConstrained
725        {
726            let key = index.to_cache_key();
727            if let Some(node) = self.cache.node(&key) {
728                similarity.update(cover.sparse(), node.lower_bound())
729            }
730        }
731        cover.backtrack();
732        path.remove(item);
733    }
734
735    /// Solves the node with the depth-2 solver and caches the tree it returns.
736    fn apply_specialized_depth2_search(
737        &mut self,
738        cover: &mut Cover,
739        _candidates: &[usize],
740        parent_index: Index,
741        upper_bound: f64,
742        path: &mut SearchPath,
743        depth: usize,
744    ) -> Result<SearchResult, FitError> {
745        let key = parent_index.to_cache_key();
746        if let Some(node) = self.cache.node(&key) {
747            if upper_bound < node.lower_bound() {
748                return Ok(SearchResult {
749                    error: node.error(),
750                    has_intersected: true,
751                    reason: Reason::LowerBoundConstrained,
752                });
753            }
754        }
755        let tree_result = self
756            .depth2_search
757            .fit(self.config.base.min_support, depth, cover, None);
758        match tree_result {
759            Err(err) => Err(err),
760            Ok(tree) => {
761                let error = tree.root_error();
762                self.cache_depth2_tree(cover, path, parent_index, &tree, tree.get_root_index());
763
764                Ok(SearchResult {
765                    error,
766                    has_intersected: true,
767                    reason: Reason::FromSpecializedAlgorithm,
768                })
769            }
770        }
771    }
772
773    /// Stores every node of a depth-2 tree in the cache, as solved. The nodes
774    /// below the root get their own leaf error and prediction, from `cover`.
775    fn cache_depth2_tree(
776        &mut self,
777        cover: &mut Cover,
778        path: &mut SearchPath,
779        index: Index,
780        tree: &Tree,
781        tree_index: usize,
782    ) {
783        let key = index.to_cache_key();
784        let error = tree.node_error(tree_index);
785        let Some(test) = tree.node_test(tree_index) else {
786            self.cache
787                .update_node(&key)
788                .map(|updater| updater.leaf().upper_bound(EXACT).optimal());
789            return;
790        };
791        self.cache
792            .update_node(&key)
793            .map(|updater| updater.error(error).test(test).upper_bound(EXACT).optimal());
794
795        let (left, right) = tree.node_children(tree_index);
796        for (branch, tree_branch_index) in [left, right].into_iter().enumerate() {
797            let branch_item = item(test, branch);
798            path.push(branch_item);
799            let size = cover.branch_on(branch_item);
800            let (leaf_error, label) = self.compute_leaf_error(cover);
801            let branch_index = self.cache.insert(&path.to_sorted_vec(), cover);
802            self.cache
803                .update_node(&branch_index.to_cache_key())
804                .map(|updater| updater.leaf_error(leaf_error).output(label).size(size));
805            self.cache_depth2_tree(cover, path, branch_index, tree, tree_branch_index);
806            cover.backtrack();
807            path.remove(&branch_item);
808        }
809    }
810
811    /// Converts a cache entry into the content of a tree node.
812    pub fn cache_entry_to_tree_entry(&self, cache_entry: &CacheEntry) -> NodeInfos {
813        NodeInfos {
814            error: cache_entry.error(),
815            out: if cache_entry.is_leaf() {
816                Some(cache_entry.out())
817            } else {
818                None
819            },
820            test: if cache_entry.is_leaf() {
821                None
822            } else {
823                Some(cache_entry.test())
824            },
825            metric: None,
826        }
827    }
828
829    /// Rebuilds the best tree from the cache, starting at the root, and
830    /// returns its error, summed over its leaves.
831    fn build_solution_tree(&mut self, cover: &mut Cover) -> Option<f64> {
832        let mut tree = Tree::default();
833        let mut path = SearchPath::new();
834        let mut error = None;
835        if let Some(cache_root) = self.cache.root() {
836            // The search only records trees that beat the root as a leaf. When
837            // none does (e.g. a single class), the root keeps an infinite error
838            // and the leaf is the answer, if within `max_error`.
839            let unsolved = cache_root.error().is_infinite() && !cache_root.is_leaf();
840            if unsolved && cache_root.leaf_error() < self.config.base.max_error {
841                tree.add_root(TreeNode::new(NodeInfos {
842                    test: None,
843                    error: cache_root.leaf_error(),
844                    metric: None,
845                    out: Some(cache_root.out()),
846                }));
847                error = Some(cache_root.leaf_error());
848            } else {
849                let tree_entry = self.cache_entry_to_tree_entry(cache_root);
850                let root = tree.add_root(TreeNode::new(tree_entry));
851                error =
852                    Some(self.build_tree_branches(cache_root, &mut path, cover, &mut tree, root));
853            }
854        }
855        self.tree = tree;
856        error
857    }
858
859    /// Adds the subtree of `node` below the tree node at `index`, and returns
860    /// its error. `cover` is on the instances of `node`.
861    fn build_tree_branches(
862        &self,
863        node: &CacheEntry,
864        path: &mut SearchPath,
865        cover: &mut Cover,
866        tree: &mut Tree,
867        index: usize,
868    ) -> f64 {
869        if node.is_leaf() || !node.has_valid_test() {
870            return node.error();
871        }
872
873        let mut error = 0.0;
874        for branch in 0..2 {
875            let branch_item = item(node.test(), branch);
876            path.push(branch_item);
877            cover.branch_on(branch_item);
878            if let Some(child) = self.cache.find(&path.to_sorted_vec(), cover) {
879                let child_entry = self.cache_entry_to_tree_entry(child);
880                let child_index = tree.add_node(index, branch == 0, TreeNode::new(child_entry));
881                error += self.build_tree_branches(child, path, cover, tree, child_index);
882            }
883            cover.backtrack();
884            path.remove(&branch_item);
885        }
886        tree.update_node(index).map(|updater| updater.error(error));
887        error
888    }
889}
890
891#[cfg(test)]
892mod dl85_test {
893    use crate::algorithms::common::errors::NativeError;
894    use crate::algorithms::common::heuristics::NoHeuristic;
895    use crate::algorithms::common::types::OptimalDepth2Policy;
896    use crate::algorithms::optimal::depth2::ErrorMinimizer;
897    use crate::algorithms::optimal::dl85::DL85Builder;
898    use crate::algorithms::TreeSearchAlgorithm;
899    use crate::caching::Trie;
900    use crate::reader::data_reader::DataReader;
901    use std::path::Path;
902
903    #[test]
904    fn run() -> Result<(), Box<dyn std::error::Error>> {
905        let reader = DataReader::default();
906        let path = Path::new("test_data/anneal.txt");
907        let mut cover = reader.read_file(path)?;
908
909        let error_fn = Box::<NativeError>::default();
910
911        let depth2 = Box::new(ErrorMinimizer::new(error_fn.clone()));
912
913        let mut algo = DL85Builder::default()
914            .max_depth(2)
915            .min_support(50)
916            .max_time(10.0)
917            .specialization(OptimalDepth2Policy::Enabled)
918            .cache(Box::<Trie>::default())
919            .heuristic(Box::<NoHeuristic>::default())
920            .depth2_search(depth2)
921            .error_function(error_fn)
922            .build()?;
923        algo.fit(&mut cover)?;
924
925        println!("Search statistics: {:#?}", algo.statistics);
926        println!("Execution time: {:.3}s", algo.time_rule.elapsed_seconds());
927
928        println!("{}", algo.tree);
929
930        Ok(())
931    }
932}