1use super::drawing::{bell, coordinate, settle, span, stream, unit};
4use super::{Random, Sampler};
5use crate::{Dimension, Goal, Point, Setting, Space};
6
7#[derive(Debug, Clone, PartialEq)]
15pub struct Tpe {
16 pub goal: Goal,
18 pub startup: usize,
21 pub candidates: usize,
24 pub quantile: f64,
27 pub seed: u64,
29}
30
31impl Tpe {
32 pub fn ask(&self, space: &Space, trial: usize, seen: &[(Point, Option<f64>)]) -> Option<Point> {
34 if space.is_empty() {
35 return None;
36 }
37 let scored: Vec<(&Point, f64)> = seen
40 .iter()
41 .filter_map(|(point, at)| at.filter(|at| !at.is_nan()).map(|at| (point, at)))
42 .collect();
43 if scored.len() < self.startup.max(2) {
44 return Random { seed: self.seed }.ask(space, trial, seen);
45 }
46
47 let (good, mut bad) = self.split(&scored);
48 bad.extend(
55 seen.iter()
56 .filter(|(_, at)| at.is_none())
57 .map(|(point, _)| point),
58 );
59 let mut state = stream(self.seed, trial);
60 let mut best: Option<Point> = None;
61 let mut best_gain = f64::NEG_INFINITY;
62
63 for _ in 0..self.candidates.max(1) {
64 let mut settings = Vec::with_capacity(space.len());
65 let mut gain = 0.0;
66 for (name, dimension) in space.dimensions() {
67 let (setting, said) = propose(
68 dimension,
69 &placed(&good, name, dimension),
70 &placed(&bad, name, dimension),
71 &mut state,
72 );
73 gain += said;
74 settings.push((name.clone(), setting));
75 }
76 if gain > best_gain {
77 best_gain = gain;
78 best = Some(Point::of(settings));
79 }
80 }
81 best
82 }
83
84 fn split<'a>(&self, scored: &[(&'a Point, f64)]) -> (Vec<&'a Point>, Vec<&'a Point>) {
88 let mut order: Vec<(&Point, f64)> = scored.to_vec();
89 order.sort_by(|(_, one), (_, other)| match self.goal {
90 Goal::Minimize => one.total_cmp(other),
91 Goal::Maximize => other.total_cmp(one),
92 });
93 let many = (self.quantile.clamp(0.0, 1.0) * order.len() as f64).ceil() as usize;
94 let many = many.clamp(1, order.len() - 1);
95 let (good, bad) = order.split_at(many);
96 (
97 good.iter().map(|(point, _)| *point).collect(),
98 bad.iter().map(|(point, _)| *point).collect(),
99 )
100 }
101}
102
103fn placed(points: &[&Point], name: &str, dimension: &Dimension) -> Vec<f64> {
107 points
108 .iter()
109 .filter_map(|point| point.get(name))
110 .filter_map(|setting| coordinate(dimension, setting))
111 .collect()
112}
113
114fn propose(dimension: &Dimension, good: &[f64], bad: &[f64], state: &mut u64) -> (Setting, f64) {
117 if good.is_empty() {
118 let (from, to) = span(dimension);
121 return (settle(dimension, from + unit(state) * (to - from)), 0.0);
122 }
123 match dimension {
124 Dimension::Choice(options) => among(options, good, bad, state),
125 _ => along(dimension, good, bad, state),
126 }
127}
128
129fn along(dimension: &Dimension, good: &[f64], bad: &[f64], state: &mut u64) -> (Setting, f64) {
131 let (from, to) = span(dimension);
132 let place = drawn_from(good, from, to, state);
133 let gain = density(good, from, to, place).ln() - density(bad, from, to, place).ln();
134 (settle(dimension, place), gain)
135}
136
137fn among(options: &[String], good: &[f64], bad: &[f64], state: &mut u64) -> (Setting, f64) {
140 let tally = |seen: &[f64]| {
141 let mut counts = vec![1.0; options.len()];
142 for &which in seen {
143 counts[(which as usize).min(options.len() - 1)] += 1.0;
144 }
145 let total: f64 = counts.iter().sum();
146 (counts, total)
147 };
148 let (liked, liked_total) = tally(good);
149 let (disliked, disliked_total) = tally(bad);
150
151 let mut left = unit(state) * liked_total;
152 let mut which = options.len() - 1;
153 for (option, count) in liked.iter().enumerate() {
154 if left < *count {
155 which = option;
156 break;
157 }
158 left -= count;
159 }
160
161 let gain = (liked[which] / liked_total).ln() - (disliked[which] / disliked_total).ln();
162 (Setting::Choice(options[which].clone()), gain)
163}
164
165fn drawn_from(values: &[f64], from: f64, to: f64, state: &mut u64) -> f64 {
168 let prior = 1.0 / (values.len() as f64 + 1.0);
169 let place = if unit(state) < prior {
170 bell(state, (from + to) / 2.0, (to - from) / 2.0)
171 } else {
172 let which = ((unit(state) * values.len() as f64) as usize).min(values.len() - 1);
173 bell(state, values[which], width_of(values, to - from))
174 };
175 place.clamp(from, to)
176}
177
178fn density(values: &[f64], from: f64, to: f64, place: f64) -> f64 {
182 if values.is_empty() {
183 return bell_at(place, (from + to) / 2.0, (to - from) / 2.0);
184 }
185 let many = values.len() as f64;
186 let prior = 1.0 / (many + 1.0);
187 let width = width_of(values, to - from);
188 let mut how = prior * bell_at(place, (from + to) / 2.0, (to - from) / 2.0);
189 for &value in values {
190 how += (1.0 - prior) / many * bell_at(place, value, width);
191 }
192 how.max(f64::MIN_POSITIVE)
193}
194
195fn width_of(values: &[f64], span: f64) -> f64 {
199 let many = values.len() as f64;
200 let mean = values.iter().sum::<f64>() / many;
201 let spread = (values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / many).sqrt();
202 (1.06 * spread * many.powf(-0.2)).clamp(span / 100.0, span)
203}
204
205fn bell_at(place: f64, centre: f64, width: f64) -> f64 {
207 let from_centre = (place - centre) / width;
208 (-0.5 * from_centre * from_centre).exp() / (width * std::f64::consts::TAU.sqrt())
209}
210
211impl From<Tpe> for Sampler {
212 fn from(how: Tpe) -> Self {
213 Self::Tpe(how)
214 }
215}