robopoker/clustering/
histogram.rs

1use crate::cards::observation::Observation;
2use crate::clustering::abstraction::Abstraction;
3use crate::Probability;
4use std::collections::BTreeMap;
5use std::ops::AddAssign;
6
7type Equity = Probability;
8
9/// A distribution over arbitrary Abstractions.
10///
11/// The sum of the weights is the total number of samples.
12/// The weight of an abstraction is the number of times it was sampled.
13#[derive(Debug, Default, Clone)]
14pub struct Histogram {
15    norm: usize,
16    weights: BTreeMap<Abstraction, usize>,
17}
18
19impl Histogram {
20    pub fn weight(&self, abstraction: &Abstraction) -> f32 {
21        self.weights.get(abstraction).copied().unwrap_or(0usize) as f32 / self.norm as f32
22    }
23
24    /// all witnessed Abstractions.
25    /// treat this like an unordered array
26    /// even though we use BTreeMap for struct.
27    pub fn support(&self) -> Vec<&Abstraction> {
28        self.weights.keys().collect()
29    }
30
31    /// insert the Abstraction into our support,
32    /// incrementing its local weight,
33    /// incrementing our global norm.
34    pub fn witness(mut self, abstraction: Abstraction) -> Self {
35        self.norm.add_assign(1usize);
36        self.weights
37            .entry(abstraction)
38            .or_insert(0usize)
39            .add_assign(1usize);
40        self
41    }
42
43    /// empty the whole thing,
44    /// reset the norm to zero,
45    /// clear the weights
46    pub fn destroy(&mut self) {
47        self.norm = 0;
48        self.weights.clear();
49    }
50
51    /// absorb the other histogram into this one.
52    /// Note that this implicitly assumes sum normalizations are the same,
53    /// which should hold for now...
54    /// until we implement Observation isomorphisms!
55    pub fn absorb(&mut self, other: &Self) {
56        assert!(self.norm == other.norm);
57        self.norm += other.norm;
58        for (key, count) in other.weights.iter() {
59            self.weights
60                .entry(key.to_owned())
61                .or_insert(0usize)
62                .add_assign(count.to_owned());
63        }
64    }
65
66    /// exhaustive calculation of all
67    /// possible Rivers and Showdowns,
68    /// naive to strategy of course.
69    ///
70    /// ONLY WORKS FOR STREET::TURN
71    /// ONLY WORKS FOR STREET::TURN
72    pub fn equity(&self) -> Equity {
73        assert!(matches!(
74            self.weights.keys().next(),
75            Some(Abstraction::Equity(_))
76        ));
77        self.posterior().iter().map(|(x, y)| x * y).sum()
78    }
79
80    /// this yields the posterior equity distribution
81    /// at the turn street.
82    /// this is the only street we explicitly can calculate
83    /// the Probability of transitioning into a Probability
84    ///     Probability -> Probability
85    /// vs  Probability -> Abstraction
86    /// hence a distribution over showdown equities.
87    ///
88    /// ONLY WORKS FOR STREET::TURN
89    /// ONLY WORKS FOR STREET::TURN
90    pub fn posterior(&self) -> Vec<(Equity, Probability)> {
91        assert!(matches!(
92            self.weights.keys().next(),
93            Some(Abstraction::Equity(_))
94        ));
95        self.weights
96            .iter()
97            .map(|(&key, &value)| (key, value as f32 / self.norm as f32))
98            .map(|(k, v)| (Equity::from(k), Probability::from(v)))
99            .collect()
100    }
101}
102
103impl From<Observation> for Histogram {
104    fn from(ref turn: Observation) -> Self {
105        assert!(turn.street() == crate::cards::street::Street::Turn);
106        turn.outnodes()
107            .into_iter()
108            .map(|river| Abstraction::from(river.equity()))
109            .collect::<Vec<Abstraction>>()
110            .into()
111    }
112}
113
114impl From<Vec<Abstraction>> for Histogram {
115    fn from(a: Vec<Abstraction>) -> Self {
116        a.into_iter()
117            .fold(Histogram::default(), |hist, abs| hist.witness(abs))
118    }
119}
120
121impl std::fmt::Display for Histogram {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        // 1. interpret each key of the Histogram as probability
124        // 2. they should already be sorted bc BTreeMap
125        let ref distribution = self.posterior();
126        // 3. Create 32 bins for the x-axis
127        let n_x_bins = 32;
128        let ref mut bins = vec![0.0; n_x_bins];
129        for (key, value) in distribution {
130            let x = key * n_x_bins as f32;
131            let x = x.floor() as usize;
132            let x = x.min(n_x_bins - 1);
133            bins[x] += value;
134        }
135        // 4. Print the histogram
136        writeln!(f)?;
137        let n_y_bins = 10;
138        for y in (1..=n_y_bins).rev() {
139            for bin in bins.iter().copied() {
140                if bin >= y as f32 / n_y_bins as f32 {
141                    write!(f, "█")?;
142                } else if bin >= y as f32 / n_y_bins as f32 - 0.75 / n_y_bins as f32 {
143                    write!(f, "▆")?;
144                } else if bin >= y as f32 / n_y_bins as f32 - 0.50 / n_y_bins as f32 {
145                    write!(f, "▄")?;
146                } else if bin >= y as f32 / n_y_bins as f32 - 0.25 / n_y_bins as f32 {
147                    write!(f, "▂")?;
148                } else {
149                    write!(f, " ")?;
150                }
151            }
152            writeln!(f)?;
153        }
154        // 5. Print x-axis
155        for _ in 0..n_x_bins {
156            write!(f, "-")?;
157        }
158        // 6. flush to STDOUT
159        Ok(())
160    }
161}