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
26pub 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 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 #[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 pub fn config(&self) -> DL85Config {
128 self.config
129 }
130
131 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 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 #[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 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 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 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 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 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 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 pub fn incumbents(&self) -> &[(f64, f64)] {
603 &self.incumbents
604 }
605
606 pub fn cache(&self) -> &C {
608 &self.cache
609 }
610
611 pub fn statistics(&self) -> &SearchStatistics {
613 &self.statistics
614 }
615
616 pub fn elapsed_seconds(&self) -> f64 {
618 self.time_rule.elapsed_seconds()
619 }
620
621 pub fn time_is_exhausted(&self) -> bool {
623 self.time_rule.exhausted()
624 }
625
626 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 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 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 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 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 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 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 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 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 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 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}