contree/algorithms/
mod.rs1mod continuous_tree;
2mod contree_lds;
3mod depth2;
4mod interval_pruner;
5mod shared;
6
7use crate::common::{
8 Cost, FitOutcome, PointSelector, ScheduleKind, SearchConfig, SearchError, SearchStatus,
9 Statistics,
10};
11use crate::data::view::DataView;
12use crate::data::Dataset;
13use crate::tree::Tree;
14pub use continuous_tree::ConTree;
15pub use contree_lds::ConTreeLds;
16
17pub(crate) fn support_feasible_splits(
26 possible_splits: &[usize],
27 view_len: usize,
28 min_sup: usize,
29) -> std::ops::Range<usize> {
30 let min_sup = min_sup.max(1);
31 if view_len < 2 * min_sup {
32 return 0..0;
33 }
34 let first = possible_splits.partition_point(|&p| p < min_sup);
35 let last = possible_splits.partition_point(|&p| view_len - p.min(view_len) >= min_sup);
36 first..last.max(first)
37}
38
39pub fn validate(config: &SearchConfig, dataset: &Dataset) -> Result<(), SearchError> {
41 if dataset.count() == 0 {
42 return Err(SearchError::EmptyDataset);
43 }
44 if dataset.num_features() == 0 {
45 return Err(SearchError::NoFeatures);
46 }
47 if !dataset.is_prepared() {
48 return Err(SearchError::UnpreparedDataset);
49 }
50 if config.min_sup == 0 {
51 return Err(SearchError::InvalidParameter {
52 name: "min_sup",
53 reason: "must be at least 1".to_string(),
54 });
55 }
56 if 2 * config.min_sup > dataset.count() {
57 return Err(SearchError::InvalidParameter {
58 name: "min_sup",
59 reason: format!(
60 "{} leaves no room for a split with n_samples={}",
61 config.min_sup,
62 dataset.count()
63 ),
64 });
65 }
66 if config.max_time.is_nan() || config.max_time <= 0.0 {
67 return Err(SearchError::InvalidParameter {
68 name: "max_time",
69 reason: "must be a positive number of seconds".to_string(),
70 });
71 }
72 Ok(())
73}
74
75pub enum GenericConTree {
77 Normal(ConTree),
79 LDS(ConTreeLds),
81}
82
83impl GenericConTree {
84 #[allow(clippy::too_many_arguments)]
87 pub fn new(
88 min_sup: usize,
89 max_depth: usize,
90 max_time: f64,
91 max_error: Cost,
92 split_selection_strategy: PointSelector,
93 max_gap: Cost,
94 use_heuristic: bool,
95 fast_d2: bool,
96 use_lds: bool,
97 ) -> Self {
98 match use_lds {
99 true => Self::LDS(ConTreeLds::new(
100 min_sup,
101 max_depth,
102 max_time,
103 max_error,
104 split_selection_strategy,
105 max_gap,
106 use_heuristic,
107 fast_d2,
108 )),
109 false => Self::Normal(ConTree::new(
110 min_sup,
111 max_depth,
112 max_time,
113 max_error,
114 split_selection_strategy,
115 max_gap,
116 use_heuristic,
117 fast_d2,
118 )),
119 }
120 }
121
122 pub fn with_budget_schedule(self, schedule: ScheduleKind) -> Self {
125 match self {
126 GenericConTree::LDS(solver) => GenericConTree::LDS(solver.with_schedule(schedule)),
127 other => other,
128 }
129 }
130
131 pub fn fit(&mut self, dataset: &Dataset) -> Result<FitOutcome, SearchError> {
133 match self {
134 GenericConTree::Normal(solver) => solver.fit(dataset),
135 GenericConTree::LDS(solver) => solver.fit(dataset),
136 }
137 }
138
139 pub fn partial_fit(&mut self, view: &DataView<'_>) -> Result<bool, SearchError> {
144 match self {
145 GenericConTree::Normal(_) => Ok(true),
146 GenericConTree::LDS(solver) => Ok(solver.partial_fit(view)),
147 }
148 }
149
150 pub fn stats(&self) -> Statistics {
152 match self {
153 GenericConTree::Normal(solver) => solver.statistics(),
154 GenericConTree::LDS(solver) => *solver.statistics(),
155 }
156 }
157
158 pub fn status(&self) -> SearchStatus {
160 match self {
161 GenericConTree::Normal(solver) => solver.status(),
162 GenericConTree::LDS(solver) => solver.status(),
163 }
164 }
165
166 pub fn tree(&mut self) -> Tree {
168 match self {
169 GenericConTree::Normal(solver) => solver.tree.clone(),
170 GenericConTree::LDS(solver) => solver.get_solution_tree(),
171 }
172 }
173}