Skip to main content

radiate_core/stats/
set.rs

1use crate::{
2    Metric, MetricUpdate,
3    stats::{Tag, TagKind, defaults::try_add_tag_from_str, fmt},
4};
5use radiate_utils::intern;
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::HashMap,
10    fmt::{Debug, Display},
11};
12
13pub(super) const METRIC_SET: &str = "metric_set";
14
15#[derive(PartialEq)]
16pub struct MetricSetSummary {
17    pub metrics: usize,
18    pub updates: f32,
19}
20
21#[derive(Clone, Default, PartialEq)]
22pub struct MetricSet {
23    metrics: HashMap<&'static str, Metric>,
24    set_stats: Metric,
25}
26
27impl MetricSet {
28    pub fn new() -> Self {
29        MetricSet {
30            metrics: HashMap::new(),
31            set_stats: Metric::new(METRIC_SET),
32        }
33    }
34
35    #[inline(always)]
36    pub fn keys(&self) -> Vec<&'static str> {
37        self.metrics.keys().cloned().collect()
38    }
39
40    #[inline(always)]
41    pub fn flush_all_into(&mut self, target: &mut MetricSet) {
42        for (key, mut m) in self.metrics.drain() {
43            if let Some(target_metric) = target.metrics.get_mut(key) {
44                target_metric.update_from(m);
45            } else {
46                try_add_tag_from_str(&mut m);
47                target.metrics.insert(key, m);
48            }
49        }
50
51        target.set_stats.update_from(self.set_stats.clone());
52        self.clear();
53    }
54
55    #[inline(always)]
56    pub fn upsert<'a>(&mut self, metric: impl Into<MetricSetUpdate<'a>>) {
57        let update = metric.into();
58        match update {
59            MetricSetUpdate::Many(metrics) => {
60                for metric in metrics {
61                    self.add_or_update_internal(metric);
62                }
63            }
64            MetricSetUpdate::Single(metric) => {
65                self.add_or_update_internal(metric);
66            }
67            MetricSetUpdate::NamedSingle(name, metric_update) => {
68                self.set_stats.apply_update(1);
69                if let Some(m) = self.metrics.get_mut(name) {
70                    m.apply_update(metric_update);
71                    return;
72                }
73
74                let new_name = radiate_utils::intern_name_as_snake_case(name);
75                if let Some(m) = self.metrics.get_mut(&new_name) {
76                    m.apply_update(metric_update);
77                } else {
78                    let mut metric = Metric::new(new_name);
79                    try_add_tag_from_str(&mut metric);
80                    metric.apply_update(metric_update);
81                    self.add(metric);
82                }
83            }
84        }
85    }
86
87    pub fn iter_tagged<'a>(
88        &'a self,
89        tag: TagKind,
90    ) -> impl Iterator<Item = (&'static str, &'a Metric)> {
91        self.metrics.iter().filter_map(move |(k, m)| {
92            if m.tags().has(tag) {
93                Some((*k, m))
94            } else {
95                None
96            }
97        })
98    }
99
100    pub fn iter_stats(&self) -> impl Iterator<Item = &Metric> {
101        self.metrics.values().filter(|m| m.statistic().is_some())
102    }
103
104    pub fn iter_times(&self) -> impl Iterator<Item = &Metric> {
105        self.metrics
106            .values()
107            .filter(|m| m.time_statistic().is_some())
108    }
109
110    pub fn tags(&self) -> impl Iterator<Item = TagKind> {
111        self.metrics
112            .values()
113            .fold(Tag::empty(), |acc, m| acc.union(m.tags()))
114            .into_iter()
115    }
116
117    #[inline(always)]
118    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Metric)> {
119        self.metrics.iter().map(|(name, metric)| (*name, metric))
120    }
121
122    #[inline(always)]
123    pub fn add(&mut self, metric: Metric) {
124        self.metrics.insert(intern!(metric.name()), metric);
125    }
126
127    #[inline(always)]
128    pub fn get(&self, name: &str) -> Option<&Metric> {
129        self.metrics.get(name)
130    }
131
132    #[inline(always)]
133    pub fn get_from_string(&self, name: String) -> Option<&Metric> {
134        self.metrics.get(name.as_str())
135    }
136
137    #[inline(always)]
138    pub fn clear(&mut self) {
139        for (_, m) in self.metrics.iter_mut() {
140            m.clear_values();
141        }
142
143        self.set_stats.clear_values();
144    }
145
146    #[inline(always)]
147    pub fn contains_key(&self, name: &str) -> bool {
148        self.metrics.contains_key(intern!(name))
149    }
150
151    #[inline(always)]
152    pub fn len(&self) -> usize {
153        self.metrics.len()
154    }
155
156    #[inline(always)]
157    pub fn summary(&self) -> MetricSetSummary {
158        MetricSetSummary {
159            metrics: self.metrics.len(),
160            updates: self.set_stats.statistic().map(|s| s.sum()).unwrap_or(0.0),
161        }
162    }
163
164    pub fn dashboard(&self) -> String {
165        fmt::render_full(self).unwrap_or_default()
166    }
167
168    // --- Default accessors ---
169    pub fn time(&self) -> Option<&Metric> {
170        self.get(super::metric_names::TIME)
171    }
172
173    pub fn score(&self) -> Option<&Metric> {
174        self.get(super::metric_names::SCORES)
175    }
176
177    pub fn improvements(&self) -> Option<&Metric> {
178        self.get(super::metric_names::BEST_SCORE_IMPROVEMENT)
179    }
180
181    pub fn age(&self) -> Option<&Metric> {
182        self.get(super::metric_names::AGE)
183    }
184
185    pub fn replace_age(&self) -> Option<&Metric> {
186        self.get(super::metric_names::REPLACE_AGE)
187    }
188
189    pub fn replace_invalid(&self) -> Option<&Metric> {
190        self.get(super::metric_names::REPLACE_INVALID)
191    }
192
193    pub fn genome_size(&self) -> Option<&Metric> {
194        self.get(super::metric_names::GENOME_SIZE)
195    }
196
197    pub fn front_size(&self) -> Option<&Metric> {
198        self.get(super::metric_names::FRONT_SIZE)
199    }
200
201    pub fn front_comparisons(&self) -> Option<&Metric> {
202        self.get(super::metric_names::FRONT_COMPARISONS)
203    }
204
205    pub fn front_removals(&self) -> Option<&Metric> {
206        self.get(super::metric_names::FRONT_REMOVALS)
207    }
208
209    pub fn front_additions(&self) -> Option<&Metric> {
210        self.get(super::metric_names::FRONT_ADDITIONS)
211    }
212
213    pub fn front_entropy(&self) -> Option<&Metric> {
214        self.get(super::metric_names::FRONT_ENTROPY)
215    }
216
217    pub fn unique_members(&self) -> Option<&Metric> {
218        self.get(super::metric_names::UNIQUE_MEMBERS)
219    }
220
221    pub fn unique_scores(&self) -> Option<&Metric> {
222        self.get(super::metric_names::UNIQUE_SCORES)
223    }
224
225    pub fn new_children(&self) -> Option<&Metric> {
226        self.get(super::metric_names::NEW_CHILDREN)
227    }
228
229    pub fn survivor_count(&self) -> Option<&Metric> {
230        self.get(super::metric_names::SURVIVOR_COUNT)
231    }
232
233    pub fn carryover_rate(&self) -> Option<&Metric> {
234        self.get(super::metric_names::CARRYOVER_RATE)
235    }
236
237    pub fn evaluation_count(&self) -> Option<&Metric> {
238        self.get(super::metric_names::EVALUATION_COUNT)
239    }
240
241    pub fn diversity_ratio(&self) -> Option<&Metric> {
242        self.get(super::metric_names::DIVERSITY_RATIO)
243    }
244
245    pub fn score_volatility(&self) -> Option<&Metric> {
246        self.get(super::metric_names::SCORE_VOLATILITY)
247    }
248
249    pub fn species_count(&self) -> Option<&Metric> {
250        self.get(super::metric_names::SPECIES_COUNT)
251    }
252
253    pub fn species_age_fail(&self) -> Option<&Metric> {
254        self.get(super::metric_names::SPECIES_AGE_FAIL)
255    }
256
257    pub fn species_distance_dist(&self) -> Option<&Metric> {
258        self.get(super::metric_names::SPECIES_DISTANCE_DIST)
259    }
260
261    pub fn species_created(&self) -> Option<&Metric> {
262        self.get(super::metric_names::SPECIES_CREATED)
263    }
264
265    pub fn species_died(&self) -> Option<&Metric> {
266        self.get(super::metric_names::SPECIES_DIED)
267    }
268
269    pub fn species_age(&self) -> Option<&Metric> {
270        self.get(super::metric_names::SPECIES_AGE)
271    }
272
273    pub fn species_size(&self) -> Option<&Metric> {
274        self.get(super::metric_names::SPECIES_SIZE)
275    }
276
277    pub fn species_evenness(&self) -> Option<&Metric> {
278        self.get(super::metric_names::SPECIES_EVENNESS)
279    }
280
281    pub fn largest_species_share(&self) -> Option<&Metric> {
282        self.get(super::metric_names::LARGEST_SPECIES_SHARE)
283    }
284
285    fn add_or_update_internal(&mut self, mut metric: Metric) {
286        self.set_stats.apply_update(1);
287        if let Some(existing) = self.metrics.get_mut(metric.name()) {
288            existing.update_from(metric);
289        } else {
290            try_add_tag_from_str(&mut metric);
291            self.metrics.insert(intern!(metric.name()), metric);
292        }
293    }
294}
295
296impl Display for MetricSet {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        let summary = self.summary();
299        let out = format!(
300            "[{} metrics, {:.0} updates]",
301            summary.metrics, summary.updates
302        );
303        write!(f, "{out}\n{}", fmt::render_full(self).unwrap_or_default())?;
304        Ok(())
305    }
306}
307
308impl Debug for MetricSet {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        write!(f, "MetricSet {{\n")?;
311        write!(f, "{}\n", fmt::render_dashboard(&self).unwrap_or_default())?;
312        write!(f, "}}")
313    }
314}
315
316#[cfg(feature = "serde")]
317impl Serialize for MetricSet {
318    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
319    where
320        S: serde::Serializer,
321    {
322        let metrics = self
323            .metrics
324            .iter()
325            .map(|(_, metric)| metric.clone())
326            .collect::<Vec<Metric>>();
327        metrics.serialize(serializer)
328    }
329}
330
331#[cfg(feature = "serde")]
332impl<'de> Deserialize<'de> for MetricSet {
333    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
334    where
335        D: serde::Deserializer<'de>,
336    {
337        let metrics = Vec::<Metric>::deserialize(deserializer)?;
338
339        let mut metric_set = MetricSet::new();
340        for metric in metrics {
341            metric_set.add(metric);
342        }
343
344        Ok(metric_set)
345    }
346}
347
348pub enum MetricSetUpdate<'a> {
349    Many(Vec<Metric>),
350    Single(Metric),
351    NamedSingle(&'static str, MetricUpdate<'a>),
352}
353
354impl From<Vec<Metric>> for MetricSetUpdate<'_> {
355    fn from(metrics: Vec<Metric>) -> Self {
356        MetricSetUpdate::Many(metrics)
357    }
358}
359
360impl From<Metric> for MetricSetUpdate<'_> {
361    fn from(metric: Metric) -> Self {
362        MetricSetUpdate::Single(metric)
363    }
364}
365
366impl<'a, U> From<(&'static str, U)> for MetricSetUpdate<'a>
367where
368    U: Into<MetricUpdate<'a>>,
369{
370    fn from((name, update): (&'static str, U)) -> Self {
371        MetricSetUpdate::NamedSingle(name, update.into())
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    const EPSILON: f32 = 1e-5;
380
381    fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
382        (a - b).abs() <= eps
383    }
384
385    fn assert_stat_eq(m: &Metric, count: i32, mean: f32, var: f32, min: f32, max: f32) {
386        assert_eq!(m.count(), count);
387        assert!(approx_eq(m.value_mean().unwrap(), mean, EPSILON), "mean");
388        assert!(approx_eq(m.value_variance().unwrap(), var, EPSILON), "var");
389        assert!(approx_eq(m.value_min().unwrap(), min, EPSILON), "min");
390        assert!(approx_eq(m.value_max().unwrap(), max, EPSILON), "max");
391    }
392
393    fn stats_of(values: &[f32]) -> (i32, f32, f32, f32, f32) {
394        // sample variance (n-1), matches your Statistic::variance
395        let n = values.len() as i32;
396        if n == 0 {
397            return (0, 0.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY);
398        }
399        let mean = values.iter().sum::<f32>() / values.len() as f32;
400
401        let mut m2 = 0.0_f32;
402        for &v in values {
403            let d = v - mean;
404            m2 += d * d;
405        }
406
407        let var = if n == 1 { 0.0 } else { m2 / (n as f32 - 1.0) };
408
409        let min = values.iter().cloned().fold(f32::INFINITY, f32::min);
410        let max = values.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
411
412        (n, mean, var, min, max)
413    }
414
415    #[test]
416    fn metric_set_flush_all_into_merges_metrics() {
417        let mut a = MetricSet::new();
418        let mut b = MetricSet::new();
419
420        a.upsert(("scores", &[1.0, 2.0, 3.0][..]));
421        b.upsert(("scores", &[10.0, 20.0][..]));
422
423        // move a into b
424        a.flush_all_into(&mut b);
425
426        let m = b.get("scores").unwrap();
427        let combined = [1.0, 2.0, 3.0, 10.0, 20.0];
428        let (n, mean, var, min, max) = stats_of(&combined);
429        assert_stat_eq(m, n, mean, var, min, max);
430    }
431}