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